From 15f95b1dd6ea0d6d37de1eb9327a09d3b2d5809e Mon Sep 17 00:00:00 2001 From: Zohar Zilberman Date: Fri, 21 Aug 2026 09:00:47 +0000 Subject: [PATCH 1/2] Show the credential overlay when the page asks for the key as it loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay was injected on `dom-ready`, which is after the page's own scripts have run. A sign-in page that starts listening for the security key as it boots — Google's 2SV challenge does exactly that — has already called `navigator.credentials.get()` by then, so wrapping `navigator.credentials` afterwards wrapped nothing: no toast, no window title, no dock bounce, no log line. An AWS SSO login backed by Google showed the user the same silent, apparently-hung window as before issue #17 was fixed. Register the overlay through the WebContents debugger instead, with `Page.addScriptToEvaluateOnNewDocument`, so it runs at document start in the page's own world — before any script the page ships, and again on the cross-origin hop from AWS to the identity provider, which moves the page to a different renderer process. Three details all fail silently and are spelled out where they live: - `Page.enable` has to come first. Without it the registration resolves and does nothing. - There has to be a renderer to talk to. On a window that has not loaded anything the command never resolves — not an error, a hung promise — so `attachLoginIndicator()` loads about:blank first, is now async, and must be awaited before `loadURL`. `getNewToken()` awaits it, and treats a window closed while it happens as the abort it is. - It reaches the top-level document and any frame in its process, but not a cross-origin iframe, which gets its own CDP target. The `dom-ready` injection stays as the fallback for those, and for a debugger that could not attach. `npm run check:overlay` is the regression test: it drives the real `attachLoginIndicator()` on a real BrowserWindow against pages that ask for a key before and after `dom-ready`, and asserts the wait reached the main process. No security key needed — only the start of the request matters. It fails on the code before this commit and passes after, and CI runs it under xvfb, which the app turns out to run under perfectly well. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014s8Y8cKzE4bpyaxPMP19wi --- .github/workflows/ci.yaml | 10 ++ AGENTS.md | 55 +++++++++-- package.json | 1 + src/aws-sso.ts | 18 +++- src/login-indicator.ts | 118 ++++++++++++++++++++++- tools/check-login-overlay.js | 179 +++++++++++++++++++++++++++++++++++ 6 files changed, 367 insertions(+), 14 deletions(-) create mode 100644 tools/check-login-overlay.js diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a68653..109f491 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,6 +48,16 @@ jobs: run: | npm run lint + # The login window's credential overlay fails silently when it breaks - + # no error, just a sign-in page that shows nothing while it waits for a + # security key. This drives a real BrowserWindow through a real WebAuthn + # wait and asserts the overlay reported it. It needs a display (xvfb) and + # --no-sandbox, because the runner's Electron has no setuid sandbox + # helper; it needs no security key. + - name: 🔑 Check the login overlay + run: | + xvfb-run -a npm run check:overlay -- --no-sandbox + build: needs: lint uses: ./.github/workflows/build.yaml diff --git a/AGENTS.md b/AGENTS.md index eb4eb78..15e3996 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,9 +69,12 @@ npm only (`package-lock.json`; CI runs `npm ci`). Do not add a `yarn.lock`. - `npm run build` — `tsc`, then copies the tray icons and `dashboard.html`. - `npm run lint` — oxlint, configured by `.oxlintrc.json`. +- `npm run check:overlay` — drives the login window's credential overlay + through a real WebAuthn wait. Needs `npm run build` first, and a display: + `xvfb-run -a npm run check:overlay -- --no-sandbox`. - `npm start` / `npm run package` / `npm run make` — Electron Forge. -Build and lint both run in CI. Neither proves the app launches; see +All three run in CI. Build and lint alone do not prove the app launches; see "Verification limits". ## ESM @@ -90,10 +93,30 @@ for a security key looks broken. `src/login-indicator.ts` fills that gap, wired up before `loadURL` and only for the in-app window; the default-browser mode gets the browser's own prompts. -- `src/login-overlay.ts` is compiled separately, read off disk, and injected on - every `dom-ready` — the main frame plus sub-frames reached through - `frame-created`, since injecting on the `WebContents` only reaches the top - frame. It re-injects safely; a `window` flag makes it a no-op. +- `src/login-overlay.ts` is compiled separately, read off disk, and injected + into every document the login window loads. It wraps + `navigator.credentials.{get,create}` and draws a toast while a request is + pending. It re-injects safely; a `window` flag makes it a no-op. +- **It has to be running before the page's own scripts are.** A page that asks + for the key as it boots — Google's security-key challenge does — has already + called `navigator.credentials.get()` by `dom-ready`, and a wrapper installed + after the call sees nothing: no toast, no title, no log line, just the silent + window the overlay exists to prevent. So `attachLoginIndicator()` registers + it through the `WebContents` debugger with + `Page.addScriptToEvaluateOnNewDocument`, which runs it at document start in + the page's own world. Three details there all fail silently: + - `Page.enable` first, or the registration resolves and does nothing. + - There has to be a renderer to talk to. On a window that has loaded + nothing the command never resolves — no error, a hung promise — which is + why `attachLoginIndicator()` loads `about:blank` first, is `async`, and + must be awaited before `loadURL`. + - It reaches the top document — across the cross-origin hop to the identity + provider, which changes renderer process — and frames sharing its + process, but not a cross-origin `