@threadbase-sh/streamer 1.52.1 → 1.52.3

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/README.md CHANGED
@@ -35,7 +35,7 @@ node dist/cli.cjs serve --verbose --local-no-auth
35
35
 
36
36
  #### Server address
37
37
 
38
- The server listens on `http://localhost:8766` (WebSocket at `ws://localhost:8766/ws`).
38
+ By default, the server listens on port `8766` on all interfaces (WebSocket path: `/ws`).
39
39
 
40
40
  #### Automatic updates
41
41
 
@@ -43,7 +43,7 @@ npm and Homebrew installs can auto-update: [docs/guides/auto-update.md](docs/gui
43
43
 
44
44
  ## Remote Access
45
45
 
46
- The server listens on port 8766 on all interfaces, so devices on the same LAN can already reach it. To let the mobile app reach it from *outside* your LAN, expose it via a tunnel — the fastest is a Cloudflare quick-tunnel (no account needed):
46
+ The server listens on port 8766 on all interfaces by default, so devices on the same LAN can already reach it. To restrict it to this machine, start it with `tb-streamer serve --host 127.0.0.1`. To let the mobile app reach it from *outside* your LAN, expose it via a tunnel — the fastest is a Cloudflare quick-tunnel (no account needed):
47
47
 
48
48
  > **Before you do this, read [Security](#security--read-this-before-exposing-the-server).** A tunnel makes the server reachable from the public internet, and the API key is the only thing between a stranger and a shell on this machine.
49
49
 
@@ -72,11 +72,15 @@ npm run db:validate # check for missing/duplicate/orphaned project_id data
72
72
 
73
73
  The key lives at `~/.threadbase/server.yaml` as plaintext (`chmod 0600`), which is the same posture as `~/.aws/credentials` or `~/.npmrc`. Rotate it with `POST /api/auth/rotate` if it leaks; re-pair your devices afterwards.
74
74
 
75
- **The server listens on all network interfaces**, not just loopback — `httpServer.listen(port)` is called without a host, which is Node's listen-everywhere default, and there is currently no flag to narrow it. So every device on your LAN can already reach port 8766, and the API key is the only thing stopping them. On a home network that is usually fine; on café Wi-Fi, a co-working space, or a corporate VLAN it is not.
75
+ **The server listens on all network interfaces by default**, not just loopback. So every device on your LAN can already reach port 8766, and the API key is the only thing stopping them. On a home network that is usually fine; on café Wi-Fi, a co-working space, or a corporate VLAN it is not. To bind only to loopback, run:
76
+
77
+ ```bash
78
+ tb-streamer serve --host 127.0.0.1
79
+ ```
76
80
 
77
81
  **A tunnel extends that to the entire internet.** From that moment the key is the only thing between a stranger and your filesystem.
78
82
 
79
- If you need it strictly local today, bind it yourself at the firewall or run it inside a network namespace — the server will not do it for you. Tracked as [#517](https://github.com/RonenMars/threadbase-streamer/issues/517).
83
+ For stricter network policies, also use your firewall or a network namespace.
80
84
 
81
85
  ### There is no spend limit
82
86
 
package/dist/cli.cjs CHANGED
@@ -152274,6 +152274,7 @@ var StreamerServer = class {
152274
152274
  dbPool = null;
152275
152275
  dbInstanceId = null;
152276
152276
  disableDb = false;
152277
+ host;
152277
152278
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
152278
152279
  skipStartupWarmup;
152279
152280
  autoResumeOnBoot;
@@ -152395,6 +152396,7 @@ var StreamerServer = class {
152395
152396
  }
152396
152397
  this.verbose = config2.verbose ?? false;
152397
152398
  this.disableDb = config2.disableDb ?? false;
152399
+ this.host = config2.host;
152398
152400
  this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
152399
152401
  this.autoResumeOnBoot = config2.autoResumeOnBoot ?? false;
152400
152402
  this.scanProfiles = config2.scanProfiles;
@@ -153047,7 +153049,7 @@ var StreamerServer = class {
153047
153049
  await runMigrations2(this.dbPool);
153048
153050
  this.log.info("Database migrations applied", { event: "db.migrations_applied" });
153049
153051
  }
153050
- await this.bindWithRetry(port);
153052
+ await this.bindWithRetry(port, this.host);
153051
153053
  if (!this.ptyManager.isRemote()) {
153052
153054
  this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
153053
153055
  this.idleReaperTimer.unref?.();
@@ -153056,7 +153058,8 @@ var StreamerServer = class {
153056
153058
  {
153057
153059
  this.log.info(`Streamer server listening on port ${port}`, {
153058
153060
  port,
153059
- event: "server.listening"
153061
+ event: "server.listening",
153062
+ ...this.host !== void 0 && { host: this.host }
153060
153063
  });
153061
153064
  try {
153062
153065
  this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
@@ -153270,15 +153273,15 @@ var StreamerServer = class {
153270
153273
  // Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call
153271
153274
  // site in listen() for why the race exists (kickstart -k relaunch). Total
153272
153275
  // worst case ≈ 6 × 500 ms = 3 s before the final attempt rethrows.
153273
- async bindWithRetry(port, attempts = 6, delayMs = 500) {
153276
+ async bindWithRetry(port, host, attempts = 6, delayMs = 500) {
153274
153277
  this.binding = true;
153275
153278
  try {
153276
- await this.bindWithRetryLoop(port, attempts, delayMs);
153279
+ await this.bindWithRetryLoop(port, host, attempts, delayMs);
153277
153280
  } finally {
153278
153281
  this.binding = false;
153279
153282
  }
153280
153283
  }
153281
- async bindWithRetryLoop(port, attempts, delayMs) {
153284
+ async bindWithRetryLoop(port, host, attempts, delayMs) {
153282
153285
  for (let attempt = 1; attempt <= attempts; attempt++) {
153283
153286
  try {
153284
153287
  await new Promise((resolve4, reject) => {
@@ -153292,7 +153295,11 @@ var StreamerServer = class {
153292
153295
  };
153293
153296
  this.httpServer.once("error", onError);
153294
153297
  this.httpServer.once("listening", onListening);
153295
- this.httpServer.listen(port);
153298
+ if (host === void 0) {
153299
+ this.httpServer.listen(port);
153300
+ } else {
153301
+ this.httpServer.listen(port, host);
153302
+ }
153296
153303
  });
153297
153304
  return;
153298
153305
  } catch (err) {
@@ -153300,13 +153307,18 @@ var StreamerServer = class {
153300
153307
  if (e.code === "EADDRINUSE" && attempt === attempts) {
153301
153308
  this.log.error(
153302
153309
  `port ${port} still busy (EADDRINUSE) after ${attempts} attempts; giving up`,
153303
- { port, attempts, event: "server.bind_failed" }
153310
+ {
153311
+ port,
153312
+ attempts,
153313
+ event: "server.bind_failed",
153314
+ ...host !== void 0 && { host }
153315
+ }
153304
153316
  );
153305
153317
  }
153306
153318
  if (e.code !== "EADDRINUSE" || attempt === attempts) throw err;
153307
153319
  this.log.debug?.(
153308
153320
  `port ${port} busy (EADDRINUSE), retry ${attempt}/${attempts - 1} in ${delayMs}ms`,
153309
- { port, attempt, event: "server.bind_retry" }
153321
+ { port, attempt, event: "server.bind_retry", ...host !== void 0 && { host } }
153310
153322
  );
153311
153323
  await new Promise((r) => setTimeout(r, delayMs));
153312
153324
  }
@@ -157841,7 +157853,7 @@ function registerProdCommands(program3) {
157841
157853
  var log14 = getLogger("cli");
157842
157854
  var program2 = new Command();
157843
157855
  program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
157844
- program2.command("serve").description("Start the streamer server").option("-p, --port <number>", "Port to listen on", "8766").option("--api-key <key>", "API key for authentication").option("--local-no-auth", "Skip auth for localhost requests", false).option("-v, --verbose", "Verbose output", false).option("--log-menubar-requests", "Log /healthz requests from the menubar app", false).option("--browse-root <path>", "Root directory for file browsing").option(
157856
+ program2.command("serve").description("Start the streamer server").option("-p, --port <number>", "Port to listen on", "8766").option("--host <address>", "Host/address to bind the server to (default: all interfaces)").option("--api-key <key>", "API key for authentication").option("--local-no-auth", "Skip auth for localhost requests", false).option("-v, --verbose", "Verbose output", false).option("--log-menubar-requests", "Log /healthz requests from the menubar app", false).option("--browse-root <path>", "Root directory for file browsing").option(
157845
157857
  "--public-url <url>",
157846
157858
  "Public URL clients should use to reach this server (https:// required, except localhost). Falls back to THREADBASE_PUBLIC_URL env or public_url: in ~/.threadbase/server.yaml."
157847
157859
  ).option(
@@ -158029,6 +158041,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
158029
158041
  }
158030
158042
  const server = new StreamerServer({
158031
158043
  port: resolvedPort,
158044
+ host: opts.host,
158032
158045
  apiKey,
158033
158046
  apiKeySource: opts.apiKey ? "cli" : "config",
158034
158047
  localNoAuth: opts.localNoAuth,