Files
WRNexusJS/packages/oauth
2026-07-15 08:19:54 +05:30
..
2026-07-12 15:55:18 +05:30
2026-07-12 15:55:18 +05:30
2026-07-15 08:19:54 +05:30
2026-07-12 15:55:18 +05:30

@wrnexus/oauth

Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.

Part of the WrNexus 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.

Installation

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 fetch and WebCrypto (crypto.getRandomValues, crypto.subtle.digest) — no other runtime dependencies.
  • The flow is stateless by design: you are responsible for storing state and verifier between startAuth and completeAuth (session or signed cookie).
  • Pairs with @wrnexus/core — feed the normalized OAuthProfile into logIn to establish a session.