fix: persistent profile across headed↔headless so IndexedDB auth survives#1
Open
gabelul wants to merge 1 commit into
Open
fix: persistent profile across headed↔headless so IndexedDB auth survives#1gabelul wants to merge 1 commit into
gabelul wants to merge 1 commit into
Conversation
The headless browser used `chromium.launch()` + `browser.newContext()` (ephemeral context, no userDataDir), while `handoff()` used `launchPersistentContext` against `~/.gstack/chromium-profile`. The state-transfer machinery (saveState/restoreState) only round-tripped cookies + localStorage + sessionStorage — IndexedDB was silently dropped on every swap. Sites that store auth in IndexedDB (Supabase, Firebase) lost their session every time we swapped headed↔headless or every time the server restarted. Mobbin in particular: log in via handoff, IDB token written to disk, resume → fresh ephemeral context → no IDB → logged out. Fix: - `launch()` now uses `launchPersistentContext` with the same userDataDir as `handoff()` and `launchHeaded()`. All storage (cookies, localStorage, sessionStorage, IndexedDB, ServiceWorkers) lives on disk in the shared profile. - `handoff()` closes the headless context BEFORE opening headed (Chromium profile is single-writer). No save/restore needed — disk state transfers automatically. - `resume()` is now async and actually swaps headed→headless via the same profile dir. Previously it was a no-op that just cleared refs, which left users stuck in headed mode. - `meta-commands.ts` awaits the now-async `bm.resume()`. Verified end-to-end: Mobbin "Invite & earn" UI (auth-only) renders in headless mode on a fresh server start, with no fresh handoff needed. The IndexedDB Supabase JWT from a prior handoff persists on disk and is read on launch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The browse tool drops IndexedDB on every state transfer between headless and headed contexts. Sites that store auth in IndexedDB (Supabase, Firebase, Adobe SSO) lose their session on every
handoff→resume, and on every server restart.Concrete repro: log in to Mobbin via
handoff(Mobbin uses Supabase Auth, JWT stored in IndexedDB). Runresume. Rungoto https://mobbin.com/browse/ios/apps. Snapshot — UI shows logged out.Root cause
Two architectural mismatches:
launch()useschromium.launch()+browser.newContext()— an ephemeral context with nouserDataDir. State has nowhere to persist.handoff()useslaunchPersistentContext('~/.gstack/chromium-profile', …)— state DOES land on disk, but the next headless launch can't see it.saveState/restoreState) only round-trips cookies + localStorage + sessionStorage. IndexedDB is silently dropped.resume()is a no-op that just clears refs — it doesn't actually swap back to headless, leaving callers stuck in headed mode.Fix
Make every browser context share the same persistent profile dir on disk. State (including IndexedDB, ServiceWorkers, etc.) transfers automatically because both contexts read the same files.
launch()→launchPersistentContext(~/.gstack/chromium-profile, { headless: true, … }). Adopts the auto-created blank page as tab 1 instead of opening a duplicate.handoff()→ close the headless context BEFORE opening headed (Chromium profile is single-writer). Drop thesaveState/restoreStatecalls — they're unnecessary now.resume()→ now async. Closes headed and re-launches headless against the same profile dir. Previous behaviour (no-op) preserved when called outside headed mode.meta-commands.ts→await bm.resume().Side effects
handoffwas already doing for headed mode.saveState/restoreStatemachinery is still used byrecreateContext()(for user-agent changes) and untouched there.~/.gstack/chromium-profileat a time. If a secondbrowseserver starts while the first is alive, it'll fail on the profile lock — same behaviour as Chrome itself with concurrent profile use.Verification
Tested on macOS arm64 with Bun 1.3.12 + Playwright 1.59.1.
Diff
3 files changed, 135 insertions(+), 65 deletions(-)