--lan key persists across restarts; stale tabs get a readable 403

The key rotated on every server restart, stranding every phone that
held the previous cookie — during active development that guaranteed a
wall of refusals from stale polling tabs after each restart. The key
now lives in data/.lan_key (gitignored, 0600 — the Playwright-state
treatment for credential-adjacent files) and is reused across
restarts; delete the file to rotate. A keyless browser navigation now
gets a one-line HTML page saying what to do instead of raw JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 16:39:24 -04:00
co-authored by Claude Fable 5
parent 344a930a8a
commit f82489716a
7 changed files with 64 additions and 27 deletions
+15
View File
@@ -1021,6 +1021,12 @@ def test_run_web_review_lan_branch_binds_and_warns(tmp_path, monkeypatch, capsys
wr.run_web_review(cfg, port=9999, lan=True)
out = capsys.readouterr().out
assert captured["host"] == "0.0.0.0"
# the key persists (0600) so restarts don't strand the phone's cookie
assert (cfg.lan_key_path.stat().st_mode & 0o777) == 0o600
key = cfg.lan_key_path.read_text().strip()
assert f"?k={key}" in out
wr.run_web_review(cfg, port=9999, lan=True)
assert f"?k={key}" in capsys.readouterr().out # same key after restart
# the default-route address leads; other interfaces are fallbacks
assert "on your phone, open: http://192.168.1.5:9999/?k=" in out
fallback = next(line for line in out.splitlines() if "try:" in line)
@@ -1030,3 +1036,12 @@ def test_run_web_review_lan_branch_binds_and_warns(tmp_path, monkeypatch, capsys
assert "Use only on a network you trust" in out
wr.run_web_review(cfg, port=9999, lan=False)
assert captured["host"] == "127.0.0.1"
def test_lan_403_is_html_for_navigations(tmp_path):
cfg = make_cfg(tmp_path)
app = create_app(cfg, client=unauthorized_client(tmp_path), lan_token="sekret")
phone = TestClient(app, base_url="http://192.168.1.99:8377")
res = phone.get("/titles", headers={"accept": "text/html,application/xhtml+xml"})
assert res.status_code == 403
assert "access key needed" in res.text # a person sees prose, not JSON