fix(webui): keep credentials out of service worker caches

This commit is contained in:
Xubin Ren
2026-08-12 21:09:29 +09:00
parent e455a2b7fa
commit 5fc8303f9e
6 changed files with 83 additions and 34 deletions
@@ -717,7 +717,7 @@ async def test_token_issue_route_requires_secret_when_static_token_configured(bu
bus,
port=port,
token="static-token",
tokenIssuePath="/auth/token",
tokenIssuePath="/custom-token",
websocketRequiresToken=True,
)
@@ -725,15 +725,16 @@ async def test_token_issue_route_requires_secret_when_static_token_configured(bu
await asyncio.sleep(0.3)
try:
denied = await _http_get(f"http://127.0.0.1:{port}/auth/token")
denied = await _http_get(f"http://127.0.0.1:{port}/custom-token")
assert denied.status_code == 401
allowed = await _http_get(
f"http://127.0.0.1:{port}/auth/token",
f"http://127.0.0.1:{port}/custom-token",
headers={"Authorization": "Bearer static-token"},
)
assert allowed.status_code == 200
assert allowed.json()["token"].startswith("nbwt_")
assert allowed.headers["Cache-Control"] == "no-store"
finally:
await channel.stop()
await server_task
@@ -3803,6 +3804,7 @@ async def test_token_issue_rejects_when_at_capacity(bus: MagicMock) -> None:
headers={"Authorization": "Bearer s"},
)
assert resp.status_code == 429
assert resp.headers["Cache-Control"] == "no-store"
data = resp.json()
assert "error" in data
finally:
@@ -227,6 +227,7 @@ async def test_bootstrap_returns_token_for_localhost(
try:
resp = await _http_get("http://127.0.0.1:29901/webui/bootstrap")
assert resp.status_code == 200
assert resp.headers["Cache-Control"] == "no-store"
body = resp.json()
assert body["token"].startswith("nbwt_")
assert channel.gateway.tokens.issued_token_audiences[body["token"]] == "webui"
+3
View File
@@ -100,6 +100,7 @@ def http_json_response(
*,
status: int = 200,
accept_encoding: str | None = None,
extra_headers: list[tuple[str, str]] | None = None,
) -> Response:
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
headers = [
@@ -112,6 +113,8 @@ def http_json_response(
if len(body) >= _JSON_GZIP_MIN_BYTES and accepts_gzip(accept_encoding):
body = gzip.compress(body, compresslevel=_JSON_GZIP_LEVEL, mtime=0)
headers.append(("Content-Encoding", "gzip"))
if extra_headers:
headers.extend(extra_headers)
headers.append(("Content-Length", str(len(body))))
reason = http.HTTPStatus(status).phrase
return Response(status, reason, Headers(headers), body)
+13 -4
View File
@@ -121,6 +121,7 @@ from nanobot.webui.workspaces import WebUIWorkspaceController
_SLOW_WEBUI_HTTP_LOG_MS = 1_000
_WEBUI_MUTATION_PAYLOAD_ATTR = "_nanobot_webui_mutation_payload"
_WEBUI_MUTATION_REQUEST_ATTR = "_nanobot_webui_mutation_request"
_NO_STORE_HEADERS = [("Cache-Control", "no-store")]
_WEBUI_MUTATION_PATHS = {
"automation.enable": "/api/webui/automations/enable",
@@ -547,9 +548,16 @@ class GatewayHTTPHandler:
"too many outstanding issued tokens ({}), rejecting issuance",
len(self.tokens.issued_tokens),
)
return _http_json_response({"error": "too many outstanding tokens"}, status=429)
return _http_json_response(
{"error": "too many outstanding tokens"},
status=429,
extra_headers=_NO_STORE_HEADERS,
)
token_value = self.tokens.issue_token(self.config.token_ttl_s)
return _http_json_response(token_response_payload(token_value, self.config.token_ttl_s))
return _http_json_response(
token_response_payload(token_value, self.config.token_ttl_s),
extra_headers=_NO_STORE_HEADERS,
)
# -- Bootstrap ----------------------------------------------------------
@@ -579,7 +587,7 @@ class GatewayHTTPHandler:
"runtime_surface": self._runtime_surface,
"runtime_capabilities": self._capabilities,
}
return _http_json_response(payload)
return _http_json_response(payload, extra_headers=_NO_STORE_HEADERS)
api_token_allowed = bool(secret) or is_local_browser
if not self.tokens.can_issue(include_api_token=api_token_allowed):
@@ -587,6 +595,7 @@ class GatewayHTTPHandler:
json.dumps({"error": "too many outstanding tokens"}).encode("utf-8"),
status=429,
content_type="application/json; charset=utf-8",
extra_headers=_NO_STORE_HEADERS,
)
token = self.tokens.issue_token(self.config.token_ttl_s, audience="webui")
api_token = (
@@ -611,7 +620,7 @@ class GatewayHTTPHandler:
}
if api_token is not None:
payload["api_token"] = api_token
return _http_json_response(payload)
return _http_json_response(payload, extra_headers=_NO_STORE_HEADERS)
def _bootstrap_ws_url(self, request: Any) -> str:
headers = getattr(request, "headers", {}) or {}