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.
This commit is contained in:
2026-08-15 17:22:58 -05:00
parent c862ed5806
commit 587c19c2a5
3 changed files with 61 additions and 12 deletions
+8 -4
View File
@@ -17,10 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Sovran Hub launcher runs it. Sovran Hub launcher runs it.
### Fixed ### Fixed
- The Hub launcher now passes `--skip-origin-startup-dialog` to Brave - The Hub launcher now uses a persistent per-user browser profile
Origin so its one-time "Proceed with Origin for free on Linux" onboarding (`$XDG_STATE_HOME/sovran-hub-browser`) instead of a throwaway `/tmp`
screen doesn't reappear (and block auto-login) on every launch, since the profile that was deleted on exit. The throwaway profile wiped the
Hub runs with a throwaway profile. `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 - 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 expired browser session, instead of leaving the dashboard tile grid in a
loading state while `/api/services` continues returning 401 every five loading state while `/api/services` continues returning 401 every five
+18 -8
View File
@@ -282,19 +282,29 @@ let
fi fi
''; '';
# ── Brave Origin launcher wrapper: stable profile dir so Wayland app_id is # ── Brave Origin launcher wrapper: a *persistent* per-user profile dir.
# deterministic and GNOME Shell can match the window to the .desktop # It must NOT be wiped on exit: the Hub's logout marker cookie
# entry (fixes generic gear icon appearing in the dock). # (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" '' hub-brave-wrapper = pkgs.writeShellScript "sovran-hub-brave.sh" ''
export PATH="${lib.makeBinPath [ pkgs.brave-origin pkgs.coreutils ]}:$PATH" 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" 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 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" 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 # With a persistent profile Brave Origin's one-time "Proceed with Origin
# "Proceed with Origin for free on Linux" onboarding dialog would reappear # for free on Linux" onboarding dialog only appears once; keep skipping it
# on every launch and block the auto-login. Skip it (Linux-only switch). # anyway so it can never block auto-login (Linux-only switch).
brave-origin --app=http://localhost:8937/auto-login \ brave-origin --app=http://localhost:8937/auto-login \
--skip-origin-startup-dialog \ --skip-origin-startup-dialog \
--class=sovran-hub \ --class=sovran-hub \
+35
View File
@@ -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 # Persistent session store
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------