diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a68653..55713aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,6 +48,10 @@ jobs: run: | npm run lint + - 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 `