feat: add GET /api/settings/sync federation endpoint

This commit is contained in:
Brian Krabach
2026-04-08 20:50:46 -07:00
parent 29cfb3c141
commit 349f49824b
2 changed files with 96 additions and 9 deletions
+30 -9
View File
@@ -64,7 +64,12 @@ from muxplex.state import (
save_state,
state_lock,
)
from muxplex.settings import load_federation_key, load_settings, patch_settings
from muxplex.settings import (
get_syncable_settings,
load_federation_key,
load_settings,
patch_settings,
)
from muxplex.ttyd import kill_orphan_ttyd, kill_ttyd, spawn_ttyd, TTYD_PORT
# ---------------------------------------------------------------------------
@@ -140,9 +145,8 @@ async def _run_poll_cycle() -> None:
if active_remote_id is not None:
settings = load_settings()
remote_instances = settings.get("remote_instances", [])
if (
isinstance(active_remote_id, int)
and 0 <= active_remote_id < len(remote_instances)
if isinstance(active_remote_id, int) and 0 <= active_remote_id < len(
remote_instances
):
remote = remote_instances[active_remote_id]
remote_url: str = remote.get("url", "").rstrip("/")
@@ -157,13 +161,13 @@ async def _run_poll_cycle() -> None:
and view_mode == "fullscreen"
and (now - last_interaction_at) < 60
):
bell_clear_url = (
f"{remote_url}/api/sessions/{viewing_session}/bell/clear"
)
bell_clear_url = f"{remote_url}/api/sessions/{viewing_session}/bell/clear"
try:
await _federation_client.post(
bell_clear_url,
headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {},
headers={"Authorization": f"Bearer {remote_key}"}
if remote_key
else {},
)
except Exception as exc:
_log.warning(
@@ -720,6 +724,21 @@ async def update_settings(request: Request) -> dict:
return result
@app.get("/api/settings/sync")
async def get_settings_sync() -> dict:
"""Return syncable settings + timestamp for federation sync.
Authenticated via federation Bearer token (same auth middleware as all other
non-exempt endpoints). Returns only the keys in SYNCABLE_KEYS plus the
settings_updated_at timestamp; infrastructure keys (host, port, federation_key,
etc.) are never included.
"""
syncable = get_syncable_settings()
ts = syncable.get("settings_updated_at", 0.0)
settings = {k: v for k, v in syncable.items() if k != "settings_updated_at"}
return {"settings": settings, "settings_updated_at": ts}
@app.get("/api/instance-info")
async def instance_info() -> dict:
"""Return this instance's display name and version.
@@ -916,7 +935,9 @@ async def federation_terminal_ws_proxy(websocket: WebSocket, remote_id: int) ->
async with websockets.connect(
ws_url,
subprotocols=[Subprotocol("tty")],
additional_headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {},
additional_headers={"Authorization": f"Bearer {remote_key}"}
if remote_key
else {},
ssl=ssl_context,
) as remote_ws:
+66
View File
@@ -2802,6 +2802,72 @@ async def test_poll_cycle_fires_federation_bell_clear_for_remote_session(
)
# ---------------------------------------------------------------------------
# GET /api/settings/sync (task-7)
# ---------------------------------------------------------------------------
def test_settings_sync_returns_200(client, tmp_path, monkeypatch):
"""GET /api/settings/sync returns HTTP 200."""
import muxplex.settings as settings_mod
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", tmp_path / "settings.json")
response = client.get("/api/settings/sync")
assert response.status_code == 200
def test_settings_sync_response_has_settings_and_timestamp(
client, tmp_path, monkeypatch
):
"""GET /api/settings/sync returns {settings: dict, settings_updated_at: float}."""
import muxplex.settings as settings_mod
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", tmp_path / "settings.json")
response = client.get("/api/settings/sync")
assert response.status_code == 200
data = response.json()
assert "settings" in data, (
f"Response must have 'settings' key, got: {list(data.keys())}"
)
assert "settings_updated_at" in data, (
f"Response must have 'settings_updated_at' key, got: {list(data.keys())}"
)
assert isinstance(data["settings"], dict), (
f"'settings' must be a dict, got: {type(data['settings'])}"
)
assert isinstance(data["settings_updated_at"], float), (
f"'settings_updated_at' must be a float, got: {type(data['settings_updated_at'])}"
)
def test_settings_sync_excludes_infrastructure_keys(client, tmp_path, monkeypatch):
"""GET /api/settings/sync settings dict must not contain infrastructure keys."""
import muxplex.settings as settings_mod
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", tmp_path / "settings.json")
response = client.get("/api/settings/sync")
assert response.status_code == 200
settings = response.json()["settings"]
infra_keys = (
"host",
"port",
"auth",
"session_ttl",
"tls_cert",
"tls_key",
"device_name",
"federation_key",
"remote_instances",
"multi_device_enabled",
"new_session_template",
"delete_session_template",
)
for key in infra_keys:
assert key not in settings, (
f"Infrastructure key '{key}' must not appear in /api/settings/sync response"
)
def test_federation_auth_headers_guard_empty_key():
"""Every federation Authorization header construction must guard against empty key.