import type { AppDispatch } from "../../store/types"; import { type GetSublayState } from "../push/mintAccountAccessToken"; /** * The account-transition core. * * One sequence — validate → tear down → select → install — shared by every path * that makes a stored account the active one. * * **A plain function, deliberately not a hook.** The same sequence is needed * from `useSwitchAccount` (a hook), from `oauthCore` (a plain module), and from * the thunk bodies in `authThunks` — neither of the last two can call a hook, * so a `useAccountTransition` would have left them re-implementing it, which is * how the unwrap bug came to exist in six places at once. * * ───────────────────────────────────────────────────────────────────────────── * VALIDATE BEFORE COMMIT — why the order is what it is * ───────────────────────────────────────────────────────────────────────────── * This used to run teardown FIRST and only then discover whether the incoming * account's stored credential still worked. Because teardown is destructive, * switching to an account whose refresh token had died signed the user out of * the account they were happily using — and the rollback could only put the * *selection* back, never the session, because by then the outgoing account's * tokens were already gone. * * The credential is now proven out of band first, through * `leaseAccountSession`, which touches nothing the live session depends on. * Only once it answers does anything get torn down. A failure is therefore a * complete no-op against the current session: the call rejects, the user stays * exactly where they were, and the dead account is marked `needsReauth` so a * switcher can show it as needing a sign-in. * * **The LEASE, specifically — not the plain `mintAccountAccessToken`.** That is * the non-holding variant, and using it here would release the single flight * the instant the exchange settled, reopening the window this design exists to * close: something else could rotate again before the install landed, leaving * the live session holding a revoked token. * * Two things can be that "something else". A second transition into the same * account — `activateStoredAccount` is exported and has no re-entrancy guard of * its own, while `useSwitchAccount`'s in-progress flag is per-hook-instance * state set inside the async callback, so a double tap or two mounted switchers * both get through. And the per-account push toggle, which exchanges a stored * credential whenever the account it targets is not the active one. Push * reconciliation used to be a third; it no longer exchanges anything. See * `leaseAccountSession`. * * **The rotation count is unchanged.** The old order refreshed after the swap; * this one refreshes before it. One exchange either way — the validate step IS * the session-establishing exchange, not an extra probe. If this ever grows a * second exchange, that is a bug: the refresh endpoint rotates, and presenting * a revoked token destroys the account's whole token family. * * **Teardown still sits immediately before the install.** It exists to stop one * account's cached data rendering under another's name, and the gap between * them is where that can happen. Validating first does not widen that gap — * steps 2 and 3 below run back to back with no `await` between them. */ export declare const ACCOUNT_TRANSITION_FAILED_MESSAGE = "Could not restore the session for this account. Please sign in again."; /** * Thrown when the incoming account's stored refresh token could not be * exchanged for a live session. Carries the underlying reason as `message` when * the server gave one. * * `credentialRejected` distinguishes "the server refused this credential" — * expired, revoked, reuse-detected, invalidated by a password change or a * remote sign-out-all — from "we could not reach the server" or "the rotation * could not be persisted". Only the first means the account needs a re-auth; * treating a flaky network as a dead account would tell users to sign in again * every time they lost signal. */ export declare class AccountTransitionError extends Error { readonly credentialRejected: boolean; constructor(message?: string, credentialRejected?: boolean); } export interface ActivateStoredAccountArgs { dispatch: AppDispatch; /** * The store's `getState`. Required: the validate step reads the target's * stored entry and has to write the rotated successor back through the same * persist path every other rotation goes through, and neither is reachable * from `dispatch` alone. */ getState: GetSublayState; projectId: string; /** The account being switched INTO. Must already be in the accounts map. */ userId: string; /** * That account's stored refresh token, as the caller read it. * * A consistency check, not the token that gets spent: the exchange reads the * entry from the store, because the entry is what the rotated successor has * to be written back into. An empty or missing value here fails the call * before any network request, which is the corrupt-map case (an interrupted * write, a hand-composed map) that used to report success with no session. */ refreshToken: string; /** * The account that was active before this call. Retained for source * compatibility and for callers that log it; with validate-before-commit * there is no longer a rollback that needs it, because a failure never * changes the selection in the first place. */ previousActiveAccountId?: string | null; } /** * Makes `userId` the active account and establishes its session. * * Resolves with the fresh access token. * * **Rejects** — with an `AccountTransitionError` — when the target's stored * credential cannot be exchanged for a session, and in that case **nothing has * been touched**: whatever session was live before the call is still live, with * its tokens, its user and its caches intact. The rejection is the only thing * that happened, plus a `needsReauth` marker on the target entry when the * server was the one that refused. * * The target's entry always survives a failure — it is the affordance an app * needs to prompt a re-auth for that account. */ export declare function activateStoredAccount({ dispatch, getState, projectId, userId, refreshToken, }: ActivateStoredAccountArgs): Promise;