Commit 21848f5
authored
🤖 feat: configurable API server bind host experiment (#1288)
Adds an opt-in **Settings → Experiments** feature to expose mux's API
server on LAN/VPN.
- Persists bind host/port in `~/.mux/config.json` (`apiServerBindHost`,
`apiServerPort`).
- Computes and surfaces `networkBaseUrls` for copy/paste (LAN/VPN IPs) +
auth token.
- Adds RPC to view/update settings and restart the HTTP/WS server.
Validation:
- `make static-check`
- `bun test src/node/services/serverService.test.ts
src/node/services/serverLockfile.test.ts src/node/config.test.ts`
---
<details>
<summary>📋 Implementation Plan</summary>
# Configurable bind URL for the local API server (Experiment)
## Answer (current behavior)
- **Electron desktop always binds the HTTP/WS API server to
`127.0.0.1`** (`src/node/services/serverService.ts`).
- You can currently override:
- `MUX_SERVER_PORT` (port; default is random)
- `MUX_SERVER_AUTH_TOKEN` (auth token)
- `MUX_NO_API_SERVER=1` (disable the server)
- The standalone CLI server supports `--host`, but **the desktop app
does not expose a bind-host setting today**.
**Consequence:** if mux only listens on loopback, your phone (LAN) and
VPN clients (e.g., Tailscale) can’t connect.
---
## Goal
Add an **opt-in experiment** that lets users configure the **bind
host/interface** (and ideally a stable port) for the desktop app’s
HTTP/WS API server, with UI text that clearly explains:
- enabling it makes mux listen beyond localhost,
- other devices on your LAN/VPN can connect,
- this has security implications.
---
## Recommended approach (config-backed experiment + connection info)
**Net estimate:** ~350–500 LoC product code.
### What the experiment does (user-facing)
When enabled, mux can bind its local API server to a non-loopback
interface (e.g. `0.0.0.0` or a specific IP), making it reachable from:
- **devices on your local network** (phone on the same Wi‑Fi), and
- **VPN interfaces** (e.g. Tailscale).
It should also surface **copy/paste connection info**:
- connect URL(s) for detected interfaces (LAN IP, Tailscale IP, etc.)
- the auth token required to access `/api` + `/orpc`
### Security/UX requirements
- The experiment description must explicitly warn that:
- **devices on your local network can connect** to your mux instance
(with the token), and
- requests are over **unencrypted HTTP** (token can be sniffed on
untrusted networks).
- Recommend: **only enable on trusted networks** and prefer
**Tailscale** for encrypted transport.
---
## Implementation plan
### 1) Add a new experiment entry (UI discoverability)
**Files:**
- `src/common/constants/experiments.ts`
**Steps:**
- Add a new ID, e.g. `EXPERIMENT_IDS.CONFIGURABLE_BIND_URL =
"configurable-bind-url"`.
- Add a definition with clear copy, e.g.:
- **name:** `"Expose API server on LAN/VPN"`
- **description (draft):**
- `"Allow mux to listen on a non-localhost address so other devices on
your LAN/VPN can connect. Anyone on your network with the auth token can
access your mux API. HTTP only; use only on trusted networks (Tailscale
recommended)."`
- `enabledByDefault: false`
- `userOverridable: true`
- `showInSettings: true`
### 2) Persist API server bind settings in `~/.mux/config.json`
**Why:** main process needs this at app launch; localStorage isn’t
available.
**Files:**
- `src/common/types/project.ts` (extend `ProjectsConfig`)
- `src/node/config.ts` (load/save)
**Add config fields (proposal):**
- `apiServerBindHost?: string` (default: unset → `127.0.0.1`)
- `apiServerPort?: number` (default: unset → current behavior; env var
still overrides)
Notes:
- Keep behavior backward-compatible by treating missing fields as
defaults.
- Keep `MUX_SERVER_PORT` as highest-precedence override (useful for CI /
power users).
### 3) Update server startup to respect the configured bind host
**Files:**
- `src/node/services/serverService.ts`
- `src/desktop/main.ts`
**Steps:**
- Extend `StartServerOptions` to accept `host?: string`.
- Pass `host` through to `createOrpcServer({ host, port, ... })`.
- In `src/desktop/main.ts`, decide host/port in this precedence order:
1. `MUX_SERVER_PORT` env var (existing)
2. config `apiServerPort`
3. default `0` (random)
and for host:
1. config `apiServerBindHost`
2. default `127.0.0.1`
### 4) Make connection URLs discoverable (LAN/Tailscale)
Today `createOrpcServer` rewrites wildcard bind hosts (`0.0.0.0` / `::`)
to `127.0.0.1` for `baseUrl`, which is fine for the desktop app + CLI
discovery.
To support phones/remote clients, add *additional* URLs rather than
changing `baseUrl`:
**Files:**
- `src/node/services/serverLockfile.ts`
- `src/node/services/serverService.ts`
- (optional) `src/node/orpc/server.ts` (only if we want server.ts to
return more metadata)
**Steps:**
- Extend `ServerLockDataSchema` to include optional fields, e.g.:
- `bindHost?: string`
- `port?: number`
- `networkBaseUrls?: string[]` (derived from `os.networkInterfaces()`,
filtered to non-internal addresses)
- Update `ServerLockfile.acquire(...)` to accept and persist these
optional fields.
- In `ServerService.startServer`, after the server is listening (we know
`actualPort`), compute:
- `networkBaseUrls` for each relevant interface IP: `http://<ip>:<port>`
- (optionally) include `ws://<ip>:<port>/orpc/ws` as well if needed
later
### 5) Add backend RPC for viewing/updating bind settings (+ restarting
server)
We want the experiment UI to be able to:
- read current bind settings
- update them
- restart the HTTP/WS server so changes apply immediately
**Files:**
- `src/common/orpc/schemas/api.ts` (add schemas under `server`)
- `src/node/orpc/router.ts` (implement handlers)
**Proposed API surface:**
- `server.getApiServerStatus` → returns current:
- `running: boolean`
- `baseUrl` (loopback)
- `networkBaseUrls` (if any)
- `token` (so user can copy it for phone usage)
- `bindHost`, `port`
- `server.setApiServerSettings` → persists config and restarts the HTTP
server.
**Restart semantics:**
- Restart only the HTTP/WS server (`ServerService.stopServer()` +
`startServer()`); this should not break the Electron renderer since it
uses MessagePort.
- Defensive behavior:
- Validate host string (non-empty) and port range before writing.
- If restart fails, attempt to revert to the previous working settings.
### 6) Update Settings → Experiments UI to include controls + warnings
**Files:**
- `src/browser/components/Settings/sections/ExperimentsSection.tsx`
**Approach:**
- Keep the experiment toggle in the experiments list, but for this
specific ID render an “expanded” row beneath it that:
- chooses bind host:
- `127.0.0.1` (localhost only)
- `0.0.0.0` (all interfaces: LAN + VPN)
- optional: “Custom…” text input
- chooses port:
- default “Random (changes each start)” vs a numeric input
- shows computed `networkBaseUrls` + a “Copy” button
- shows the token + “Copy token” button
- shows a bold warning blurb (see copy below)
**Suggested warning copy (UI):**
> Exposes mux’s API server to your LAN/VPN. Devices on your local
network can connect if they have the auth token. Traffic is unencrypted
HTTP; enable only on trusted networks (Tailscale recommended).
---
## Validation
- Unit tests for:
- `ServerLockDataSchema` backward compatibility (old lock files still
parse)
- network URL computation (filters out internal interfaces; stable
ordering)
- Manual checks:
- With bind host `0.0.0.0` and fixed port, confirm phone can load
`http://<LAN-IP>:<port>/api/docs`.
- Confirm `/api/*` requests require the auth token.
---
<details>
<summary>Alternatives considered</summary>
### A) Environment variable only (`MUX_SERVER_HOST`)
**Net estimate:** ~20–40 LoC.
- Add `MUX_SERVER_HOST` env var read in `src/desktop/main.ts` and pass
into `ServerService.startServer`.
- No experiments UI.
Pros: trivial.
Cons: doesn’t satisfy “Experiments tab” UX + doesn’t help
discoverability.
### B) Make the existing experiments system config-backed
**Net estimate:** ~250–400 LoC *in addition* to the feature.
- Today experiment overrides are stored in localStorage; main process
can’t read them.
- We could generalize experiments to optionally persist overrides in
`~/.mux/config.json` and expose them to both main + renderer.
Pros: more consistent long-term.
Cons: larger cross-cutting refactor than necessary for a single setting.
</details>
</details>
---
_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh`_
---------
Signed-off-by: Thomas Kosiewski <[email protected]>1 parent c7f7323 commit 21848f5
File tree
18 files changed
+1211
-33
lines changed- src
- browser/components
- Settings/sections
- common
- constants
- orpc
- schemas
- types
- desktop
- node
- orpc
- services
18 files changed
+1211
-33
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | | - | |
| 46 | + | |
| 47 | + | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| |||
73 | 75 | | |
74 | 76 | | |
75 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
76 | 83 | | |
77 | 84 | | |
78 | 85 | | |
| |||
0 commit comments