Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Browser SDK

The @aero2/sdk/browser subpath gives you everything you need to integrate Aero2 into a single-page app: PKCE generation, redirect login, callback handling, silent token refresh, and pluggable storage.

Works in modern browsers (Chrome, Safari, Firefox, Edge), and anywhere fetch and Web Crypto are available — that means it also runs in Cloudflare Workers and Node 18+ with no polyfills.

Install

npm install @aero2/sdk

30-second quickstart

import { createBrowserClient } from "@aero2/sdk/browser";
 
const client = createBrowserClient({
  issuer: "https://acme.aero2.dev",
  clientId: "my-spa",
  redirectUri: "https://app.example.com/callback",
});
 
// On a "Sign in" button click
button.addEventListener("click", () => client.loginWithRedirect());
 
// On the /callback route
const { tokens, appState } = await client.handleRedirectCallback();
 
// Anywhere you need the access token
const token = await client.getAccessToken();
fetch("/api/orders", {
  headers: { Authorization: `Bearer ${token}` },
});
 
// On sign-out
await client.logout({ returnTo: "/" });

How the flow works

  1. loginWithRedirect generates a fresh PKCE verifier/challenge pair and a CSRF state nonce, stores the verifier in storage, then redirects to the IdP's authorize endpoint with the challenge.
  2. The IdP authenticates the user and redirects back to your redirectUri with ?code=...&state=....
  3. handleRedirectCallback validates the state, exchanges the code + verifier for an access token + refresh token (no client_secret needed — that's the point of PKCE), and persists the session.
  4. getAccessToken returns the cached token; if it's within the refresh-skew window of expiring, it silently refreshes using the refresh token. Concurrent callers are de-duplicated to a single refresh.
  5. logout clears local state and (optionally) navigates to a post-logout URL.

Storage adapters

The SDK uses pluggable storage so you can pick the right tradeoff for your app.

import {
  createBrowserClient,
  localStorageAdapter,
  memoryStorage,
  sessionStorageAdapter,
} from "@aero2/sdk/browser";
 
const client = createBrowserClient({
  issuer: "https://acme.aero2.dev",
  clientId: "my-spa",
  redirectUri: "https://app.example.com/callback",
 
  // Default: localStorage. Persists across page loads.
  storage: localStorageAdapter(),
 
  // sessionStorage: cleared on tab close. Stricter, no cross-tab sharing.
  // storage: sessionStorageAdapter(),
 
  // In-memory: lost on page reload. Required if your app must avoid
  // any browser-persistent token storage.
  // storage: memoryStorage(),
});

You can also pass a custom adapter that implements TokenStorage:

import type { TokenStorage } from "@aero2/sdk/browser";
 
const encrypted: TokenStorage = {
  getItem: async (k) => decrypt(localStorage.getItem(k)),
  setItem: async (k, v) => localStorage.setItem(k, encrypt(v)),
  removeItem: (k) => localStorage.removeItem(k),
};

Round-tripping app state

Pass appState to loginWithRedirect to resume the user's pre-login navigation:

await client.loginWithRedirect({
  appState: { returnTo: "/dashboard/team" },
});
 
// On the callback route:
const { appState } = await client.handleRedirectCallback();
window.history.replaceState({}, "", (appState as { returnTo?: string })?.returnTo ?? "/");

appState is stored alongside the PKCE verifier under a key derived from the OAuth state nonce and consumed atomically — it's CSRF-bound to the same state value that protects the redirect.

Custom scope per call

await client.loginWithRedirect({
  scope: "openid email orders:read",
});

If omitted, uses the scope from createBrowserClient (default: "openid profile email").

Userinfo

const user = await client.getUser<{ sub: string; email: string; name?: string }>();

Calls the /oauth2/userinfo endpoint with the current access token. Cached after first fetch — the cache is cleared on logout().

SSR / Node / Workers

The browser SDK uses globalThis.window and globalThis.localStorage by default but can be steered to other environments by passing windowImpl and a non-DOM storage:

const client = createBrowserClient({
  issuer,
  clientId,
  redirectUri,
  storage: memoryStorage(),
  windowImpl: { location: { href: req.url, search: "", origin: "https://app" } },
  fetchImpl: customFetch,
});

This is useful for Next.js / Remix loaders that want to participate in the same session model.

Errors

import {
  Aero2Error,        // base class
  Aero2NetworkError, // fetch failed
  Aero2OAuthError,   // IdP returned a non-2xx error response
} from "@aero2/sdk/browser";
 
try {
  await client.handleRedirectCallback();
} catch (err) {
  if (err instanceof Aero2OAuthError && err.errorCode === "invalid_grant") {
    // Code/verifier mismatch (replay, expired, wrong app). Rare in practice.
  }
  throw err;
}

If getAccessToken fails to refresh (e.g. the refresh token was revoked), the local session is cleared automatically — the next getAccessToken() returns null, signaling the caller to prompt for re-login.