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

commit90323e8dd4a019cc6f22128f37e2a0891497f872
parent51edbfd14e
authorLucas Galante <[email protected]>
date2026-08-27 08:51
feat: cce://cookies, a confirm-then-clear page for site data

Cookies became persistent earlier today, which means the browser could
only ever accumulate state — no way to sign out of everything, and no way
to recover a site whose cookie state has wedged.

Deliberately a page with a link rather than a chord that acts: persistent
logins make an accidental keystroke expensive. Ctrl+Shift+Delete (and
about:cookies) opens the page; the clear happens only on following the
link, and a confirmation page replaces it.

The plumbing is the interesting part. The cce: protocol handler runs on
Servo's fetch threads and cannot reach the Servo instance, so
cce://cookies/clear raises a shared flag and returns the confirmation
page; the app's next pump sees the flag and calls
SiteDataManager::clear_cookies. Same shape as the other internal pages,
which own Arc'd stores rather than engine handles.

Shadow-verified against a local server that echoes the Cookie header it
receives: cookie present and sent -> chord opens the page -> follow the
link -> reload the site -> COOKIE ABSENT.

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

 src/main.rs    | 10 ++++++++++
 src/pages.rs   | 30 ++++++++++++++++++++++++++++++
 src/webview.rs |  9 +++++++++
 3 files changed, 49 insertions(+)

diff --git a/src/main.rs b/src/main.rs
index 5c57f0f..400a10a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -202,6 +202,9 @@ fn parse_url_input(input: &str, search_prefix: &str) -> Option<Url> {
     if s.eq_ignore_ascii_case("about:downloads") {
         return Url::parse("cce://downloads").ok();
     }
+    if s.eq_ignore_ascii_case("about:cookies") {
+        return Url::parse("cce://cookies").ok();
+    }
     if let Ok(u) = Url::parse(s) {
         if matches!(u.scheme(), "http" | "https" | "file" | "data" | "about" | "cce") {
             return Some(u);
@@ -810,6 +813,13 @@ impl Application for BrowserApp {
                     *needs_rebuild = true;
                     return self.close_tab(self.host.active_index());
                 }
+                // Ctrl+Shift+Delete opens the cookie page rather than
+                // clearing outright; the page asks first.
+                Key::Named(NamedKey::Delete) if event.shift => {
+                    self.open_internal_page("cce://cookies");
+                    *needs_rebuild = true;
+                    return None;
+                }
                 Key::Character(c) if c == "h" || c == "b" || c == "j" => {
                     let page = match c.as_str() {
                         "h" => "cce://history",
diff --git a/src/pages.rs b/src/pages.rs
index 4c78ab5..8c60d83 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -257,6 +257,31 @@ pub struct CceProtocol {
     pub history: Arc<History>,
     pub bookmarks: Arc<Bookmarks>,
     pub downloads: Arc<crate::downloads::Downloads>,
+    /// Raised by cce://cookies/clear. The handler runs on fetch threads and
+    /// cannot reach Servo, so it flags the request and the app's next pump
+    /// performs the clear through the SiteDataManager.
+    pub clear_cookies: Arc<std::sync::atomic::AtomicBool>,
+}
+
+/// Confirmation page for clearing cookies. Deliberately a page with a link
+/// rather than a chord that acts immediately: logins persist now, so an
+/// accidental keystroke would sign the user out of everything.
+fn cookies_page() -> String {
+    page(
+        "Cookies",
+        "Signed-in sessions live here",
+        "<div class=e><span class=w></span><span class=u>Clearing cookies signs you out of          every site and cannot be undone. Bookmarks and history are untouched.</span></div>         <div class=e><span class=w></span>         <a class=rm href=\"cce://cookies/clear\">Clear all cookies</a></div>",
+        "",
+    )
+}
+
+fn cookies_cleared_page() -> String {
+    page(
+        "Cookies",
+        "Cleared",
+        "<div class=e><span class=w></span><span class=u>All cookies were cleared.          Sites you were signed in to will ask you to sign in again.</span></div>",
+        "",
+    )
 }
 
 impl ProtocolHandler for CceProtocol {
@@ -289,6 +314,11 @@ impl ProtocolHandler for CceProtocol {
                 self.downloads.clear_finished();
                 Some(self.downloads.html())
             }
+            "cookies" => Some(cookies_page()),
+            "cookies/clear" => {
+                self.clear_cookies.store(true, std::sync::atomic::Ordering::SeqCst);
+                Some(cookies_cleared_page())
+            }
             _ => None,
         };
         let response = match body {
diff --git a/src/webview.rs b/src/webview.rs
index 2a3c909..3d342d5 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -242,6 +242,8 @@ pub struct ServoHost {
     force_dark: bool,
     /// Deadlines for pending reloads, earliest last (popped off the back).
     reload_at: Vec<std::time::Instant>,
+    /// Raised by the cce://cookies/clear page; acted on here in `pump`.
+    clear_cookies: std::sync::Arc<std::sync::atomic::AtomicBool>,
 }
 
 impl ServoHost {
@@ -312,11 +314,13 @@ impl ServoHost {
         let history = std::sync::Arc::new(History::load());
         let bookmarks = std::sync::Arc::new(Bookmarks::load());
         let downloads = std::sync::Arc::new(Downloads::default());
+        let clear_cookies = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
         let mut protocols = ProtocolRegistry::default();
         let handler = CceProtocol {
             history: history.clone(),
             bookmarks: bookmarks.clone(),
             downloads: downloads.clone(),
+            clear_cookies: clear_cookies.clone(),
         };
         if let Err(e) = protocols.register("cce", handler) {
             log::error!("failed to register cce: protocol: {e:?}");
@@ -394,6 +398,7 @@ impl ServoHost {
             force_dark_sheet,
             force_dark,
             reload_at: Vec::new(),
+            clear_cookies,
         };
         host.open_tab(url);
         host
@@ -510,6 +515,10 @@ impl ServoHost {
     /// tab if it produced a frame. Returns (new frame, any state change).
     pub fn pump(&mut self) -> (bool, bool) {
         self.servo.spin_event_loop();
+        if self.clear_cookies.swap(false, std::sync::atomic::Ordering::SeqCst) {
+            self.servo.site_data_manager().clear_cookies(None);
+            log::info!("cleared all cookies");
+        }
         if self.reload_at.last().is_some_and(|at| std::time::Instant::now() >= *at) {
             self.reload_at.pop();
             for tab in &self.tabs {