From bcb88ebefb103106063d78bd5af1b8f0167bd35e Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 1 Jun 2026 11:55:17 -0700 Subject: [PATCH] [eric] auth: dev-only /api/dev/token so the split-port frontend can authenticate (404s when packaged) --- backend/auth.py | 3 +++ backend/main.py | 11 +++++++++++ backend/tests/test_auth_router.py | 23 +++++++++++++++++++++++ frontend/src/shared/config.ts | 12 ++++++++++++ 4 files changed, 49 insertions(+) diff --git a/backend/auth.py b/backend/auth.py index 154be5d4..84953cb4 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -186,6 +186,9 @@ _AUTH_EXEMPT_EXACT = { # public api.openswarm.com/api/oauth/google/refresh doesn't already # do for any internet caller, so no new attack surface. "/api/tools/google-oauth-token", + # Dev-only token handoff for the split-port frontend (no Electron preload + # to read the token from). The route itself 404s in packaged builds. + "/api/dev/token", } _AUTH_EXEMPT_PREFIX = ( diff --git a/backend/main.py b/backend/main.py index ad21f1ee..a4f3e124 100644 --- a/backend/main.py +++ b/backend/main.py @@ -386,6 +386,17 @@ async def websocket_dashboard(websocket: WebSocket): ws_manager.disconnect_global(websocket) +@app.get("/api/dev/token") +async def dev_token(): + """Hand the per-install token to the dev frontend, which has no Electron + preload to read it from. Disabled in packaged builds (the preload exists + there); localhost binding is the only thing gating it in dev.""" + if os.environ.get("OPENSWARM_PACKAGED") == "1": + return JSONResponse({"error": "not available"}, status_code=404) + from backend.auth import get_auth_token + return JSONResponse({"token": get_auth_token()}) + + @app.post("/api/browser/command") async def browser_command(request: Request): """HTTP endpoint called by the browser MCP server subprocess. diff --git a/backend/tests/test_auth_router.py b/backend/tests/test_auth_router.py index 9eeb7a72..2fe86d15 100644 --- a/backend/tests/test_auth_router.py +++ b/backend/tests/test_auth_router.py @@ -268,3 +268,26 @@ def test_signout_succeeds_even_when_cloud_unreachable(client, reset_settings): s2 = load_settings() assert s2.user_id is None assert s2.openswarm_bearer_token is None + + +# --------------------------------------------------------------------------- +# The dev-token handoff must be dev-only so it can't widen prod surface (#49). +# --------------------------------------------------------------------------- + +def test_dev_token_is_dev_only(): + """/api/dev/token hands the install token to the split-port dev frontend + without auth, but 404s in packaged builds where the preload supplies it.""" + import os + import backend.auth as auth_mod + noauth = TestClient(app) # deliberately no bearer header + + os.environ.pop("OPENSWARM_PACKAGED", None) + r = noauth.get("/api/dev/token") + assert r.status_code == 200 + assert r.json()["token"] == auth_mod._TOKEN + + os.environ["OPENSWARM_PACKAGED"] = "1" + try: + assert noauth.get("/api/dev/token").status_code == 404 + finally: + os.environ.pop("OPENSWARM_PACKAGED", None) diff --git a/frontend/src/shared/config.ts b/frontend/src/shared/config.ts index e36edd45..00b47eaf 100644 --- a/frontend/src/shared/config.ts +++ b/frontend/src/shared/config.ts @@ -23,6 +23,18 @@ export async function refreshAuthToken(): Promise { } catch { _authTokenCache = ''; } + return _authTokenCache; + } + // Dev (split-port, no Electron preload): the backend hands us the token over + // localhost. The route 404s in packaged builds, so this only fires under run.sh. + try { + const r = await fetch(`http://${host}:${port}/api/dev/token`); + if (r.ok) { + const data = await r.json(); + _authTokenCache = typeof data?.token === 'string' ? data.token : ''; + } + } catch { + _authTokenCache = ''; } return _authTokenCache; }