/** * Overworld portal: the client side of app-scoped tokens. * * Two distinct credential kinds replace the old single app-agnostic token: * - an identity SESSION token (from `client.auth.login` / `register`) that * talks to the Management API and is the ONLY thing that can mint app * tokens. Never send it to a game stack. * - short-lived, app-scoped GAMEPLAY tokens, one per game, used against that * game's Game API + realtime surface. * * Typical wiring is two `CrowdyClient`s sharing nothing but the same Management * URL: an "Overworld"/identity client holding the session token, and a per-game * client whose token store holds that game's app token. * * Browser handoff (cross-origin) is an OAuth2 Authorization-Code + PKCE flow: * 1. game origin: `beginEntry()` -> redirect the player to the Overworld * `/authorize` page carrying a PKCE challenge + the game's redirect URI. * 2. Overworld origin (holds the session): `handleAuthorizeRequest()` -> * mints a one-time code and redirects back to the game. * 3. game origin: `completeEntry()` -> exchanges the code (+ the PKCE verifier * it kept locally) for an app token and stores it. * * Native / same-origin callers can skip the redirect dance and call * `mintAppToken(appId)` directly with a session token. */ import type { GraphQLClient } from '../client.js'; import type { SessionStore } from '../session.js'; export interface AppTokenResponse { /** Opaque app-scoped gameplay token. Send to the app's Game API as a Bearer. */ token: string; gameTokenId: string; /** The app this token is confined to (decimal string). */ appId: string; /** ISO-8601 UTC expiry. Refresh (same app) or re-portal before this. */ expiresAt: string; /** Base HTTPS URL of the Game API serving this app (null if unrouted). */ gameApiUrl: string | null; /** WebSocket URL of the Game API serving this app. */ gameApiWsUrl: string | null; /** * Stable entry origin that reaches SOME healthy instance, whatever has failed. * * WHY THIS IS NOT `gameApiUrl`. The published origin resolves to EVERY datacenter's * load balancer, while `gameApiUrl` is the ONE datacenter holding this app's shards. * So `gameApiUrl` is where the gameplay goes and this is the way back: it is the only * address that survives losing a datacenter. * * It matters most for a client holding only an app token, which is the normal state * after a portal entry. Such a client CANNOT re-mint — minting needs the identity * session it does not have — so without this it has nothing left to ask when its * endpoint stops answering. Pass it as `realtime.discoveryUrl` and the SDK builds * re-discovery from it. * * Null against a Game API older than the datacenter rebuild, which did not return it. */ discoveryUrl: string | null; /** Browser launch URL for this app, if configured. */ launchUrl: string | null; } export interface PortalAuthorizationCode { code: string; redirectUri: string; expiresAt: string; } /** Persists the PKCE verifier across the cross-origin redirect round-trip. */ export interface PkceStore { get(state: string): string | null | Promise; set(state: string, verifier: string): void | Promise; remove(state: string): void | Promise; } /** Default PKCE store backed by sessionStorage; no-op when unavailable (SSR). */ export declare class BrowserSessionPkceStore implements PkceStore { private readonly prefix; constructor(prefix?: string); private ss; get(state: string): string | null; set(state: string, verifier: string): void; remove(state: string): void; } export interface PortalConsentState { appId: string; appName: string | null; /** True for first-party/trusted apps (consent always skipped). */ trusted: boolean; alreadyGranted: boolean; /** True if the Overworld must show a consent screen before minting a code. */ consentRequired: boolean; } export interface AppAuthorizationGrant { grantId: string; appId: string; appName: string | null; scopes: string[]; status: string; grantedAt: string; revokedAt: string | null; } /** Thrown by {@link PortalAPI.handleAuthorizeRequest} when the user must consent. */ export declare class PortalConsentRequiredError extends Error { readonly appId: string; readonly appName: string | null; constructor(appId: string, appName: string | null); } export interface BeginEntryParams { /** Target app id (decimal string). */ appId: string; /** The Overworld identity origin's authorize page, e.g. `https://overworld.example.com/authorize`. */ authorizeUrl: string; /** Where the Overworld should send the player back (this game's callback). */ redirectUri: string; /** Optional CSRF/correlation state; one is generated if omitted. */ state?: string; } export declare class PortalAPI { private readonly api; private readonly session; private readonly pkceStore; constructor(api: GraphQLClient, session: SessionStore, pkceStore?: PkceStore); /** * Native/direct mint: exchange the caller's identity session token for an * app-scoped gameplay token. Returns the token; it is NOT stored on this * client (build a per-game client with it). Free/open apps auto-grant access; * paid apps require an existing entitlement. */ mintAppToken(appId: string): Promise; /** * Overworld/identity side: mint a one-time authorization code for a target * app, bound to the destination game's PKCE challenge + redirect URI. * Requires the identity session token. */ createAuthorizationCode(params: { appId: string; codeChallenge: string; codeChallengeMethod?: string; redirectUri: string; }): Promise; /** * Destination-game side: exchange a one-time code (+ PKCE verifier) for an app * token and store it on this client's session so subsequent Game API calls are * authenticated. Public — no session token required. */ exchangeCode(code: string, codeVerifier?: string): Promise; /** * Same-app refresh: rotate the current app token for a fresh one (extended * TTL) and store it. Call before expiry to keep playing without bouncing * through the Overworld. Requires the current app token on this session. */ refresh(): Promise; /** Whether portaling into an app needs a consent prompt (Overworld side). */ getConsent(appId: string): Promise; /** Record the user's consent for an app (call from the consent screen). */ authorizeApp(appId: string, scopes?: string[]): Promise; /** Revoke a prior authorization; also revokes the user's live tokens for it. */ revokeAppAuthorization(appId: string): Promise; /** The user's active app authorizations ("connected apps"). */ myAuthorizedApps(): Promise; /** Register/update an app's portal client settings (requires manage_apps). */ setAppClientSettings(input: { appId: string; redirectUris?: string[]; clientType?: string; launchUrl?: string; }): Promise; /** * Destination-game side, step 1: generate a PKCE pair, persist the verifier, * and return the Overworld authorize URL to navigate to. The caller does * `window.location.assign(url)`. */ beginEntry(params: BeginEntryParams): Promise; /** * Overworld/identity side: handle an incoming `/authorize` request. Reads the * game's params from the URL, mints a code with the session token, and returns * the URL to redirect the player back to (carrying `code` + `state`). */ handleAuthorizeRequest(search?: string, options?: { grantConsent?: boolean; scopes?: string[]; }): Promise; /** * Destination-game side, step 3: read `code` + `state` from the callback URL, * load the stored verifier, exchange for an app token, and store it. Returns * null when there is no `code` param (so it's safe to call unconditionally on * boot). */ completeEntry(search?: string): Promise; } //# sourceMappingURL=portal.d.ts.map