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.
|
list. Nothing else is read out of the browser.
|
||||||
4. Inject them into `WKHTTPCookieStore`.
|
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
|
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.
|
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}"),
|
Err(e) => format!("failed: {e}"),
|
||||||
};
|
};
|
||||||
let app_state = app.state::<AppState>();
|
let app_state = app.state::<AppState>();
|
||||||
let diag = app_state.diag.lock().unwrap().clone();
|
|
||||||
let last = app_state.last_notification.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 };
|
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.
|
/// 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())
|
.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
|
// ------------------------------------------------------- browser pairing
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ pub fn run() {
|
|||||||
commands::pick_hidden,
|
commands::pick_hidden,
|
||||||
commands::test_notification,
|
commands::test_notification,
|
||||||
commands::notification_status,
|
commands::notification_status,
|
||||||
|
commands::probe_cookies,
|
||||||
|
commands::cookie_probe,
|
||||||
commands::list_browsers,
|
commands::list_browsers,
|
||||||
commands::pair_browser,
|
commands::pair_browser,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -46,3 +46,5 @@ export const pairBrowser = (browser: string) =>
|
|||||||
export const testNotification = (appId: string) =>
|
export const testNotification = (appId: string) =>
|
||||||
invoke<void>("test_notification", { appId });
|
invoke<void>("test_notification", { appId });
|
||||||
export const notificationStatus = () => invoke<string>("notification_status");
|
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 [browser, setBrowser] = useState("");
|
||||||
const [pairing, setPairing] = useState(false);
|
const [pairing, setPairing] = useState(false);
|
||||||
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
|
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
|
||||||
|
const [cookieStatus, setCookieStatus] = useState<string | null>(null);
|
||||||
const [paired, setPaired] = useState<PairResult | null>(null);
|
const [paired, setPaired] = useState<PairResult | null>(null);
|
||||||
|
|
||||||
const groups = [...config.groups].sort((a, b) => a.order - b.order);
|
const groups = [...config.groups].sort((a, b) => a.order - b.order);
|
||||||
@@ -263,11 +264,29 @@ export default function Settings({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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}>
|
<button onClick={pair} disabled={pairing || !browser} className={BTN_PRIMARY}>
|
||||||
{pairing ? <Spinner /> : config.settings.lastPairedAt ? "Pair again" : "Pair"}
|
{pairing ? <Spinner /> : config.settings.lastPairedAt ? "Pair again" : "Pair"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{cookieStatus && (
|
||||||
|
<p className={`${HELP} font-mono`}>{cookieStatus}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{config.settings.lastPairedAt && !paired && (
|
{config.settings.lastPairedAt && !paired && (
|
||||||
<p className={HELP}>Last paired {config.settings.lastPairedAt}.</p>
|
<p className={HELP}>Last paired {config.settings.lastPairedAt}.</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user