fix(auth): a Google connect names its tab's account too, and a refused tab says why

- /auth/google/connect is a navigation and carries no headers: the tab names the account it
  was built for in `expect`, checked by the same comparison as the request header
  (built_for_another_account); a mismatch lands on /?account=changed
- the link marker names the OAuth state of its own flow: an older flow coming back no longer
  spends the marker of the connect still open (which then returned as a plain sign-in)
- a 401 account_changed reloads the tab with a one-time flag, and the reloaded tab says it
  now shows the account signed in
- a refused switch to a suspended or not-yet-approved account says so in the signed-in
  app's words, naming the address a request was recorded for
- the prefs merge locks FOR NO KEY UPDATE, so a transaction that only added a row referring
  to the account does not hold the save up; a test pins the lock
- giving up a lost page closes a channel page open over it
- e2e README names the chromium-only specs
This commit is contained in:
2026-10-21 02:05:33 +02:00
parent 94418a087e
commit ef99f7b5c7
19 changed files with 487 additions and 121 deletions
+6 -5
View File
@@ -223,10 +223,11 @@ function reportFailure(err: HttpError, quiet: boolean): void {
if (RETRIABLE_GATEWAY.has(err.status)) {
markConnectivityLost();
} else if (err.status === 401) {
// Session ended server-side (e.g. the account was suspended or deleted mid-visit). Let the app
// shell decide what to do (drop to the login page if we were signed in). Not a modal — but
// Session ended server-side (e.g. the account was suspended or deleted mid-visit), or this tab
// shows another account than the one signed in now. Let the app shell decide what to do (drop to
// the login page, or reload as the account signed in, if we were signed in). Not a modal — but
// handled all the same, so not the caller's to toast either.
onUnauthorized?.();
onUnauthorized?.(err);
reported.add(err);
} else if (quiet) {
/* caller handles the error inline — no global modal */
@@ -256,8 +257,8 @@ export interface ReqConfig {
// page. The handler itself guards against firing when we were never signed in, so it's safe to
// call for every 401. (The bootstrap /api/me probe no longer 401s — it returns 200 with a null
// user when logged out — but other protected endpoints still do.)
let onUnauthorized: (() => void) | null = null;
export function setUnauthorizedHandler(fn: (() => void) | null): void {
let onUnauthorized: ((err: HttpError) => void) | null = null;
export function setUnauthorizedHandler(fn: ((err: HttpError) => void) | null): void {
onUnauthorized = fn;
}