From b1d233069a3d1b0684490bd3ba11276f83c00d79 Mon Sep 17 00:00:00 2001 From: Sancho Neves-Graca Date: Sun, 26 Jul 2026 03:21:09 +0000 Subject: [PATCH] feat(server): add --host / SIDESHOW_HOST to bind one address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serve() was called with no hostname, so node listened on every interface, and the startup line printed "localhost" unconditionally — hiding that. There was no flag or env var to restrict it. The default is unchanged (every interface: what containers and LAN-shared instances need). --host / SIDESHOW_HOST binds one address, and the startup line now reports the address actually bound, calling out the wildcard case. Co-Authored-By: Claude Opus 5 --- .changeset/curly-pans-shave.md | 12 ++++++++++++ bin/sideshow.js | 18 +++++++++++++++--- server/index.ts | 17 +++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 .changeset/curly-pans-shave.md diff --git a/.changeset/curly-pans-shave.md b/.changeset/curly-pans-shave.md new file mode 100644 index 0000000..5795d3d --- /dev/null +++ b/.changeset/curly-pans-shave.md @@ -0,0 +1,12 @@ +--- +"sideshow": minor +--- + +serve: add `--host` / `SIDESHOW_HOST` to bind one address + +`serve` listened on every interface with no way to restrict it, and printed +`listening on http://localhost:PORT` regardless — so a server sharing a host or +a network with anything else was reachable from it, and the startup line said +otherwise. The default is unchanged (every interface, which is what containers +and LAN-shared instances need); `--host 127.0.0.1` now keeps it off the network +entirely, and the startup line reports the address actually bound. diff --git a/bin/sideshow.js b/bin/sideshow.js index 55083da..4f8ec7e 100755 --- a/bin/sideshow.js +++ b/bin/sideshow.js @@ -18,7 +18,10 @@ const SELF = fileURLToPath(import.meta.url); const HELP = `sideshow — a live visual surface for terminal coding agents usage: - sideshow serve [--port N] [--open] start the surface (API + viewer) + sideshow serve [--port N] [--host H] [--open] + start the surface (API + viewer) + --host bind to one address (e.g. 127.0.0.1); default is every + interface sideshow publish [options] publish an HTML post (one html surface) --title post title --md add a markdown surface (prose) — repeatable @@ -146,6 +149,8 @@ environment: SIDESHOW_URL server base URL (default http://localhost:8228; set to a deployed instance, e.g. https://sideshow.you.workers.dev) SIDESHOW_TOKEN bearer token for a deployed instance + SIDESHOW_HOST address serve binds to (default: every interface). Set to + 127.0.0.1 to keep the server off the network entirely SIDESHOW_SESSION fixed session id (overrides auto-detection) SIDESHOW_AGENT agent name used when creating sessions `; @@ -887,14 +892,21 @@ function readStdin() { const commands = { async serve() { const { values: flags } = parse({ - options: { port: { type: "string" }, open: { type: "boolean" } }, + options: { + port: { type: "string" }, + host: { type: "string" }, + open: { type: "boolean" }, + }, }); const port = flags.port ?? process.env.PORT ?? "8228"; + const host = flags.host ?? process.env.SIDESHOW_HOST; const child = spawn(process.execPath, [entrypoint("server", "index.ts")], { stdio: "inherit", - env: { ...process.env, PORT: port }, + env: { ...process.env, PORT: port, ...(host ? { SIDESHOW_HOST: host } : {}) }, }); if (flags.open) { + // Always open localhost: a wildcard or loopback bind is reachable there, + // and it is the only address guaranteed to resolve on this machine. const url = `http://localhost:${port}`; const { opener, openerArgs } = process.platform === "darwin" diff --git a/server/index.ts b/server/index.ts index db522b4..30a0749 100644 --- a/server/index.ts +++ b/server/index.ts @@ -83,7 +83,20 @@ const app = createApp({ }); const port = Number(process.env.PORT ?? 8228); +// SIDESHOW_HOST (or `serve --host`) restricts the listener to one address. +// Unset keeps the previous behaviour — node's default, every interface — because +// that is what a container or a LAN-shared instance needs. Set it to 127.0.0.1 +// when the server shares a host with anything you don't want reaching it; that +// is stronger than the token, which is a single shared secret by design. +const hostname = process.env.SIDESHOW_HOST || undefined; -serve({ fetch: app.fetch, port }, (info) => { - console.log(`sideshow listening on http://localhost:${info.port}`); +serve({ fetch: app.fetch, port, hostname }, (info) => { + // Report the address actually bound. Printing "localhost" unconditionally hid + // the fact that the default listens on every interface. + const shown = hostname ?? "localhost"; + const authority = shown.includes(":") ? `[${shown}]` : shown; + console.log( + `sideshow listening on http://${authority}:${info.port}` + + (hostname ? "" : " (all interfaces — set SIDESHOW_HOST to restrict)"), + ); });