fied 0.2.0 → 0.2.1

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 +117 -24
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -166,10 +166,18 @@ var DEFAULT_RELAY = "https://fied.app";
166
166
  var MSG_TERMINAL_OUTPUT = 1;
167
167
  var MSG_TERMINAL_INPUT = 2;
168
168
  var MSG_RESIZE = 3;
169
+ var RESIZE_MIN_COLS = 20;
170
+ var RESIZE_MAX_COLS = 1e3;
171
+ var RESIZE_MIN_ROWS = 5;
172
+ var RESIZE_MAX_ROWS = 300;
173
+ var MAX_INVALID_RESIZE_FRAMES = 5;
169
174
  var RECONNECT_BASE_MS = 1e3;
170
175
  var RECONNECT_MAX_MS = 3e4;
171
176
  async function share(options) {
172
- const relay = options.relay ?? DEFAULT_RELAY;
177
+ const relayTarget = parseRelayTarget(
178
+ options.relay ?? DEFAULT_RELAY,
179
+ options.allowInsecureRelay ?? process.env.FIED_ALLOW_INSECURE_RELAY === "1"
180
+ );
173
181
  const sessions = listSessions();
174
182
  if (sessions.length === 0) {
175
183
  console.error("No tmux sessions found. Start one with: tmux new -s mysession");
@@ -205,36 +213,75 @@ async function share(options) {
205
213
  console.log(` Size: ${cols}x${rows}`);
206
214
  console.log("");
207
215
  }
208
- const bridge = new RelayBridge(relay, cryptoKey, keyFragment, pty, options.background);
216
+ const bridge = new RelayBridge(relayTarget, cryptoKey, keyFragment, pty, options.background);
209
217
  const onUrl = (url) => {
210
218
  if (options.background) {
211
219
  addSession({
212
220
  pid: process.pid,
213
221
  tmuxSession: targetSession,
214
222
  url,
215
- relay,
223
+ relay: relayTarget.httpBase.toString(),
216
224
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
217
225
  });
218
226
  }
219
227
  };
220
228
  await bridge.connect(onUrl);
221
- const shutdown = () => {
229
+ let closed = false;
230
+ const cleanup = () => {
231
+ if (closed) return;
232
+ closed = true;
222
233
  bridge.destroy();
223
- pty.kill();
224
- if (options.background) removeSession(process.pid);
234
+ try {
235
+ pty.kill();
236
+ } catch {
237
+ }
238
+ if (options.background) {
239
+ removeSession(process.pid);
240
+ }
241
+ };
242
+ const exitNow = (code) => {
243
+ cleanup();
244
+ process.exit(code);
225
245
  };
226
- process.on("SIGINT", shutdown);
227
- process.on("SIGTERM", shutdown);
246
+ process.once("SIGINT", () => exitNow(0));
247
+ process.once("SIGTERM", () => exitNow(0));
228
248
  await new Promise((resolve) => {
229
249
  pty.onExit(() => {
230
- bridge.destroy();
231
- if (options.background) removeSession(process.pid);
250
+ cleanup();
232
251
  resolve();
233
252
  });
234
253
  });
235
254
  }
236
- async function createSession(relay) {
237
- const res = await fetch(`${relay}/api/sessions`, { method: "POST" });
255
+ function parseRelayTarget(relay, allowInsecureRelay) {
256
+ let parsed;
257
+ try {
258
+ parsed = new URL(relay);
259
+ } catch {
260
+ throw new Error(`Invalid relay URL: ${relay}`);
261
+ }
262
+ const isHttps = parsed.protocol === "https:";
263
+ const isHttp = parsed.protocol === "http:";
264
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
265
+ if (!isHttps) {
266
+ if (!(isHttp && isLocalhost && allowInsecureRelay)) {
267
+ throw new Error(
268
+ "Relay must use https://. For local development, use http://localhost with --allow-insecure-relay or FIED_ALLOW_INSECURE_RELAY=1"
269
+ );
270
+ }
271
+ }
272
+ const httpBase = new URL(parsed.toString());
273
+ httpBase.hash = "";
274
+ httpBase.search = "";
275
+ if (!httpBase.pathname.endsWith("/")) {
276
+ httpBase.pathname = `${httpBase.pathname}/`;
277
+ }
278
+ const wsBase = new URL(httpBase.toString());
279
+ wsBase.protocol = httpBase.protocol === "https:" ? "wss:" : "ws:";
280
+ return { httpBase, wsBase };
281
+ }
282
+ async function createSession(relayHttpBase) {
283
+ const url = new URL("api/sessions", relayHttpBase);
284
+ const res = await fetch(url.toString(), { method: "POST" });
238
285
  if (!res.ok) {
239
286
  throw new Error(`Failed to create session: ${res.status} ${res.statusText}`);
240
287
  }
@@ -243,8 +290,8 @@ async function createSession(relay) {
243
290
  }
244
291
  var WS_CONNECT_TIMEOUT_MS = 1e4;
245
292
  var RelayBridge = class {
246
- constructor(relay, key, keyFragment, pty, silent = false) {
247
- this.relay = relay;
293
+ constructor(relayTarget, key, keyFragment, pty, silent = false) {
294
+ this.relayTarget = relayTarget;
248
295
  this.key = key;
249
296
  this.keyFragment = keyFragment;
250
297
  this.pty = pty;
@@ -263,18 +310,24 @@ var RelayBridge = class {
263
310
  encoder = new TextEncoder();
264
311
  decoder = new TextDecoder();
265
312
  sessionId = null;
313
+ onUrl = null;
314
+ invalidResizeFrames = 0;
266
315
  async connect(onUrl) {
267
316
  if (this.destroyed) return;
317
+ if (onUrl) {
318
+ this.onUrl = onUrl;
319
+ }
268
320
  if (!this.sessionId) {
269
321
  try {
270
- this.sessionId = await createSession(this.relay);
322
+ this.sessionId = await createSession(this.relayTarget.httpBase);
271
323
  } catch {
272
324
  if (!this.silent) console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
273
325
  this.scheduleReconnect();
274
326
  return;
275
327
  }
276
- const url = `${this.relay}/s/${this.sessionId}#${this.keyFragment}`;
277
- onUrl?.(url);
328
+ const shareUrl = new URL(`s/${this.sessionId}`, this.relayTarget.httpBase);
329
+ const url = `${shareUrl.toString()}#${this.keyFragment}`;
330
+ this.onUrl?.(url);
278
331
  if (!this.silent) {
279
332
  console.log(` \x1B[1mShare this link:\x1B[0m`);
280
333
  console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
@@ -284,8 +337,9 @@ var RelayBridge = class {
284
337
  console.log("");
285
338
  }
286
339
  }
287
- const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${this.sessionId}/ws?role=host`;
288
- const ws = new WebSocket(wsUrl);
340
+ const wsUrl = new URL(`api/sessions/${this.sessionId}/ws`, this.relayTarget.wsBase);
341
+ wsUrl.searchParams.set("role", "host");
342
+ const ws = new WebSocket(wsUrl.toString());
289
343
  ws.binaryType = "arraybuffer";
290
344
  this.ws = ws;
291
345
  this.connectTimeout = setTimeout(() => {
@@ -316,10 +370,22 @@ var RelayBridge = class {
316
370
  this.pty.write(this.decoder.decode(plaintext));
317
371
  } else if (frame.type === MSG_RESIZE) {
318
372
  const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
319
- const { cols, rows } = JSON.parse(this.decoder.decode(plaintext));
320
- this.pty.resize(cols, rows);
373
+ const resize = parseResizePayload(this.decoder.decode(plaintext));
374
+ if (!resize) {
375
+ this.invalidResizeFrames += 1;
376
+ if (this.invalidResizeFrames >= MAX_INVALID_RESIZE_FRAMES) {
377
+ ws.close(1008, "invalid resize frames");
378
+ }
379
+ return;
380
+ }
381
+ this.invalidResizeFrames = 0;
382
+ this.pty.resize(resize.cols, resize.rows);
383
+ }
384
+ } catch (err) {
385
+ if (!this.silent) {
386
+ const detail = err instanceof Error ? err.message : String(err);
387
+ console.error(` \x1B[33mIncoming frame rejected:\x1B[0m ${detail}`);
321
388
  }
322
- } catch {
323
389
  }
324
390
  });
325
391
  ws.on("close", () => {
@@ -365,10 +431,30 @@ var RelayBridge = class {
365
431
  const { iv, ciphertext } = await encrypt(this.key, plaintext);
366
432
  const frame = frameMessage(type, iv, ciphertext);
367
433
  this.ws.send(frame);
368
- } catch {
434
+ } catch (err) {
435
+ if (!this.silent) {
436
+ const detail = err instanceof Error ? err.message : String(err);
437
+ console.error(` \x1B[33mEncryption failed:\x1B[0m ${detail}`);
438
+ }
369
439
  }
370
440
  }
371
441
  };
442
+ function parseResizePayload(payload) {
443
+ let parsed;
444
+ try {
445
+ parsed = JSON.parse(payload);
446
+ } catch {
447
+ return null;
448
+ }
449
+ if (!parsed || typeof parsed !== "object") return null;
450
+ const typed = parsed;
451
+ if (!Number.isInteger(typed.cols) || !Number.isInteger(typed.rows)) return null;
452
+ const cols = typed.cols;
453
+ const rows = typed.rows;
454
+ if (cols < RESIZE_MIN_COLS || cols > RESIZE_MAX_COLS) return null;
455
+ if (rows < RESIZE_MIN_ROWS || rows > RESIZE_MAX_ROWS) return null;
456
+ return { cols, rows };
457
+ }
372
458
 
373
459
  // src/prompt.ts
374
460
  import { createInterface } from "node:readline";
@@ -438,6 +524,7 @@ if (args.includes("--help") || args.includes("-h")) {
438
524
  \x1B[1mOptions:\x1B[0m
439
525
  --session, -s <name> tmux session to share (auto-detected if only one)
440
526
  --relay <url> relay server URL (default: https://fied.app)
527
+ --allow-insecure-relay allow http://localhost relay (dev only)
441
528
  --help, -h show this help
442
529
 
443
530
  \x1B[1mExamples:\x1B[0m
@@ -454,6 +541,8 @@ if (args.includes("--__daemon")) {
454
541
  options.session = args[++i];
455
542
  } else if (args[i] === "--relay" && args[i + 1]) {
456
543
  options.relay = args[++i];
544
+ } else if (args[i] === "--allow-insecure-relay") {
545
+ options.allowInsecureRelay = true;
457
546
  }
458
547
  }
459
548
  share({ ...options, background: true }).catch(() => process.exit(1));
@@ -466,11 +555,14 @@ if (args.includes("--__daemon")) {
466
555
  async function main() {
467
556
  let relay;
468
557
  let session;
558
+ let allowInsecureRelay = false;
469
559
  for (let i = 0; i < args.length; i++) {
470
560
  if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
471
561
  session = args[++i];
472
562
  } else if (args[i] === "--relay" && args[i + 1]) {
473
563
  relay = args[++i];
564
+ } else if (args[i] === "--allow-insecure-relay") {
565
+ allowInsecureRelay = true;
474
566
  } else if (!args[i].startsWith("-")) {
475
567
  continue;
476
568
  } else {
@@ -531,6 +623,7 @@ async function main() {
531
623
  const binPath = fileURLToPath(import.meta.url);
532
624
  const childArgs = ["--__daemon", "--session", session];
533
625
  if (relay) childArgs.push("--relay", relay);
626
+ if (allowInsecureRelay) childArgs.push("--allow-insecure-relay");
534
627
  const child = spawnChild(process.execPath, [binPath, ...childArgs], {
535
628
  detached: true,
536
629
  stdio: "ignore"
@@ -543,7 +636,7 @@ async function main() {
543
636
  console.error("");
544
637
  setTimeout(() => process.exit(0), 500);
545
638
  } else {
546
- await share({ session, relay });
639
+ await share({ session, relay, allowInsecureRelay });
547
640
  }
548
641
  }
549
642
  function timeSince(date) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fied",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Share your tmux session in the browser with end-to-end encryption",
5
5
  "type": "module",
6
6
  "bin": {