#!/usr/bin/env python3 """ Signing out while a token renewal is in flight must not leave a half a session. A tab left open overnight is exactly this race: the idle watch signs the browser out at the same moment the renewal fires — one second apart in the hub's log. `refreshAccessToken` then came back to an `_auth` that was already null and wrote `{ ..._auth, token, refreshToken }`; spreading null is silent, so what landed in localStorage was a token with **no identity at all**. The app believes that object is a session. It renders the signed-in interface from it and throws on the first field it reads — `user.username[0]` — so every load afterwards is a blank page, and it is stored, so it survives reloads, cache clearing and the device restarting. A reader has no way back but clearing the site's data, which nothing on screen used to mention. Driven against the shipped `hub-client.js` in a real browser, with `fetch` stubbed so the renewal can be held open across the sign-out. auth_race_probe.py Prints JSON. """ import http.server import json import socketserver import subprocess import sys import tempfile import threading import time from pathlib import Path STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static" PORT = 8767 RECORDS = [] socketserver.TCPServer.allow_reuse_address = True PAGE = r"""
""" class H(http.server.BaseHTTPRequestHandler): def log_message(self, *a): pass def do_POST(self): length = int(self.headers.get("Content-Length") or 0) if self.path == "/log": RECORDS.append(json.loads(self.rfile.read(length).decode())) else: self.rfile.read(length) self.send_response(204) self.end_headers() def _send(self, body: bytes, ctype: str) -> None: self.send_response(200) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): path = self.path.split("?")[0] if path == "/": self._send(PAGE.encode(), "text/html; charset=utf-8") return asset = (STATIC / path.lstrip("/")).resolve() if not str(asset).startswith(str(STATIC)) or not asset.is_file(): self.send_response(404) self.end_headers() return self._send(asset.read_bytes(), "text/javascript") def main() -> int: with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv: threading.Thread(target=srv.serve_forever, daemon=True).start() with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile: proc = subprocess.Popen( ["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox", f"--user-data-dir={profile}", f"http://127.0.0.1:{PORT}/"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(300): if RECORDS: break time.sleep(0.1) proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() proc.wait() if not RECORDS: print(json.dumps({"error": "no measurement"}), file=sys.stderr) return 1 print(json.dumps(RECORDS[0], indent=1)) return 0 if __name__ == "__main__": raise SystemExit(main())