diff --git a/docs/superpowers/specs/2026-09-01-work-app-design.md b/docs/superpowers/specs/2026-09-01-work-app-design.md index f974324..018d570 100644 --- a/docs/superpowers/specs/2026-09-01-work-app-design.md +++ b/docs/superpowers/specs/2026-09-01-work-app-design.md @@ -149,6 +149,17 @@ Settings lists the browsers actually installed. Pairing with a Chromium browser: list. Nothing else is read out of the browser. 4. Inject them into `WKHTTPCookieStore`. +**Verified on the machine.** Pairing against Arc imported 43 cookies across 9 domains, +and a probe from inside the page then read back `host=example.odoo.com +names=tz,cids,frontend_lang visible=3` — `tz` existing only in Arc's store, which is what +proves the import landed rather than merely being handed over. `setCookie` is +fire-and-forget, so the import's own count could never have shown this. + +The sites still presented sign-in pages. That is the documented limitation, not a broken +import: the cookies are in the store and visible to the page, and the session is being +refused at the far end. Settings keeps the probe as **Check cookies**, so the same +question can be answered again without guessing. + Pairing is a button, not a background job. Cookies rotate; a silent task that periodically reaches into the Keychain is worse than one the user presses when something logs them out. diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f028bd1..af2e212 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -453,11 +453,9 @@ pub fn notification_status(app: AppHandle) -> String { Err(e) => format!("failed: {e}"), }; let app_state = app.state::(); - let diag = app_state.diag.lock().unwrap().clone(); let last = app_state.last_notification.lock().unwrap().clone(); - let page = if diag.is_empty() { "not run yet".into() } else { diag }; let from_page = if last.is_empty() { "none yet".into() } else { last }; - format!("permission: {state} · direct: {raised} · page: {page} · from page: {from_page}") + format!("permission: {state} · direct: {raised} · from page: {from_page}") } /// Asks macOS for notification permission, once, at startup. @@ -503,6 +501,41 @@ pub fn test_notification(app_id: String, app: AppHandle) -> Result<(), String> { .map_err(|e| e.to_string()) } +/// Asks the page which cookies it can actually see. +/// +/// `setCookie` is fire-and-forget, so the import's count is what was handed to +/// WebKit, not what WebKit kept. This reads the other end. HttpOnly cookies are +/// invisible to script by design, so the answer is a floor, not a total — but a +/// zero here means the injection never landed at all. +#[tauri::command] +pub fn probe_cookies(app_id: String, app: AppHandle) -> Result<(), String> { + let wv = app + .get_webview(&webviews::label_for(&app_id)) + .ok_or_else(|| format!("{app_id} has no webview"))?; + wv.eval( + r#"(function () { + var names = document.cookie + ? document.cookie.split(';').map(function (c) { return c.split('=')[0].trim(); }) + : []; + if (window.__workAppSend) { + window.__workAppSend('diag', { + host: location.hostname, + visible: String(names.length), + names: names.slice(0, 12).join(',') + }); + } + })();"#, + ) + .map_err(|e| e.to_string()) +} + +/// What the last cookie probe saw. +#[tauri::command] +pub fn cookie_probe(app: AppHandle) -> String { + let diag = app.state::().diag.lock().unwrap().clone(); + if diag.is_empty() { "no answer from the page".into() } else { diag } +} + // ------------------------------------------------------- browser pairing #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7993c13..5972779 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -85,6 +85,8 @@ pub fn run() { commands::pick_hidden, commands::test_notification, commands::notification_status, + commands::probe_cookies, + commands::cookie_probe, commands::list_browsers, commands::pair_browser, ]) diff --git a/src/api.ts b/src/api.ts index 0fcd608..e8407f5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -46,3 +46,5 @@ export const pairBrowser = (browser: string) => export const testNotification = (appId: string) => invoke("test_notification", { appId }); export const notificationStatus = () => invoke("notification_status"); +export const probeCookies = (appId: string) => invoke("probe_cookies", { appId }); +export const cookieProbe = () => invoke("cookie_probe"); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 1af10d0..64fa6f3 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -46,6 +46,7 @@ export default function Settings({ const [browser, setBrowser] = useState(""); const [pairing, setPairing] = useState(false); const [notifyStatus, setNotifyStatus] = useState(null); + const [cookieStatus, setCookieStatus] = useState(null); const [paired, setPaired] = useState(null); const groups = [...config.groups].sort((a, b) => a.order - b.order); @@ -263,11 +264,29 @@ export default function Settings({ ))} + + {cookieStatus && ( +

{cookieStatus}

+ )} + {config.settings.lastPairedAt && !paired && (

Last paired {config.settings.lastPairedAt}.

)}