Client SDK
@casteluke/forge-client wraps the "Sign in with Forge" flow described in /docs/oauth into a small, framework-agnostic core plus a React adapter — so you don't have to hand-roll PKCE and token exchange yourself.
Workspace-only for now
This package is private: true and lives at packages/forge-client — not published to npm yet. Consume it as a workspace dependency from another package/app in this monorepo ("@casteluke/forge-client": "*").
Core (framework-agnostic)
Use these directly if you're not on React, or to build a platform adapter of your own (React Native, for instance).
import {
generateState,
generatePkcePair,
resolveForgeEndpoints,
buildAuthorizeUrl,
exchangeCodeForToken,
verifyIdToken,
fetchUserinfo,
} from "@casteluke/forge-client";
const endpoints = resolveForgeEndpoints("https://auth.yourapp.com");
// 1. Kick off sign-in — redirect the user here.
const state = generateState();
const pkce = await generatePkcePair();
// Persist state and pkce.codeVerifier somewhere that survives the redirect.
const authorizeUrl = buildAuthorizeUrl({
authorizeEndpoint: endpoints.authorizeEndpoint,
clientId: "your-client-id",
redirectUri: "https://yourapp.com/callback",
state,
codeChallenge: pkce.codeChallenge,
});
// 2. On the callback (?code=...&state=...):
const tokens = await exchangeCodeForToken({
tokenEndpoint: endpoints.tokenEndpoint,
clientId: "your-client-id",
code: "the-code-from-the-query-string",
redirectUri: "https://yourapp.com/callback",
codeVerifier: pkce.codeVerifier,
});
const claims = await verifyIdToken({
idToken: tokens.id_token,
jwksUri: endpoints.jwksUri,
issuer: "https://auth.yourapp.com",
audience: "your-client-id",
});
const userinfo = await fetchUserinfo(endpoints.userinfoEndpoint, tokens.access_token);React adapter
Owns the whole browser-redirect flow for a public client — no client secret ever touches the browser.
import { ForgeAuthProvider, useForgeAuth } from "@casteluke/forge-client/react";
function App() {
return (
<ForgeAuthProvider
forgeBaseUrl="https://auth.yourapp.com"
clientId="your-client-id"
redirectUri="https://yourapp.com/callback"
>
<Page />
</ForgeAuthProvider>
);
}
function Page() {
const { user, accessToken, loading, error, signIn, signOut } = useForgeAuth();
if (loading) return <p>Loading...</p>;
if (!user) return <button onClick={signIn}>Sign in with Forge</button>;
return (
<div>
<p>Signed in as {user.email}</p>
<button onClick={signOut}>Sign out</button>
</div>
);
}Mount <ForgeAuthProvider> on the same route as redirectUri — it detects the ?code&state landing on that page, completes the exchange, and strips those query params once done.
signOut() is local-only
It clears in-memory state but doesn't call /oauth2/revoke — wire that call in yourself if you need the access token actually invalidated server-side, not just forgotten by your app.
React Native
Not implemented yet — deferred so the core above could ship first and be shaped by real usage before committing to an RN-specific API. The core works as-is in React Native with a getRandomValues polyfill (react-native-get-random-values) and, depending on your RN/Hermes version, a btoa polyfill. The React adapter's window.location/sessionStorage calls won't work as-is — React Native needs its own adapter using deep links (Linking) instead.