runExclusive must serialize all token operations. If you run multiple server instances, this must use a distributed lock (e.g., Redis, database row lock). Single-use refresh tokens will fail if two instances try to refresh concurrently.
A minimal in-memory implementation for development:
Access tokens expire after 24 hours. The SDK refreshes them automatically when credentialHeaders() detects an expired token. Refresh happens inside runExclusive to prevent concurrent rotation of single-use refresh tokens.
You can tune refresh timing:
Code
new OAuthAuth({ // ... refreshSkewMs: 60_000, // refresh 60s before expiry (default)});
Revocation
Revoke tokens explicitly when a user disconnects your app:
Code
import { HttpTransport } from "gemini-markets/server";const transport = new HttpTransport({ env: "sandbox", auth });await auth.revoke(transport);// tokens are cleared from the store after successful server-side revocation
Browser OAuth (PKCE)
Browser apps cannot hold a client secret. Use BrowserOAuthAuth which enforces public-client PKCE at the type level — you cannot accidentally pass a clientSecret.
Code
// login.ts — the page that starts the OAuth flowimport { createClient, BrowserOAuthAuth } from "gemini-markets/browser";const auth = new BrowserOAuthAuth({ client: { type: "public", clientId: "your-client-id", redirectUri: "http://localhost:3000/callback", }, env: "sandbox", tokenStore: myBrowserTokenStore,});// Step 1: Start authorization (generates PKCE challenge automatically)const { url, transaction } = await auth.beginAuthorization(["orders:read"]);// Step 2: Persist the transaction — the redirect will lose in-memory statesessionStorage.setItem("gemini_oauth_tx", JSON.stringify(transaction));// Step 3: Redirect the userwindow.location.href = url;
Code
// callback.ts — the page Gemini redirects back toconst raw = sessionStorage.getItem("gemini_oauth_tx");if (!raw) throw new Error("OAuth transaction not found — was the flow started?");const transaction = JSON.parse(raw);sessionStorage.removeItem("gemini_oauth_tx");// Step 4: Complete the exchange with the saved transactionawait auth.completeAuthorization(window.location.href, transaction);// Step 5: Use the clientconst client = createClient({ auth });const events = await client.predictions.listEvents({ status: ["active"] });
The PKCE code challenge and verifier are generated automatically using Web Crypto (crypto.subtle). The verifier is sent during code exchange — no secret ever leaves the browser.
Scopes
Scopes control what the OAuth token can access. Request only what your app needs: