termbridge 0.3.0 → 0.3.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.
package/dist/bin.js CHANGED
@@ -1828,6 +1828,22 @@ var parseArgs = (argv) => {
1828
1828
  options.tunnel = "cloudflare";
1829
1829
  continue;
1830
1830
  }
1831
+ if (current === "--tunnel-token") {
1832
+ const token = args.shift();
1833
+ if (!token) {
1834
+ throw new Error("missing tunnel token");
1835
+ }
1836
+ options.tunnelToken = token;
1837
+ continue;
1838
+ }
1839
+ if (current === "--tunnel-url") {
1840
+ const url = args.shift();
1841
+ if (!url) {
1842
+ throw new Error("missing tunnel url");
1843
+ }
1844
+ options.tunnelUrl = url;
1845
+ continue;
1846
+ }
1831
1847
  throw new Error(`unknown option: ${current}`);
1832
1848
  }
1833
1849
  return { command, options };
@@ -1842,6 +1858,8 @@ Usage:
1842
1858
  Options:
1843
1859
  --port <port> Bind the local server to a fixed port
1844
1860
  --proxy <port> Proxy a local dev server (e.g., Vite) through termbridge
1861
+ --tunnel-token <t> Use a named Cloudflare Tunnel token (requires --port)
1862
+ --tunnel-url <url> Public URL for a named tunnel (required with --tunnel-token)
1845
1863
  --session <name> Use a specific tmux session name
1846
1864
  --kill-on-exit Kill the tmux session when the CLI exits
1847
1865
  --no-qr Disable QR code output
@@ -1883,7 +1901,8 @@ var defaultDeps = {
1883
1901
  spawnPty: pty.spawn,
1884
1902
  env: process.env,
1885
1903
  defaultCols: 80,
1886
- defaultRows: 24
1904
+ defaultRows: 24,
1905
+ _skipSpawnHelperCheck: false
1887
1906
  };
