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.
- package/dist/bin.js +117 -24
- 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
|
|
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(
|
|
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
|
-
|
|
229
|
+
let closed = false;
|
|
230
|
+
const cleanup = () => {
|
|
231
|
+
if (closed) return;
|
|
232
|
+
closed = true;
|
|
222
233
|
bridge.destroy();
|
|
223
|
-
|
|
224
|
-
|
|
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.
|
|
227
|
-
process.
|
|
246
|
+
process.once("SIGINT", () => exitNow(0));
|
|
247
|
+
process.once("SIGTERM", () => exitNow(0));
|
|
228
248
|
await new Promise((resolve) => {
|
|
229
249
|
pty.onExit(() => {
|
|
230
|
-
|
|
231
|
-
if (options.background) removeSession(process.pid);
|
|
250
|
+
cleanup();
|
|
232
251
|
resolve();
|
|
233
252
|
});
|
|
234
253
|
});
|
|
235
254
|
}
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
|
247
|
-
this.
|
|
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.
|
|
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
|
|
277
|
-
|
|
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 =
|
|
288
|
-
|
|
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
|
|
320
|
-
|
|
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) {
|