Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
af7f3ee
fix(access): the owner's own phone opens the Tailscale address withou…
jetmobsol Oct 6, 2026
1bb515d
no-mistakes(review): limit sole-login grant to this PC's Tailscale name
jetmobsol Oct 6, 2026
2c33b85
no-mistakes(review): make owner no-login grant an explicit serve-only…
jetmobsol Oct 6, 2026
34a60d5
no-mistakes(review): re-read stale tailnet status before owner grant
jetmobsol Oct 6, 2026
c3fc262
no-mistakes(review): read tailnet status on every serve-path owner grant
jetmobsol Oct 6, 2026
dff3029
no-mistakes(review): read tailnet status only before owner grants, si…
jetmobsol Oct 6, 2026
3fbeeb9
no-mistakes(review): gate owner grant on tailnet Host; retry failed r…
jetmobsol Oct 6, 2026
dbf6814
no-mistakes(review): injectable tailnet identity source; unsettled st…
jetmobsol Oct 6, 2026
9042d2e
no-mistakes(review): skip owner-grant status read when a token is con…
jetmobsol Oct 6, 2026
66266b8
no-mistakes(document): Point INSTALL.md at the serve-only pairing option
jetmobsol Oct 6, 2026
5fd423b
no-mistakes(review): move changelog entry to Unreleased; gate grant r…
jetmobsol Oct 6, 2026
5ec94f9
no-mistakes(review): docs: serve-only flag as fallback, not sole code…
jetmobsol Oct 6, 2026
15c2bd4
no-mistakes(document): docs: add serve-only grant to protocol access …
jetmobsol Oct 6, 2026
b98efab
Merge remote-tracking branch 'origin/main' into HEAD
devswha Oct 7, 2026
f3e24aa
docs(changelog): link #525 and credit its author
devswha Oct 7, 2026
badd3cc
docs(access): the sole-login proof covers the nodes this PC's Tailsca…
devswha Oct 7, 2026
de69f4a
Merge remote-tracking branch 'origin/main' into HEAD
devswha Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa
read as an option of its own, so the question fell back to a card of arrow keys. Option
descriptions that wrap over two lines are shown whole, not cut off at the first line.
([#517](https://github.com/devswha/herdr-web-ui/pull/517) by @kilhyeonjun)
- With `HERDR_WEB_TAILSCALE_SERVE_ONLY=1`, which declares `tailscale serve` the only way in, your
own phone lands in the app as soon as it opens the `tailscale serve` address, with no pairing
code, on a tailnet that one Tailscale login owns and that has no tagged node on it. Before,
`tailscale serve` states no login for some of those requests, and every request without one was
taken for a tagged node's and asked to pair. Off by default, such a request still pairs. Another
Tailscale login is still refused, a tagged node still pairs, Funnel stays closed, a LAN client
that claims the same headers still gains nothing, and a configured token is still required of
everything but a paired device.
Comment thread
devswha marked this conversation as resolved.
Resolved
([#525](https://github.com/devswha/herdr-web-ui/pull/525) by @jetmobsol)

## [0.3.52] - 2026-10-06

Expand Down
4 changes: 3 additions & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ code, or the exact command still to run. Who gets in:

- The user's own Tailscale devices get in as the user: `tailscale serve` states the login, and the
server compares it with this PC's. Nothing to configure. Other people's logins are refused, and
tagged devices (no person's login) need pairing.
tagged devices (no person's login) need pairing. If the user's own phone is asked to pair anyway,
`HERDR_WEB_TAILSCALE_SERVE_ONLY=1` lets it in without a code on a tailnet one login owns
([Access and safety](docs/guide.md#access-and-safety)).
- Any other device (someone else's, or a LAN or public address) is paired: **Settings → Devices**
on the PC shows a six-digit code and a QR code; the device enters it once. On a headless PC with
no browser, `bun "$(ls -d ~/.config/herdr/plugins/github/devswha.herdr-web-ui-* | head -1)/scripts/plugin.ts" pair`
Expand Down
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ https://github.com/user-attachments/assets/db788c07-cd68-486d-8ce9-e676a2889c2d
curl -fsSL https://devswha.github.io/herdr-web-ui/install.sh | sh
```

Linux(x64、arm64)または macOS に対応しています。必要な herdr 0.9.0+、Bun 1.4+、Node 18+ がなければ現在のユーザー向けにインストールし、その後アプリを herdr プラグインとしてインストールします。既存の herdr が 0.9.0 より古い場合は、自分で herdr を更新・再起動してからインストーラーを再実行してください。デフォルトの待ち受けアドレスを使用し、Tailscale が起動している場合、HTTPS の設定に成功すると tailnet 内のアクセス用アドレスと QR コードが表示されます。
Linux(x64、arm64)または macOS に対応しています。必要な herdr 0.9.0+、Bun 1.4+、Node 18+ がなければ現在のユーザー向けにインストールし、その後アプリを herdr プラグインとしてインストールします。既存の herdr が 0.9.0 より古い場合は、自分で herdr を更新・再起動してからインストーラーを再実行してください。デフォルトの待ち受けアドレスを使用し、Tailscale が起動している場合、HTTPS の設定に成功すると tailnet 内のアクセス用アドレスと QR コードが表示されます。自分のデバイスはもともとコードなしで入れます。`tailscale serve` がログイン名を伝えるためです。それでも自分のスマートフォンがペアリングを求められるときは、`HERDR_WEB_TAILSCALE_SERVE_ONLY=1` を設定すると、ログインが1つだけの tailnet ならコードなしで入れるようになります。ただし、公開プロキシやトンネルなど別の経路でこのポートに届く場合は設定しないでください([アクセスと安全性](docs/guide.md#access-and-safety))。

<p align="center">
<img src="docs/screenshots/install.png" width="720" alt="インストーラーの出力:Bun、Node、herdr プラグインのインストール後、tailscale serve でアプリへのアクセスを有効にし、スマートフォン用の QR コードを表示します。">
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ https://github.com/user-attachments/assets/db788c07-cd68-486d-8ce9-e676a2889c2d
curl -fsSL https://devswha.github.io/herdr-web-ui/install.sh | sh
```

Linux(x64, arm64)와 macOS를 지원합니다. herdr 0.9.0 이상, Bun 1.4 이상, Node 18 이상 중 없는 것을 현재 사용자 계정에 설치한 뒤, 앱을 herdr 플러그인으로 설치합니다. 이미 설치된 herdr가 0.9.0보다 오래됐다면 herdr를 직접 업데이트하고 다시 시작한 다음 설치 스크립트를 다시 실행하세요. 기본 수신 주소를 쓰고 Tailscale이 켜져 있으면, HTTPS 설정이 끝났을 때 tailnet 주소와 QR 코드가 나옵니다.
Linux(x64, arm64)와 macOS를 지원합니다. herdr 0.9.0 이상, Bun 1.4 이상, Node 18 이상 중 없는 것을 현재 사용자 계정에 설치한 뒤, 앱을 herdr 플러그인으로 설치합니다. 이미 설치된 herdr가 0.9.0보다 오래됐다면 herdr를 직접 업데이트하고 다시 시작한 다음 설치 스크립트를 다시 실행하세요. 기본 수신 주소를 쓰고 Tailscale이 켜져 있으면, HTTPS 설정이 끝났을 때 tailnet 주소와 QR 코드가 나옵니다. 자신의 기기는 원래 코드 없이 들어갑니다. `tailscale serve`가 로그인을 알려 주기 때문입니다. 그래도 자신의 폰이 페어링을 요구받는다면 `HERDR_WEB_TAILSCALE_SERVE_ONLY=1`을 설정하면 로그인이 하나뿐인 tailnet에서는 코드 없이 들어갈 수 있습니다. 단, 공용 프록시나 터널 등 다른 경로로 이 포트에 접근할 수 있다면 설정하지 마세요 ([접근과 안전](docs/guide.md#access-and-safety)).

<p align="center">
<img src="docs/screenshots/install.png" width="720" alt="설치 스크립트 출력: Bun, Node, herdr 플러그인이 설치되고 tailscale serve가 앱을 공개한 뒤 폰용 QR 코드가 나옵니다.">
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ A browser and phone client for [herdr](https://github.com/herdrdev/herdr). Read
curl -fsSL https://devswha.github.io/herdr-web-ui/install.sh | sh
```

Linux (x64, arm64) or macOS. Installs missing herdr 0.9.0+, Bun 1.4+ and Node 18+ prerequisites for your user, then installs the app as a herdr plugin. If an existing herdr installation is older than 0.9.0, update and restart herdr yourself before rerunning the installer. With the default listen address and Tailscale running, successful HTTPS setup provides a tailnet address and QR code.
Linux (x64, arm64) or macOS. Installs missing herdr 0.9.0+, Bun 1.4+ and Node 18+ prerequisites for your user, then installs the app as a herdr plugin. If an existing herdr installation is older than 0.9.0, update and restart herdr yourself before rerunning the installer. With the default listen address and Tailscale running, successful HTTPS setup provides a tailnet address and QR code. Your own devices get in without a code as it is: `tailscale serve` states your login. If your own phone is asked to pair anyway, `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` lets it in without a code on a tailnet one login owns, but only when nothing else, such as a public proxy or tunnel, reaches this port ([Access and safety](docs/guide.md#access-and-safety)).

Windows x64, in PowerShell:

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ https://github.com/user-attachments/assets/db788c07-cd68-486d-8ce9-e676a2889c2d
curl -fsSL https://devswha.github.io/herdr-web-ui/install.sh | sh
```

支持 Linux(x64、arm64)或 macOS。安装程序会为当前用户补齐 herdr 0.9.0+、Bun 1.4+ 和 Node 18+ 依赖,然后将应用安装为 herdr 插件。如果已安装的 herdr 低于 0.9.0,请先自行更新并重启 herdr,再重新运行安装程序。使用默认监听地址且 Tailscale 正在运行时,HTTPS 配置成功后会提供 tailnet 内的访问地址和二维码。
支持 Linux(x64、arm64)或 macOS。安装程序会为当前用户补齐 herdr 0.9.0+、Bun 1.4+ 和 Node 18+ 依赖,然后将应用安装为 herdr 插件。如果已安装的 herdr 低于 0.9.0,请先自行更新并重启 herdr,再重新运行安装程序。使用默认监听地址且 Tailscale 正在运行时,HTTPS 配置成功后会提供 tailnet 内的访问地址和二维码。你自己的设备本来就能免验证码进入:`tailscale serve` 会告知你的登录名。如果你的手机仍被要求配对,设置 `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` 后,在只有一个登录名的 tailnet 上也能免验证码进入;但仅限于没有公共代理或隧道等其他途径能访问此端口的情况([访问与安全](docs/guide.md#access-and-safety))。

<p align="center">
<img src="docs/screenshots/install.png" width="720" alt="安装程序输出:安装 Bun、Node 和 herdr 插件,然后通过 tailscale serve 提供应用访问地址,并显示供手机扫描的二维码。">
Expand Down
9 changes: 5 additions & 4 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ tailscale serve --bg --https=443 http://127.0.0.1:7317

The one-line installer runs this for you when Tailscale runs on the PC and does not serve the app yet, on the first free port of 443, 8443, 7317 and 17317, and prints the command that undoes it. On Linux, `tailscale serve` needs root or `sudo tailscale set --operator=$USER` once; the installer says so when Tailscale refuses.

Only devices in your tailnet can open that address, and only yours get in without a code: see [Access and safety](#access-and-safety).
Only devices in your tailnet can open that address. Your own devices get in without a code as it is: `tailscale serve` states your login. If your own phone is asked to pair anyway, `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` lets it in without a code on a tailnet one login owns, and only when nothing else, such as a public reverse proxy or tunnel, reaches this port (see [Access and safety](#access-and-safety)). Devices that belong to someone else pair with a six-digit code.

**Settings → Phone** in the app does this step for you as far as it can: it shows the address Tailscale already serves for this PC as a QR code, or the exact command still to run, and the address it will give.

Expand Down Expand Up @@ -279,20 +279,20 @@ More in [remote PCs](remote-pcs.md).

Anyone who can reach the server can type into your terminals, so what matters is who gets in. It listens on `127.0.0.1` by default, which means only this computer. From anywhere else, a request gets in in one of three ways:

- **It is you, says Tailscale.** `tailscale serve` states the requesting device's Tailscale login in a header it strips from anything incoming. A login that matches this PC's own gets in; another login is refused, and a tagged device (one with no person's login) needs pairing. Nothing to set up, unless this PC's own Tailscale node is tagged: it then has no login of its own, so every device pairs, yours included, or you name your login in `HERDR_WEB_TAILSCALE_OWNER`.
- **It is you, says Tailscale.** `tailscale serve` states the requesting device's Tailscale login in a header it strips from anything incoming. A login that matches this PC's own gets in; another login is refused, and a tagged device (one with no person's login) needs pairing. If you set `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` to say `tailscale serve` is the only way in, and `tailscale status` on this PC lists one login on every node it sees and no tagged node, a serve request that states no login is the owner as well: no one else is among those nodes to be mistaken for (a node an ACL hides from this PC is not seen, and cannot reach serve either), so your own phone opens the address and lands in the app with no code. Nothing to set up, unless this PC's own Tailscale node is tagged: it then has no login of its own, so every device pairs, yours included, or you name your login in `HERDR_WEB_TAILSCALE_OWNER`.
- **It is a paired device.** **Settings → Devices**, on the PC (or on a device already paired), shows a six-digit code that lives ten minutes and a QR code that carries it. On a headless PC, the `pair` command prints the same in its terminal (see [In a terminal](#in-a-terminal)); Devices also shows the pairing link as text, to send to the other device. The other device enters it once and keeps its own credential in an HttpOnly cookie; the list shows it, and **Revoke** immediately closes its terminal connections and roster stream and refuses subsequent requests.
- **It holds the token.** `HERDR_WEB_TOKEN`, for scripts and proxies, as a cookie after sign-in or as `Authorization: Bearer <token>`. When a token is set, everything else needs it, this computer and your own Tailscale login included: enter it once on each device, and that browser stays signed in for a year. A paired device still gets in without it.

| How you reach it | What gets you in |
| --- | --- |
| This computer only (default) | Nothing needed. With a token set, the token |
| SSH tunnel (`ssh -L 7317:127.0.0.1:7317 host`) | Nothing needed. With a token set, the token |
| `tailscale serve`, your own devices | Nothing needed: your login. With a token set, the token, once per device |
| `tailscale serve`, your own devices | Nothing needed: your login. With `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` on a one-login tailnet, the owner's phone needs nothing either. With a token set, the token, once per device |
| `tailscale serve` on a tailnet you share with others | Your devices: your login (with a token set, the token once). Theirs: refused unless you pair them |
| Your LAN (`HOST=0.0.0.0` or a LAN address) | Pair each device, or set a token |
| A public domain or reverse proxy | Set a token, with HTTPS, and set the proxy up as in [Behind a reverse proxy](#behind-a-reverse-proxy). Never `tailscale funnel` it |

Until the first device is paired, and with no token set, a LAN or proxied address is open to anyone who reaches it, as it always was: the server warns on startup. The exception is a proxy on this PC while its Tailscale login is known, as with `tailscale serve`: a request that carries no login there needs pairing from the start. Pairing the first device closes it for good; revoking every device does not reopen it. Without a token, this computer itself stays in whatever happens, so you can never lock yourself out: revoke everything and pair again from `http://localhost:7317`. With a token set, this computer signs in with the token.
Until the first device is paired, and with no token set, a LAN or proxied address is open to anyone who reaches it, as it always was: the server warns on startup. The exception is a proxy on this PC while its Tailscale login is known, as with `tailscale serve`: a request that carries no login there needs pairing from the start, unless `HERDR_WEB_TAILSCALE_SERVE_ONLY=1` is set and that tailnet has one login and no tagged node, where such a request is the owner's own device. With that setting off, which is the default, every such request pairs. Pairing the first device closes it for good; revoking every device does not reopen it. Without a token, this computer itself stays in whatever happens, so you can never lock yourself out: revoke everything and pair again from `http://localhost:7317`. With a token set, this computer signs in with the token.

The pairing code is a one-time secret: five wrong tries spend it.

Expand Down Expand Up @@ -375,6 +375,7 @@ Observe connections cannot take a pane, and a displaced bridge never takes it ba
| `HERDR_SOCKET` | `~/.config/herdr/herdr.sock` | herdr socket for API calls and terminal attach. For a named session, use `~/.config/herdr/sessions/<name>/herdr.sock`. |
| `HERDR_WEB_TOKEN` | unset | Shared token for scripts and proxies. Once set, every client that is not a paired device needs it, this computer and your own Tailscale login included |
| `HERDR_WEB_TAILSCALE_OWNER` | this PC's Tailscale login | The Tailscale login that gets in through `tailscale serve` without pairing. Set it on a PC whose Tailscale node is tagged, which has no login of its own |
| `HERDR_WEB_TAILSCALE_SERVE_ONLY` | unset (off) | `1` declares `tailscale serve` the only way anything reaches this port. Then the owner's own device gets in through serve without a code, on a tailnet one login owns with no tagged node. Enable it only when no public reverse proxy, tunnel or other forwarding server exposes this port: a visitor through one would otherwise get the owner's access |
| `HERDR_WEB_STATE_DIR` | `~/.config/herdr-web-ui` | Push keys, device subscriptions, PC registrations and update builds |
| `HERDR_WEB_OPENAI_API_KEY` | unset | OpenAI API key for [voice input](#voice-input). Set here, it cannot be changed from the app |
| `HERDR_WEB_OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API root for voice input |
Expand Down
2 changes: 1 addition & 1 deletion server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Bun backend: HTTP API + WebSocket fan-out over herdr's unix-socket API, plus the
- Output control adds `{"t":"p","paused":true|false}`. Parent pause and stdout backpressure are independent; both must clear to resume. Kill resumes the reader to observe EOF and has a bounded forced cleanup. PtySession awaits stdout EOF before reporting process exit.
- ACK-capable attaches carry `flow_control:"ack"`; `pty-data.flow` identifies the subscription and cumulative UTF-8 byte offset. `pty-ack` releases only already-sent credit in that subscription. Pause at 256 KiB, resume at <=64 KiB, close consumers blocked for 2s with code 4008; 1 MiB hard application/transport budgets. `-1` from Bun send is already queued, never retransmit it. Legacy clients have the transport cap only. See docs/terminal-flow-control.md and server/output.contract.test.ts.
- `HerdrError.code` → HTTP status (http.ts `errorResponse`): `connect_failed`/`timeout` → 502; other HerdrError codes → 404; anything else → 500 `internal_error`.
- Auth: auth.ts `isAuthenticated` accepts the `herdr_web_token` cookie or an Authorization Bearer (length-safe compare); access.ts `decideAccess` (token → device cookie → Tailscale owner → loopback local → pairing) runs for /ws and every /api/* route except /api/health, /api/auth and /api/devices/pair; static files stay open.
- Auth: auth.ts `isAuthenticated` accepts the `herdr_web_token` cookie or an Authorization Bearer (length-safe compare); access.ts `decideAccess` (token → device cookie → Tailscale owner, by login header, or with no login header on a tailnet one login provably owns when HERDR_WEB_TAILSCALE_SERVE_ONLY declares serve the only ingress → loopback local → pairing) runs for /ws and every /api/* route except /api/health, /api/auth and /api/devices/pair; static files stay open.
- An unauthenticated /ws upgrade is refused with a plain-text 401 `unauthorized` (the WS client never parses a body); every other refusal is the JSON envelope.
- `terminalInfoFor` resolves pane_id → `{ terminalId, rect }` from one fresh `session.snapshot` (rect = the pane's `layouts[].panes[].rect`, null when absent); missing pane → `pane_not_found`, pane without terminal → `no_terminal`.
- The layout `rect` is NOT the terminal's current size (measured on herdr 0.9.3): it stayed 120x40 while a `terminal attach` sized the pane 100x30, then 140x40, and the pane kept 140x40 after that attach left. Only the attached client's size is real; nothing in the API tells another bridge's size. An attachment started again (held retry, live handoff) therefore reuses its own cached `cols`/`rows` (#363 review).
Expand Down
Loading
Loading