1888
1907
  var ensureSpawnHelperExecutable = () => {
1889
1908
  if (spawnHelperChecked || process.platform === "win32") {
@@ -1956,7 +1975,9 @@ var createTmuxBackend = (deps = {}) => {
1956
1975
  if (entry.pty) {
1957
1976
  return entry.pty;
1958
1977
  }
1959
- ensureSpawnHelperExecutable();
1978
+ if (!runtime._skipSpawnHelperCheck) {
1979
+ ensureSpawnHelperExecutable();
1980
+ }
1960
1981
  const ptyInstance = runtime.spawnPty("tmux", ["attach-session", "-t", entry.session.name], {
1961
1982
  name: "xterm-256color",
1962
1983
  cols: entry.cols,
@@ -2059,36 +2080,83 @@ var readLines = (data, carry, onLine) => {
2059
2080
  }
2060
2081
  return remainder;
2061
2082
  };
2083
+ var normalizeLine = (line) => line.trim();
2062
2084
  var createCloudflaredProvider = (deps = {}) => {
2063
2085
  const spawn2 = deps.spawn ?? spawnCallback;
2064
2086
  let child = null;
2065
- const start = (localUrl) => {
2087
+ const start = (localUrl, options = {}) => {
2066
2088
  if (child) {
2067
2089
  return Promise.reject(new Error("cloudflared already running"));
2068
2090
  }
2069
- child = spawn2("cloudflared", ["tunnel", "--url", localUrl]);
2091
+ const token = options.token?.trim();
2092
+ const publicUrl = options.publicUrl?.trim();
2093
+ if (token && !publicUrl) {
2094
+ return Promise.reject(new Error("tunnel public URL required when using tunnel token"));
2095
+ }
2096
+ const args = token ? ["tunnel", "run", "--token", token] : ["tunnel", "--url", localUrl];
2097
+ child = spawn2("cloudflared", args);
2070
2098
  let stdoutCarry = "";
2071
2099
  let stderrCarry = "";
2100
+ const errorLines = [];
2072
2101
  return new Promise((resolve4, reject) => {
2102
+ let resolved = false;
2103
+ let resolveTimer = null;
2104
+ const resolveOnce = (url) => {
2105
+ if (resolved) {
2106
+ return;
2107
+ }
2108
+ resolved = true;
2109
+ if (resolveTimer) {
2110
+ clearTimeout(resolveTimer);
2111
+ resolveTimer = null;
2112
+ }
2113
+ resolve4({ publicUrl: url });
2114
+ };
2073
2115
  const handleLine = (line) => {
2074
- const url = parseCloudflaredUrl(line);
2116
+ const cleaned = normalizeLine(line);
2117
+ if (!cleaned) {
2118
+ return;
2119
+ }
2120
+ options.log?.(cleaned, "stdout");
2121
+ const url = parseCloudflaredUrl(cleaned);
2075
2122
  if (url) {
2076
- resolve4({ publicUrl: url });
2123
+ resolveOnce(url);
2077
2124
  }
2078
2125
  };
2079
2126
  const handleOutput = (data, isStdout) => {
2080
2127
  if (isStdout) {
2081
2128
  stdoutCarry = readLines(data.toString(), stdoutCarry, handleLine);
2082
2129
  } else {
2083
- stderrCarry = readLines(data.toString(), stderrCarry, handleLine);
2130
+ stderrCarry = readLines(data.toString(), stderrCarry, (line) => {
2131
+ const cleaned = normalizeLine(line);
2132
+ if (!cleaned) {
2133
+ return;
2134
+ }
2135
+ options.log?.(cleaned, "stderr");
2136
+ errorLines.push(cleaned);
2137
+ if (errorLines.length > 6) {
2138
+ errorLines.shift();
2139
+ }
2140
+ const url = parseCloudflaredUrl(cleaned);
2141
+ if (url) {
2142
+ resolveOnce(url);
2143
+ }
2144
+ });
2084
2145
  }
2085
2146
  };
2086
2147
  child?.stdout.on("data", (data) => handleOutput(data, true));
2087
2148
  child?.stderr.on("data", (data) => handleOutput(data, false));
2088
2149
  child?.once("error", (error) => reject(error));
2089
2150
  child?.once("exit", (code) => {
2090
- reject(new Error(`cloudflared exited (${code ?? "unknown"})`));
2151
+ if (resolved) {
2152
+ return;
2153
+ }
2154
+ const suffix = errorLines.length > 0 ? `: ${errorLines.join(" | ")}` : "";
2155
+ reject(new Error(`cloudflared exited (${code ?? "unknown"})${suffix}`));
2091
2156
  });
2157
+ if (token && publicUrl) {
2158
+ resolveTimer = setTimeout(() => resolveOnce(publicUrl), 1200);
2159
+ }
2092
2160
  });
2093
2161
  };
2094
2162
  const stop = async () => {
@@ -2373,11 +2441,11 @@ var isAllowedOrigin = (origin, host) => {
2373
2441
  };
2374
2442
  var allowedControlKeys = new Set(TERMINAL_CONTROL_KEYS);
2375
2443
  var parseClientMessage = (payload) => {
2376
- const size = typeof payload === "string" ? payload.length : payload.byteLength;
2444
+ const size = typeof payload === "string" ? payload.length : Array.isArray(payload) ? payload.reduce((sum, buf) => sum + buf.length, 0) : payload.byteLength;
2377
2445
  if (size > MAX_WS_MESSAGE_SIZE) {
2378
2446
  return { ok: false, error: "too_large" };
2379
2447
  }
2380
- const text = typeof payload === "string" ? payload : payload.toString();
2448
+ const text = typeof payload === "string" ? payload : Array.isArray(payload) ? Buffer.concat(payload).toString() : payload.toString();
2381
2449
  try {
2382
2450
  const parsed = JSON.parse(text);
2383
2451
  if (parsed.type === "input" && typeof parsed.data === "string") {
@@ -2660,6 +2728,18 @@ var parseSessionCount = (value) => {
2660
2728
  }
2661
2729
  return parsed;
2662
2730
  };
2731
+ var normalizePublicUrl = (value) => {
2732
+ let parsed;
2733
+ try {
2734
+ parsed = new URL(value);
2735
+ } catch {
2736
+ throw new Error("invalid tunnel url");
2737
+ }
2738
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2739
+ throw new Error("invalid tunnel url");
2740
+ }
2741
+ return value.endsWith("/") ? value.slice(0, -1) : value;
2742
+ };
2663
2743
  var startCommand = async (options, deps = {}) => {
2664
2744
  const logger = deps.logger ?? createDefaultLogger();
2665
2745
  const processRef = deps.process ?? process;
@@ -2674,6 +2754,16 @@ var startCommand = async (options, deps = {}) => {
2674
2754
  const terminalBackend = (deps.createTerminalBackend ?? (() => createTmuxBackend()))();
2675
2755
  const terminalRegistry = (deps.createTerminalRegistry ?? (() => createTerminalRegistry()))();
2676
2756
  const tunnelProvider = (deps.createTunnelProvider ?? (() => createCloudflaredProvider()))();
2757
+ const tunnelTokenRaw = options.tunnelToken ?? env.TERMBRIDGE_TUNNEL_TOKEN;
2758
+ const tunnelToken = tunnelTokenRaw?.trim() || void 0;
2759
+ const tunnelUrlRaw = options.tunnelUrl ?? env.TERMBRIDGE_TUNNEL_URL;
2760
+ const tunnelUrl = tunnelToken && tunnelUrlRaw ? normalizePublicUrl(tunnelUrlRaw) : void 0;
2761
+ if (tunnelToken && !options.port) {
2762
+ throw new Error("port required when using tunnel token");
2763
+ }
2764
+ if (tunnelToken && !tunnelUrl) {
2765
+ throw new Error("tunnel url required when using tunnel token");
2766
+ }
2677
2767
  const wsLimiter = createRateLimiter({ limit: 30, windowMs: 6e4 });
2678
2768
  const serverFactory = deps.createServer ?? ((serverDeps) => createAppServer({
2679
2769
  ...serverDeps,
@@ -2704,7 +2794,10 @@ var startCommand = async (options, deps = {}) => {
2704
2794
  const { token } = auth.issueToken();
2705
2795
  let publicUrl = "";
2706
2796
  try {
2707
- const result = await tunnelProvider.start(localUrl);
2797
+ const result = await tunnelProvider.start(localUrl, {
2798
+ token: tunnelToken,
2799
+ publicUrl: tunnelUrl
2800
+ });
2708
2801
  publicUrl = result.publicUrl;
2709
2802
  } catch (error) {
2710
2803
  const message = error instanceof Error ? error.message : "unknown error";
@@ -2796,7 +2889,7 @@ var buildSessionName = (options, localUrl) => {
2796
2889
  return "termbridge";
2797
2890
  }
2798
2891
  };
2799
- var buildRedeemUrl = (result) => `${result.publicUrl}/s/${result.token}`;
2892
+ var buildRedeemUrl = (result) => `${result.publicUrl}/__tb/s/${result.token}`;
2800
2893
  var generateQr = async (text) => new Promise((resolve4) => {
2801
2894
  qrcode.generate(text, { small: true }, (output) => resolve4(output));
2802
2895
  });