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 an account operation could not proceed — either because the * incoming account's stored refresh token could not be exchanged for a live * session, or because the account named is not in the map at all. Carries the * underlying reason as `message` when the server gave one. * * Two independent discriminants, because the three outcomes call for three * different responses and collapsing any two of them misleads the caller: * * - `credentialRejected` — "the server refused this credential": expired, * revoked, reuse-detected, invalidated by a password change or a remote * sign-out-all. Only this one 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. * - `accountNotFound` — "there is no such account on this device". A caller * bug or a stale id (a switcher rendered from a snapshot the map has since * moved past), not a credential problem: nothing was attempted, nothing was * marked, and re-authenticating would not help. Reported as a typed error * rather than a bare `Error` so a caller can tell it apart without matching * on message text it does not own. * - both `false` — the exchange never got an answer, or the rotated successor * could not be persisted. Worth a retry. * * They are never both `true`: an account that is not in the map has no * credential to have refused. */ export declare class AccountTransitionError extends Error { readonly credentialRejected: boolean; /** * `true` when the operation named an account that is not in the stored * account map. Additive — every pre-existing construction site leaves it * `false`. */ readonly accountNotFound: boolean; constructor(message?: string, credentialRejected?: boolean, accountNotFound?: 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. * * **Naming an account the map does not hold rejects too, with * `accountNotFound`** — and that one marks nothing at all, not even * `signedOut`. There is no entry to mark and no credential to have refused. */ export declare function activateStoredAccount({ dispatch, getState, projectId, userId, refreshToken, }: ActivateStoredAccountArgs): Promise;