termbridge 0.2.0 → 0.3.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 CHANGED
@@ -1788,6 +1788,22 @@ var parseArgs = (argv) => {
1788
1788
  options.port = port;
1789
1789
  continue;
1790
1790
  }
1791
+ if (current === "--proxy") {
1792
+ const proxy = parseNumber(args.shift());
1793
+ if (!proxy || proxy <= 0) {
1794
+ throw new Error("invalid proxy port");
1795
+ }
1796
+ options.proxy = proxy;
1797
+ continue;
1798
+ }
1799
+ if (current === "--dev-proxy-url") {
1800
+ const url = args.shift();
1801
+ if (!url) {
1802
+ throw new Error("missing dev proxy URL");
1803
+ }
1804
+ options.devProxyUrl = url;
1805
+ continue;
1806
+ }
1791
1807
  if (current === "--session") {
1792
1808
  const session = args.shift();
1793
1809
  if (!session) {
@@ -1825,6 +1841,7 @@ Usage:
1825
1841
 
1826
1842
  Options:
1827
1843
  --port <port> Bind the local server to a fixed port
1844
+ --proxy <port> Proxy a local dev server (e.g., Vite) through termbridge
1828
1845
  --session <name> Use a specific tmux session name
1829
1846
  --kill-on-exit Kill the tmux session when the CLI exits
1830
1847
  --no-qr Disable QR code output
@@ -1866,7 +1883,8 @@ var defaultDeps = {
1866
1883
  spawnPty: pty.spawn,
1867
1884
  env: process.env,
1868
1885
  defaultCols: 80,
1869
- defaultRows: 24
1886
+ defaultRows: 24,
1887
+ _skipSpawnHelperCheck: false
1870
1888
  };
1871
1889
  var ensureSpawnHelperExecutable = () => {
1872
1890
  if (spawnHelperChecked || process.platform === "win32") {
@@ -1939,7 +1957,9 @@ var createTmuxBackend = (deps = {}) => {
1939
1957
  if (entry.pty) {
1940
1958
  return entry.pty;
1941
1959
  }
1942
- ensureSpawnHelperExecutable();
1960
+ if (!runtime._skipSpawnHelperCheck) {
1961
+ ensureSpawnHelperExecutable();
1962
+ }
1943
1963
  const ptyInstance = runtime.spawnPty("tmux", ["attach-session", "-t", entry.session.name], {
1944
1964
  name: "xterm-256color",
1945
1965
  cols: entry.cols,
@@ -2246,9 +2266,9 @@ var createTerminalRegistry = () => {
2246
2266
  };
2247
2267
 
2248
2268
  // src/server/server.ts
2249
- import { createServer as createHttpServer } from "http";
2269
+ import { createServer as createHttpServer, request as httpRequest } from "http";
2250
2270
  import { randomBytes as randomBytes3 } from "crypto";
2251
- import { WebSocketServer } from "ws";
2271
+ import { WebSocketServer, WebSocket as WsWebSocket } from "ws";
2252
2272
 
2253
2273
  // ../packages/shared/src/index.ts
2254
2274
  var TERMINAL_CONTROL_KEYS = [
@@ -2356,11 +2376,11 @@ var isAllowedOrigin = (origin, host) => {
2356
2376
  };
2357
2377
  var allowedControlKeys = new Set(TERMINAL_CONTROL_KEYS);
2358
2378
  var parseClientMessage = (payload) => {
2359
- const size = typeof payload === "string" ? payload.length : payload.byteLength;
2379
+ const size = typeof payload === "string" ? payload.length : Array.isArray(payload) ? payload.reduce((sum, buf) => sum + buf.length, 0) : payload.byteLength;
2360
2380
  if (size > MAX_WS_MESSAGE_SIZE) {
2361
2381
  return { ok: false, error: "too_large" };
2362
2382
  }
2363
- const text = typeof payload === "string" ? payload : payload.toString();
2383
+ const text = typeof payload === "string" ? payload : Array.isArray(payload) ? Buffer.concat(payload).toString() : payload.toString();
2364
2384
  try {
2365
2385
  const parsed = JSON.parse(text);
2366
2386
  if (parsed.type === "input" && typeof parsed.data === "string") {
@@ -2385,24 +2405,44 @@ var sendWsMessage = (socket, message) => {
2385
2405
  };
2386
2406
  var createSessionName = () => `termbridge-${randomBytes3(4).toString("hex")}`;
2387
2407
  var createAppServer = (deps) => {
2388
- const staticHandler = createStaticHandler(deps.uiDistPath, "/app");
2408
+ const staticHandler = createStaticHandler(deps.uiDistPath, "/__tb/app");
2389
2409
  const wss = new WebSocketServer({ noServer: true });
2390
2410
  const connectionInfo = /* @__PURE__ */ new WeakMap();
2411
+ const proxyRequest = (request, response, targetPath, search) => {
2412
+ const targetUrl = `http://localhost:${deps.proxyPort}${targetPath}${search}`;
2413
+ const proxyHeaders = { ...request.headers };
2414
+ delete proxyHeaders.cookie;
2415
+ delete proxyHeaders.host;
2416
+ proxyHeaders.host = `localhost:${deps.proxyPort}`;
2417
+ const proxyReq = httpRequest(
2418
+ targetUrl,
2419
+ { method: request.method, headers: proxyHeaders },
2420
+ (proxyRes) => {
2421
+ response.writeHead(proxyRes.statusCode, proxyRes.headers);
2422
+ proxyRes.pipe(response);
2423
+ }
2424
+ );
2425
+ proxyReq.on("error", () => {
2426
+ response.statusCode = 502;
2427
+ response.end("proxy error");
2428
+ });
2429
+ request.pipe(proxyReq);
2430
+ };
2391
2431
  const server = createHttpServer(async (request, response) => {
2392
2432
  const url = new URL(request.url, `http://${request.headers.host}`);
2393
- if (request.method === "GET" && url.pathname === "/healthz") {
2433
+ if (request.method === "GET" && url.pathname === "/__tb/healthz") {
2394
2434
  response.statusCode = 200;
2395
2435
  response.end("ok");
2396
2436
  return;
2397
2437
  }
2398
- if (request.method === "GET" && url.pathname === "/") {
2438
+ if (request.method === "GET" && url.pathname === "/" && !deps.proxyPort) {
2399
2439
  response.statusCode = 302;
2400
- response.setHeader("Location", "/app");
2440
+ response.setHeader("Location", "/__tb/app");
2401
2441
  response.end();
2402
2442
  return;
2403
2443
  }
2404
- if (request.method === "GET" && url.pathname.startsWith("/s/")) {
2405
- const token = url.pathname.slice(3);
2444
+ if (request.method === "GET" && url.pathname.startsWith("/__tb/s/")) {
2445
+ const token = url.pathname.slice("/__tb/s/".length);
2406
2446
  const ip = getIp(request);
2407
2447
  if (!deps.redemptionLimiter.allow(ip)) {
2408
2448
  response.statusCode = 429;
@@ -2417,11 +2457,11 @@ var createAppServer = (deps) => {
2417
2457
  }
2418
2458
  response.statusCode = 302;
2419
2459
  response.setHeader("Set-Cookie", deps.auth.createSessionCookie(session.id));
2420
- response.setHeader("Location", "/app");
2460
+ response.setHeader("Location", "/__tb/app");
2421
2461
  response.end();
2422
2462
  return;
2423
2463
  }
2424
- if (url.pathname === "/api/csrf") {
2464
+ if (url.pathname === "/__tb/api/csrf") {
2425
2465
  const session = deps.auth.getSessionFromRequest(request);
2426
2466
  if (!session) {
2427
2467
  response.statusCode = 401;
@@ -2436,7 +2476,7 @@ var createAppServer = (deps) => {
2436
2476
  response.end("not found");
2437
2477
  return;
2438
2478
  }
2439
- if (url.pathname === "/api/terminals") {
2479
+ if (url.pathname === "/__tb/api/terminals") {
2440
2480
  const session = deps.auth.getSessionFromRequest(request);
2441
2481
  if (!session) {
2442
2482
  response.statusCode = 401;
@@ -2467,15 +2507,48 @@ var createAppServer = (deps) => {
2467
2507
  return;
2468
2508
  }
2469
2509
  }
2510
+ if (request.method === "GET" && url.pathname === "/__tb/api/config") {
2511
+ const session = deps.auth.getSessionFromRequest(request);
2512
+ if (!session) {
2513
+ response.statusCode = 401;
2514
+ response.end("unauthorized");
2515
+ return;
2516
+ }
2517
+ jsonResponse(response, 200, {
2518
+ proxyPort: deps.proxyPort ?? null,
2519
+ devProxyUrl: deps.devProxyUrl ?? null
2520
+ });
2521
+ return;
2522
+ }
2470
2523
  const handled = await staticHandler(request, response);
2471
2524
  if (!handled) {
2525
+ if (deps.proxyPort && deps.auth.getSessionFromRequest(request)) {
2526
+ proxyRequest(request, response, url.pathname, url.search);
2527
+ return;
2528
+ }
2472
2529
  response.statusCode = 404;
2473
2530
  response.end("not found");
2474
2531
  }
2475
2532
  });
2476
2533
  server.on("upgrade", (request, socket, head) => {
2477
2534
  const url = new URL(request.url, `http://${request.headers.host}`);
2478
- if (!url.pathname.startsWith("/ws/terminal/")) {
2535
+ if (!url.pathname.startsWith("/__tb/ws/terminal/")) {
2536
+ if (deps.proxyPort && deps.auth.getSessionFromRequest(request)) {
2537
+ const targetUrl = `ws://localhost:${deps.proxyPort}${url.pathname}${url.search}`;
2538
+ const proxyWs = new WsWebSocket(targetUrl);
2539
+ proxyWs.on("open", () => {
2540
+ wss.handleUpgrade(request, socket, head, (clientWs) => {
2541
+ clientWs.on("message", (data) => proxyWs.send(data));
2542
+ proxyWs.on("message", (data) => clientWs.send(data));
2543
+ clientWs.on("close", () => proxyWs.close());
2544
+ proxyWs.on("close", () => clientWs.close());
2545
+ });
2546
+ });
2547
+ proxyWs.on("error", () => {
2548
+ socket.destroy();
2549
+ });
2550
+ return;
2551
+ }
2479
2552
  socket.destroy();
2480
2553
  return;
2481
2554
  }
@@ -2615,7 +2688,9 @@ var startCommand = async (options, deps = {}) => {
2615
2688
  uiDistPath: resolveUiDistPath(),
2616
2689
  auth,
2617
2690
  terminalRegistry,
2618
- terminalBackend
2691
+ terminalBackend,
2692
+ proxyPort: options.proxy,
2693
+ devProxyUrl: options.devProxyUrl
2619
2694
  });
2620
2695
  const started = await server.listen(options.port ?? 0);
2621
2696
  const localUrl = `http://127.0.0.1:${started.port}`;
@@ -2640,7 +2715,7 @@ var startCommand = async (options, deps = {}) => {
2640
2715
  await started.close();
2641
2716
  throw error;
2642
2717
  }
2643
- const redeemUrl = `${publicUrl}/s/${token}`;
2718
+ const redeemUrl = `${publicUrl}/__tb/s/${token}`;
2644
2719
  logger.info(`Local server: ${localUrl}`);
2645
2720
  logger.info(`Tunnel URL: ${redeemUrl}`);
2646
2721
  if (!options.noQr && deps.qr) {
@@ -2724,7 +2799,7 @@ var buildSessionName = (options, localUrl) => {
2724
2799
  return "termbridge";
2725
2800
  }
2726
2801
  };
2727
- var buildRedeemUrl = (result) => `${result.publicUrl}/s/${result.token}`;
2802
+ var buildRedeemUrl = (result) => `${result.publicUrl}/__tb/s/${result.token}`;
2728
2803
  var generateQr = async (text) => new Promise((resolve4) => {
2729
2804
  qrcode.generate(text, { small: true }, (output) => resolve4(output));
2730
2805
  });