Prove cookie injection lands, rather than assuming it
setCookie is fire-and-forget, so the import's count was what WebKit was handed, not what it kept. A probe from inside the page reads back the other end: pairing against Arc gave `names=tz,cids,frontend_lang` for aputure.odoo.com, and `tz` exists only in Arc's store — so the import demonstrably landed. The sites still ask for sign-in. That is the far end refusing the session, not a broken import, and the two are now distinguishable instead of being guessed at.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -453,11 +453,9 @@ pub fn notification_status(app: AppHandle) -> String {
|
||||
Err(e) => format!("failed: {e}"),
|
||||
};
|
||||
let app_state = app.state::<AppState>();
|
||||
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::<AppState>().diag.lock().unwrap().clone();
|
||||
if diag.is_empty() { "no answer from the page".into() } else { diag }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- browser pairing
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
@@ -46,3 +46,5 @@ export const pairBrowser = (browser: string) =>
|
||||
export const testNotification = (appId: string) =>
|
||||
invoke<void>("test_notification", { appId });
|
||||
export const notificationStatus = () => invoke<string>("notification_status");
|
||||
export const probeCookies = (appId: string) => invoke<void>("probe_cookies", { appId });
|
||||
export const cookieProbe = () => invoke<string>("cookie_probe");
|
||||
|
||||
@@ -46,6 +46,7 @@ export default function Settings({
|
||||
const [browser, setBrowser] = useState("");
|
||||
const [pairing, setPairing] = useState(false);
|
||||
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
|
||||
const [cookieStatus, setCookieStatus] = useState<string | null>(null);
|
||||
const [paired, setPaired] = useState<PairResult | null>(null);
|
||||
|
||||
const groups = [...config.groups].sort((a, b) => a.order - b.order);
|
||||
@@ -263,11 +264,29 @@ export default function Settings({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!activeId) return;
|
||||
setCookieStatus("checking…");
|
||||
await api.probeCookies(activeId);
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
setCookieStatus(await api.cookieProbe());
|
||||
}}
|
||||
disabled={!activeId}
|
||||
title="What the current app can actually see"
|
||||
className={BTN}
|
||||
>
|
||||
Check cookies
|
||||
</button>
|
||||
<button onClick={pair} disabled={pairing || !browser} className={BTN_PRIMARY}>
|
||||
{pairing ? <Spinner /> : config.settings.lastPairedAt ? "Pair again" : "Pair"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cookieStatus && (
|
||||
<p className={`${HELP} font-mono`}>{cookieStatus}</p>
|
||||
)}
|
||||
|
||||
{config.settings.lastPairedAt && !paired && (
|
||||
<p className={HELP}>Last paired {config.settings.lastPairedAt}.</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user