/** * The browser half of the OAuth authorization-code flow. * * The server only ever sees a `code` and a `redirectUri` taken from the * request body, so `state`, PKCE and the binding between "this browser started * a login" and "this code came back" are, by construction, the client's job. * In the admin login they had been nobody's job: the authorize URL carried no * `state`, and the return leg accepted any `?code=` as long as a localStorage * marker naming the *provider* was set — a marker written on button click and * removed only on a callback that carried a code, so abandoning the flow left * it set indefinitely. * * Everything here is provider-agnostic on purpose. A button that wants a new * provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE * without deciding anything, which is what stops the next provider shipping * without them. */ export interface OAuthRedirectRequest { /** Provider id, as mounted at `POST /auth/`. */ provider: string; /** The provider's authorization endpoint. */ authorizeUrl: string; clientId: string; scope: string; /** Where the provider sends the browser back. Echoed to the token exchange. */ redirectUri: string; /** Set when the provider implements PKCE. */ pkce?: boolean; /** Extra authorize-endpoint parameters (`response_type`, and so on). */ params?: Record; } /** What the browser remembers between leaving for the provider and coming back. */ export interface PendingOAuthRedirect { provider: string; state: string; codeVerifier?: string; redirectUri: string; startedAt: number; } export type OAuthCallbackResult = /** No OAuth callback in this URL. */ { status: "none"; } /** The provider reported a failure, or the user declined. */ | { status: "error"; error: string; } /** * A code arrived that this browser cannot account for: no pending * authorization, an expired one, or a `state` that does not match. This is * what a login-CSRF / code-injection attempt looks like. */ | { status: "mismatch"; } | { status: "ok"; provider: string; code: string; codeVerifier?: string; redirectUri: string; }; type Storage = Pick; export declare function readPendingOAuthRedirect(storage?: Storage | null): PendingOAuthRedirect | null; export declare function clearPendingOAuthRedirect(storage?: Storage | null): void; /** * Build the authorize URL and the pending record that will validate its * callback. Split out from {@link startOAuthRedirect} so it can be tested * without navigating. */ export declare function buildOAuthAuthorization(request: OAuthRedirectRequest, now?: number): Promise<{ url: string; pending: PendingOAuthRedirect; }>; /** Remember the authorization and send the browser to the provider. */ export declare function startOAuthRedirect(request: OAuthRedirectRequest, storage?: Storage | null): Promise; /** * Interpret a return from the provider. * * Clears the pending record on **every** path that saw a callback — including * `?error=` and a state mismatch — so a declined or abandoned consent screen * cannot leave the browser primed to accept somebody else's code later. */ export declare function consumeOAuthCallback(search: string, storage?: Storage | null, now?: number): OAuthCallbackResult; export {};