@wrnexus/oauth
OAuth 2.0, PKCE, provider presets, and profile mapping.
Private registry access required
This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:
bun add @wrnexus/oauth@0.2.43Request preview access. Never put registry tokens in source control.
Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.
Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.
Overview
@wrnexus/oauth implements the OAuth 2.0 Authorization Code flow (with PKCE) for server-side sign-in. It ships ready-made provider presets and a defineProvider helper for custom providers, then gives you two flow functions — startAuth (build the redirect) and completeAuth (exchange the code and fetch the user's profile). It has no runtime dependencies: it uses the platform fetch and WebCrypto only. Pairs naturally with @wrnexus/core's logIn to establish a session once you have a normalized profile.
bun add @wrnexus/oauth
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).
API
Providers
Each preset takes ProviderCredentials and returns an OAuthProvider.
interface ProviderCredentials {
clientId: string;
clientSecret: string;
scopes?: string[]; // override the preset's default scopes
}
| Export | Default scopes | Notes |
|---|---|---|
google(creds) | openid, email, profile | Sets access_type: offline for refresh tokens. |
github(creds) | read:user, user:email | Maps name (falls back to login) and avatar_url. |
discord(creds) | identify, email | Builds the avatar CDN URL from the user id + hash. |
defineProvider(config) | — | Pass a full OAuthProvider to define a custom OAuth 2.0 provider. |
An OAuthProvider describes the endpoints, scopes, credentials, optional extra authorize params, and a mapProfile normalizer:
interface OAuthProvider {
name: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
clientId: string;
clientSecret: string;
authorizeParams?: Record<string, string>; // e.g. access_type, prompt
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
Flow
startAuth(provider, options): Promise<StartAuthResult>
Builds the authorize redirect URL with a generated PKCE challenge and CSRF state. Store the returned state and verifier (session/cookie), then 302 the user to url.
interface StartAuthOptions {
redirectUri: string;
state?: string; // reuse a state instead of generating one
params?: Record<string, string>; // extra authorize params, merged last
}
interface StartAuthResult {
url: string; // authorize URL to redirect to
state: string; // CSRF state — verify on callback
verifier: string; // PKCE code verifier — pass to completeAuth
}
completeAuth(provider, options): Promise<{ tokens, profile }>
On the callback: exchanges the authorization code for tokens, then fetches and normalizes the user profile. Convenience wrapper over exchangeCode + fetchProfile.
interface CompleteAuthOptions {
code: string;
redirectUri: string;
verifier?: string; // the PKCE verifier from startAuth
fetch?: typeof fetch; // inject a fetch implementation (tests)
}
Lower-level helpers
| Export | Signature | Purpose |
|---|---|---|
exchangeCode(provider, options) | → Promise<OAuthTokens> | Exchange an authorization code for tokens. |
fetchProfile(provider, tokens, fetch?) | → Promise<OAuthProfile> | Fetch + normalize the user's profile. |
randomToken(bytes?) | → string | Random URL-safe token (default 32 bytes) for state/verifiers. |
Types
interface OAuthTokens {
access_token: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
id_token?: string;
scope?: string;
}
interface OAuthProfile {
id: string;
email?: string;
name?: string;
avatar?: string;
raw: Record<string, unknown>;
}
Usage
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { logIn } from "@wrnexus/core";
const provider = google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
});
const redirectUri = "https://example.com/auth/callback";
// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) {
const { url, state, verifier } = await startAuth(provider, { redirectUri });
// Persist state + verifier in the session, then redirect.
ctx.session.set("oauth_state", state);
ctx.session.set("oauth_verifier", verifier);
return Response.redirect(url, 302);
}
// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) {
if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");
const { profile } = await completeAuth(provider, {
code,
redirectUri,
verifier: ctx.session.get("oauth_verifier"),
});
logIn(ctx, { id: profile.id, email: profile.email });
}
Custom provider with defineProvider:
import { defineProvider, startAuth } from "@wrnexus/oauth";
const gitlab = defineProvider({
name: "gitlab",
authorizeUrl: "https://gitlab.com/oauth/authorize",
tokenUrl: "https://gitlab.com/oauth/token",
userInfoUrl: "https://gitlab.com/api/v4/user",
scopes: ["read_user"],
clientId: process.env.GITLAB_CLIENT_ID!,
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
mapProfile: (raw) => ({
id: String(raw.id),
email: raw.email as string | undefined,
name: raw.name as string | undefined,
avatar: raw.avatar_url as string | undefined,
raw,
}),
});
Requirements / Notes
- Bun-only. Relies on the global
fetchand WebCrypto (crypto.getRandomValues, - The flow is stateless by design: you are responsible for storing
stateand - Pairs with [
@wrnexus/core](../core) — feed the normalizedOAuthProfileinto
crypto.subtle.digest) — no other runtime dependencies.
verifier between startAuth and completeAuth (session or signed cookie).
logIn to establish a session.
Complete TypeScript API
This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.
/**
* @wrnexus/oauth — OAuth 2.0 sign-in with any provider. Ships presets for Google,
* GitHub, and Discord, and `defineProvider` for a custom one. Dependency-free
* (uses `fetch` + WebCrypto for PKCE). Pairs with @wrnexus/core's `logIn`.
*
* const provider = google({ clientId, clientSecret });
* // 1. send the user to the provider:
* const { url, state, verifier } = await startAuth(provider, { redirectUri });
* // (store `state` + `verifier` in the session, then 302 to `url`)
* // 2. on the callback:
* const { profile } = await completeAuth(provider, { code, redirectUri, verifier });
* logIn(ctx, { id: profile.id, email: profile.email });
*/
interface OAuthTokens {
access_token: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
id_token?: string;
scope?: string;
}
interface OAuthProfile {
id: string;
email?: string;
name?: string;
avatar?: string;
raw: Record<string, unknown>;
}
interface OAuthProvider {
name: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
clientId: string;
clientSecret: string;
/** Extra params for the authorize request (e.g. `access_type`, `prompt`). */
authorizeParams?: Record<string, string>;
/** Normalize the provider's raw userinfo into an OAuthProfile. */
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
interface ProviderCredentials {
clientId: string;
clientSecret: string;
scopes?: string[];
}
type FetchLike = typeof fetch;
declare function google(creds: ProviderCredentials): OAuthProvider;
declare function github(creds: ProviderCredentials): OAuthProvider;
declare function discord(creds: ProviderCredentials): OAuthProvider;
/** Define a custom OAuth2 provider. */
declare function defineProvider(config: OAuthProvider): OAuthProvider;
/** A random URL-safe token (for `state` and the PKCE verifier). */
declare function randomToken(bytes?: number): string;
interface StartAuthOptions {
redirectUri: string;
/** Provide to reuse a state (else one is generated). */
state?: string;
/** Extra authorize params (merged over the provider's). */
params?: Record<string, string>;
}
interface StartAuthResult {
/** The full authorize URL to redirect the user to. */
url: string;
/** CSRF state — store it (session/cookie) and verify on callback. */
state: string;
/** PKCE code verifier — store it and pass to `completeAuth`. */
verifier: string;
}
/** Build the authorize redirect (with PKCE + state). */
declare function startAuth(provider: OAuthProvider, options: StartAuthOptions): Promise<StartAuthResult>;
interface CompleteAuthOptions {
code: string;
redirectUri: string;
/** The PKCE verifier from `startAuth`. */
verifier?: string;
/** Inject a fetch implementation (tests). */
fetch?: FetchLike;
}
/** Exchange the authorization code for tokens, then fetch the user profile. */
declare function completeAuth(provider: OAuthProvider, options: CompleteAuthOptions): Promise<{
tokens: OAuthTokens;
profile: OAuthProfile;
}>;
/** Exchange an authorization code for tokens. */
declare function exchangeCode(provider: OAuthProvider, options: CompleteAuthOptions): Promise<OAuthTokens>;
/** Fetch + normalize the user's profile from the provider. */
declare function fetchProfile(provider: OAuthProvider, tokens: OAuthTokens, fetchImpl?: FetchLike): Promise<OAuthProfile>;
export { type CompleteAuthOptions, type OAuthProfile, type OAuthProvider, type OAuthTokens, type ProviderCredentials, type StartAuthOptions, type StartAuthResult, completeAuth, defineProvider, discord, exchangeCode, fetchProfile, github, google, randomToken, startAuth };
Examples
Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.
Typical usage
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { logIn } from "@wrnexus/core";
const provider = google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
});
const redirectUri = "https://example.com/auth/callback";
// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) {
const { url, state, verifier } = await startAuth(provider, { redirectUri });
// Persist state + verifier in the session, then redirect.
ctx.session.set("oauth_state", state);
ctx.session.set("oauth_verifier", verifier);
return Response.redirect(url, 302);
}
// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) {
if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");
const { profile } = await completeAuth(provider, {
code,
redirectUri,
verifier: ctx.session.get("oauth_verifier"),
});
logIn(ctx, { id: profile.id, email: profile.email });
}Custom provider with defineProvider
import { defineProvider, startAuth } from "@wrnexus/oauth";
const gitlab = defineProvider({
name: "gitlab",
authorizeUrl: "https://gitlab.com/oauth/authorize",
tokenUrl: "https://gitlab.com/oauth/token",
userInfoUrl: "https://gitlab.com/api/v4/user",
scopes: ["read_user"],
clientId: process.env.GITLAB_CLIENT_ID!,
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
mapProfile: (raw) => ({
id: String(raw.id),
email: raw.email as string | undefined,
name: raw.name as string | undefined,
avatar: raw.avatar_url as string | undefined,
raw,
}),
});