git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commitd2c6b1a26ea49974401aae5727cb035d603284ce
parent8cce9f734d
authorLucas Galante <[email protected]>
date2026-09-01 14:34
fix: persist cookies on disk in the WPE backend

A persistent WebKitNetworkSession persists website data (localStorage,
IndexedDB, service workers) on its own, but the cookie store stays
memory-only until webkit_cookie_manager_set_persistent_storage() names a
file — so every launch started logged out of every cookie-backed site
(Google), while token-in-localStorage logins (Cloudflare) survived and
disguised the gap. Cookies now live in profile/cookies.sqlite, 0700 dir.

Also wire the clear_cookies relay in pump(): cce://cookies/clear set the
flag but nothing on the WPE side ever consumed it. It now clears the
session's cookies via the website data manager, mirroring ServoHost.

Verified in a shadow session: stable Google cookies (AEC,
SEARCH_SAMESITE) keep their values across a browser restart, proving the
jar is read back and sent, not just written.

Co-Authored-By: Claude Fable 5 <[email protected]>

 WPE-PORT.md     | 13 ++++++++++---
 src/wpe/host.rs | 35 +++++++++++++++++++++++++++++++----
 2 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/WPE-PORT.md b/WPE-PORT.md
index 1f606e4..87ae6ce 100644
--- a/WPE-PORT.md
+++ b/WPE-PORT.md
@@ -278,9 +278,16 @@ been served one), but it is a far higher bar than the marketing page that earlie
 used: a heavy JS application behind Cloudflare's own protection, reached through a
 login. The engine-identity worry that motivated half this document has not materialised.
 
-**Cookies persist.** The login survives in WebKit's own origin-keyed store under
-`~/.local/state/cce/browser/profile/storage`. (`cookie_jar.json` beside it is Servo's
-format, now dead weight.)
+**Website data persists — but cookies did not, at first.** What survived restarts
+in the early days was WebKit's origin-keyed store under
+`~/.local/state/cce/browser/profile/storage` (localStorage, IndexedDB, service
+workers), which a persistent `WebKitNetworkSession` writes on its own — and which
+made this section originally claim "cookies persist". They didn't: WebKit's cookie
+store is memory-only until `webkit_cookie_manager_set_persistent_storage()` names a
+file, so cookie-backed logins (Google) evaporated with the process while
+token-in-localStorage logins (Cloudflare) survived, disguising the gap. Fixed
+2026-09-01: cookies now live in `profile/cookies.sqlite`. (`cookie_jar.json` beside
+it is Servo's format, now dead weight.)
 
 **Three bugs found by use, none by testing:**
 
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index 253a396..71321ad 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -176,10 +176,13 @@ impl WebKitHost {
                 "wpe_display_connect failed"
             );
 
-            // Persisted profile: without a data directory WebKit keeps cookies
-            // in memory only, so every launch starts logged out of every site.
-            // Same location and the same 0700 reasoning as the Servo backend —
-            // the jar holds live sessions.
+            // Persisted profile. The data directory persists website data
+            // (localStorage, IndexedDB, service workers) on its own, but the
+            // cookie store stays memory-only until it is explicitly given a
+            // file — the set_persistent_storage call below, without which
+            // every launch starts logged out of every site even though the
+            // rest of the profile survives. Same location and the same 0700
+            // reasoning as the Servo backend — the jar holds live sessions.
             let profile = crate::pages::state_dir().join("profile");
             let _ = std::fs::create_dir_all(&profile);
             {
@@ -191,6 +194,12 @@ impl WebKitHost {
                 cstr(&profile.join("cache").to_string_lossy()),
             );
             let session = webkit_network_session_new(data_dir.as_ptr(), cache_dir.as_ptr());
+            let cookie_db = cstr(&profile.join("cookies.sqlite").to_string_lossy());
+            webkit_cookie_manager_set_persistent_storage(
+                webkit_network_session_get_cookie_manager(session),
+                cookie_db.as_ptr(),
+                WebKitCookiePersistentStorage::WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE,
+            );
 
             let history = std::sync::Arc::new(crate::pages::History::load());
             let bookmarks = std::sync::Arc::new(crate::pages::Bookmarks::load());
@@ -487,6 +496,24 @@ impl WebKitHost {
         if let Some(p) = &self.poll {
             p.drain();
         }
+        // `cce://cookies/clear` runs on WebKit's fetch path and cannot reach
+        // the session from there, so it sets the flag and this acts on it —
+        // the same relay `ServoHost::pump` uses. Timespan 0 clears them all.
+        if self
+            .clear_cookies
+            .swap(false, std::sync::atomic::Ordering::SeqCst)
+        {
+            unsafe {
+                webkit_website_data_manager_clear(
+                    webkit_network_session_get_website_data_manager(self.session),
+                    WebKitWebsiteDataTypes::WEBKIT_WEBSITE_DATA_COOKIES,
+                    0,
+                    std::ptr::null_mut(),
+                    None,
+                    std::ptr::null_mut(),
+                );
+            }
+        }
         unsafe {
             while g_main_context_iteration(std::ptr::null_mut(), 0) != 0 {}
         }