import { AuthFailureReason, AuthResult, AuthState, LoginResult, MfaChallenge, Schema, TokenResult } from '@plitzi/sdk-shared'; /** Why a state changed. Not control flow — it is what makes an auth trace readable, and every transition below * names one, because "it went to guest" is not a diagnosis and "the hint cookie was gone" is. */ export type AuthStateReason = 'bootstrap' | 'restored' | 'granted' | 'renewed' | 'identity' | 'redirect' | 'exchanged' | 'no-provider' | 'skip-auth' | 'hint-missing' | 'no-evidence' | 'signed-out' | 'another-tab' | 'settled' | 'authenticating'; export type AuthEvent = { type: 'state'; state: AuthState; reason?: AuthStateReason; } | { type: 'login'; token?: TokenResult; } | { type: 'logout'; } | { type: 'expired'; reason: AuthFailureReason; }; export type AuthEventListener = (event: AuthEvent) => void; /** What the rendering server already knows about this visitor, handed over so the browser does not re-ask. */ export type AuthBootstrap = { user?: U; accessToken?: string; expiresAt?: number; skipAuth?: boolean; }; export type AuthProviderProps = { /** The credential naming the space this SDK instance renders, forwarded on requests made on the space's behalf — * the exchange is one, since only the space can say which identity provider its credentials may come from. */ spaceKey?: string; tokenStorage?: Schema['settings']['tokenStorage']; sessionHintCookie?: Schema['settings']['sessionHintCookie']; sessionGate?: Schema['settings']['sessionGate']; sessionRevalidateSeconds?: Schema['settings']['sessionRevalidateSeconds']; storageKey?: string; }; /** * Everything about a session that does not depend on how a particular backend is spoken to: what is known, how long * it stays true, when to renew it, and what to do when it stops being true. A subclass supplies only the four * requests, so an auth backend is described rather than reimplemented. * * The design goal is that knowing whether someone is signed in should normally cost nothing. Three sources answer it * without a request — a server-rendered page's own answer, a stored session whose token has not lapsed, and the * session hint cookie — and the backend is asked only when none of them can, or when what they say has gone stale. */ declare abstract class AuthProvider> { abstract readonly name: string; protected state: AuthState; private session; private readonly store; private readonly gate; private readonly hintCookie?; private readonly revalidateSeconds; private readonly listeners; private renewal?; /** * Which session an answer belongs to. Moved on by every change of WHO is signed in — a sign-out, a sign-in, another * tab's — and read around each request that can adopt or end a session. * * An identity asked for before a sign-out and answered after it carried the old cookie, so it came back `ok` and * signed the person straight back in: the router then read a session on the guest's own page and answered "Access * Denied". Under load that is not a corner case — the account screen asks who is there while its button is pressed. */ private epoch; private renewalTimer?; private detachListeners?; /** Set when a request failed to reach the backend, so the next `online` event retries instead of waiting. */ private offline; protected readonly spaceKey: string; constructor({ spaceKey, tokenStorage, sessionHintCookie, sessionGate, sessionRevalidateSeconds, storageKey }?: AuthProviderProps); protected abstract get capabilities(): { renew: boolean; identity: boolean; }; /** The backend's endpoints. Refusals reported from elsewhere are matched against these, so a session only reacts * to the API that issued it. Empty means "unknown", and then every refusal is taken as ours. */ protected get endpoints(): string[]; /** A session, why there is none — or, for an account with a second factor, the challenge its code completes. */ protected abstract requestLogin(params: Record): Promise | MfaChallenge>; protected abstract requestRenewal(refreshToken?: string): Promise>; protected abstract requestIdentity(): Promise>; protected abstract requestLogout(): Promise; /** * Hands the credential this browser just obtained to the rendering server, so it can establish a session of its * own — see `handOffToServer`. Returning undefined, the default, means this provider's grants already come from * the server and there is nothing to hand over. */ protected requestExchange(): Promise | undefined>; /** * A grant waiting in the current URL, for providers whose sign-in is a redirect rather than a request: OAuth and * OIDC (Auth0 among them) send the browser away and bring it back with a code to exchange. That is the freshest * evidence a page can have, so it is consulted before anything stored — and it is the provider's job to clean the * code out of the URL once taken. * * Returning undefined, the default, means this provider does not sign in by redirect. */ protected consumeRedirect(): Promise | undefined>; get user(): U | undefined; get token(): TokenResult | undefined; getState(): AuthState; can(permission: string): boolean; /** * What this browser can be seen to hold, right now, without asking anyone or changing anything. * * `init()` reaches the same conclusion, but it runs in an effect — so the first paint of a client-rendered page * happens before it, and a visitor whose session was sitting in storage the whole time saw the signed-out page * flash past. There is nothing to wait for: the credential is in `localStorage` and the hint is a cookie, both * synchronous. This is the same evidence `decide()` acts on, read a few milliseconds earlier. * * `init` means "cannot say yet" — there is evidence, but confirming it needs a request. Only `authenticated` and * `guest` are claims, and both are ones the browser can make on its own. */ peekState(): AuthState; /** The same read, with the identity it found — so a page can render the visitor, not just know there is one. */ peek(): { state: AuthState; user?: U; token?: TokenResult; }; /** * Decides what this page already knows and what, if anything, it has to ask. Resolves once the answer is good * enough to render with — which under the default gate means as soon as a stored session says so, with the * confirmation happening behind the render rather than in front of it. */ init(bootstrap?: AuthBootstrap): Promise; /** * No path may leave a page waiting. Every branch below either settles the state itself or comes back through here, * so a backend that answers something nobody anticipated costs a visitor a wrong guess about their session — never * a page that renders nothing at all. */ private settle; private decide; /** Drops timers and window listeners. Called when the provider is replaced, so a re-configured space leaves none behind. */ dispose(): void; /** * Resolves to WHY when it does not resolve to a session. * * It used to resolve to `undefined` for every refusal there is, which left the one screen that has to explain * itself — a sign-in form — with a single sentence to cover a wrong password, an address that has never been * confirmed, a blocked account and a backend that could not be reached. Three quarters of the time it was telling * somebody to check credentials that were perfectly correct. */ login(params: Record): Promise; /** * A credential obtained in the browser — by a client-side identity provider, or by a redirect coming back from * one — is unknown to the server that renders the pages. It therefore renders every one of them as a guest while * the browser knows perfectly well who this is, and the page changes under the visitor the moment it hydrates. * * Handing the credential over closes that gap: the server verifies it with the provider and establishes its own * session, cookie and all, so the next server-rendered page already knows. Providers whose grants came from that * same server return undefined here and nothing happens. * * Answers with the reason when it ended the session, so the caller can say which refusal this was rather than * inferring one from the state it is left in. */ private handOffToServer; refresh(): Promise; /** * Confirms the session against the backend. Skipped when the last confirmation is recent enough and the token has * not lapsed, unless forced — which is what a caller does before an action it cannot take back. */ revalidate(force?: boolean): Promise; /** * What an app calls when the backend refused a credential it just used. `expired` is renewable and silently is; * anything terminal ends the session here and now, without waiting for a timer to notice. */ invalidate(reason?: AuthFailureReason): void; /** What a refusal does to the session — and whether it was renewed because of it, so the request may be asked again. */ private recover; logout(): Promise; on(listeners: AuthEventListener | AuthEventListener[]): () => void; protected emit(event: AuthEvent): void; protected setState(state: AuthState, reason?: AuthStateReason): void; /** Single-flight: a timer firing while a 401 is being handled must not rotate the refresh token twice. */ private renew; private performRenewal; private loadIdentity; private handleFailure; /** Takes on what a grant or an identity call returned, and records that the backend confirmed it just now. */ private adopt; /** * Ends the session locally. `reason` distinguishes an expiry the client discovered from a sign-out the person * asked for; both clear the same state, but only the first is something the app may want to react to. * * The cleared entry is written back rather than removed, because "nobody is signed in, confirmed just now" is * worth remembering: it is what keeps a signed-out visitor from paying for the same refused request on every load. */ private endSession; /** Unix seconds this session's access token dies at, from what the backend said or, failing that, from the token. */ private expiresAt; /** * Whether a refusal is one a renewal answers: the access credential expired — or it is MISSING while this browser can * still renew, which is the same thing seen from the other side. A cookie carrying the access token lives exactly as * long as the token, so the browser drops it the moment it expires; a tab that was in the background past that, its * renewal timer frozen, sends nothing at all and is told `missing`. Ending the session there signed people out of a * page they only had to come back to — and a reload, which renews on the server, put them right back in. */ private renewable; private hasLiveToken; private isFresh; /** * Whether this browser shows any sign of holding a session: a stored credential, or the hint cookie the backend * publishes beside its httpOnly one. * * **Nothing is ever asked without it.** A page that calls an API "just in case" gets a 401 for every signed-out * visitor on every load and every tab focus — noise in the logs that says nothing, and a request that could never * have succeeded. Evidence is cheap to produce and the backend decides whether it is any good. * * The hint is read live rather than at boot, so a sign-in that happened in another app on this domain is picked * up by the next revalidation instead of waiting for a reload. */ private hasEvidence; private scheduleRenewal; private onRenewalDue; private attach; /** * Another tab changed the session. Adopting it rather than re-checking is the point: two tabs that each renewed on * their own would rotate the refresh token out from under each other, and the one that lost would sign its user * out over nothing. */ private adoptFromStorage; } export default AuthProvider;