From 587c19c2a58503648bc780c485c70d20e86e489e Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Sat, 15 Aug 2026 17:22:58 -0500 Subject: [PATCH] fix(hub): persistent browser profile so logout survives window reopen The Hub launcher used an ephemeral /tmp profile deleted on exit, which wiped the hub_manual_logout marker cookie. On reopen, /auto-login minted a new session and logged the user straight back in without a password. Use a persistent per-user profile under XDG_STATE_HOME and drop the deletion trap so the logout marker survives close/reopen. Keep --skip-origin-startup-dialog. Adds regression tests. --- CHANGELOG.md | 12 ++++++++---- modules/core/sovran-hub.nix | 26 ++++++++++++++++++-------- tests/test_security.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b63e286..1e9d4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Sovran Hub launcher runs it. ### Fixed - - The Hub launcher now passes `--skip-origin-startup-dialog` to Brave - Origin so its one-time "Proceed with Origin for free on Linux" onboarding - screen doesn't reappear (and block auto-login) on every launch, since the - Hub runs with a throwaway profile. + - The Hub launcher now uses a persistent per-user browser profile + (`$XDG_STATE_HOME/sovran-hub-browser`) instead of a throwaway `/tmp` + profile that was deleted on exit. The throwaway profile wiped the + `hub_manual_logout` marker cookie on every close, so `/auto-login` + silently logged the user straight back in after they signed out and + reopened the Hub window. The persistent profile makes explicit logout + stick (until the next password login) while still skipping Brave Origin's + one-time startup dialog. - The Hub now redirects to the login page when an API request finds an expired browser session, instead of leaving the dashboard tile grid in a loading state while `/api/services` continues returning 401 every five diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index a195937..fec5cb6 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -282,19 +282,29 @@ let fi ''; - # ── Brave Origin launcher wrapper: stable profile dir so Wayland app_id is - # deterministic and GNOME Shell can match the window to the .desktop - # entry (fixes generic gear icon appearing in the dock). + # ── Brave Origin launcher wrapper: a *persistent* per-user profile dir. + # It must NOT be wiped on exit: the Hub's logout marker cookie + # (hub_manual_logout) and the session cookie live in this profile. + # Launching from a fresh/ephemeral profile every time throws away the + # marker, so /auto-login would mint a new session and silently log the + # user straight back in after they signed out and reopened the window. + # A stable directory also keeps the Wayland app_id deterministic so + # GNOME Shell can match the window to the .desktop entry (dock icon). hub-brave-wrapper = pkgs.writeShellScript "sovran-hub-brave.sh" '' export PATH="${lib.makeBinPath [ pkgs.brave-origin pkgs.coreutils ]}:$PATH" - HUB_DATA="/tmp/sovran-hub-brave-$(id -u)" + # Per-user, persistent browser state. $XDG_STATE_HOME keeps it out of the + # way of backups and survives reboots and window close/reopen. + if [ -n "$XDG_STATE_HOME" ]; then + HUB_DATA="$XDG_STATE_HOME/sovran-hub-browser" + else + HUB_DATA="$HOME/.local/state/sovran-hub-browser" + fi mkdir -p "$HUB_DATA" - trap '[ -n "$HUB_DATA" ] && rm -rf "$HUB_DATA"' EXIT INT TERM export BAMF_DESKTOP_FILE_HINT="/run/current-system/sw/share/applications/sovran-hub.desktop" export GIO_LAUNCHED_DESKTOP_FILE="/run/current-system/sw/share/applications/sovran-hub.desktop" - # The Hub launches with a throwaway profile, so Brave Origin's one-time - # "Proceed with Origin for free on Linux" onboarding dialog would reappear - # on every launch and block the auto-login. Skip it (Linux-only switch). + # With a persistent profile Brave Origin's one-time "Proceed with Origin + # for free on Linux" onboarding dialog only appears once; keep skipping it + # anyway so it can never block auto-login (Linux-only switch). brave-origin --app=http://localhost:8937/auto-login \ --skip-origin-startup-dialog \ --class=sovran-hub \ diff --git a/tests/test_security.py b/tests/test_security.py index 69ea47b..08e214c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -424,6 +424,41 @@ class TestManualLogoutPersistence(unittest.TestCase): ) +class TestHubBrowserProfilePersistence(unittest.TestCase): + """The desktop launcher must keep a persistent browser profile. + + The hub_manual_logout marker (and the session cookie) are stored in this + profile. If the launcher used an ephemeral /tmp profile that it deleted on + exit, closing and reopening the Hub window would wipe the marker and + /auto-login would silently log the user back in without a password. + """ + + @classmethod + def setUpClass(cls): + path = os.path.join( + _REPO_ROOT, "modules", "core", "sovran-hub.nix" + ) + with open(path, encoding="utf-8") as f: + cls.wrapper = f.read() + + def test_profile_is_not_under_tmp(self): + # The profile must live in a persistent per-user location, not /tmp. + self.assertNotIn("/tmp/sovran-hub-brave", self.wrapper) + + def test_profile_is_not_deleted_on_exit(self): + # There must be no trap that removes the user-data-dir on exit. + self.assertNotRegex(self.wrapper, r"rm\s+-rf\s+.*HUB_DATA") + self.assertNotIn("trap '", self.wrapper) + + def test_profile_is_persistent_per_user_location(self): + self.assertIn("sovran-hub-browser", self.wrapper) + # It should honour XDG_STATE_HOME (standard, persistent per-user dir). + self.assertIn("XDG_STATE_HOME", self.wrapper) + + def test_launcher_still_uses_user_data_dir(self): + self.assertIn("--user-data-dir=", self.wrapper) + + # --------------------------------------------------------------------------- # Persistent session store # ---------------------------------------------------------------------------