Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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
wrnexusnpm 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,crypto.subtle.digest) — no other runtime dependencies. - The flow is stateless by design: you are responsible for storing
stateandverifierbetweenstartAuthandcompleteAuth(session or signed cookie). - Pairs with
@wrnexus/core— feed the normalizedOAuthProfileintologInto establish a session. OIDC integrations can combine strict discovery with the rotating JWKS verifier:
import { createRemoteJwks } from "@wrnexus/jwt";
import { discoverOidc, verifyOidcIdToken } from "@wrnexus/oauth";
const metadata = await discoverOidc("https://issuer.example");
const jwks = createRemoteJwks(metadata.jwks_uri);
const claims = await verifyOidcIdToken(idToken, {
issuer: metadata.issuer,
clientId: "client-id",
jwks,
nonce: expectedNonce,
accessToken,
});
Discovery requires an exact normalized issuer and HTTPS endpoints without URL
credentials/fragments. ID-token verification checks the RS256 signature,
expiry/not-before, issuer, audience, required OIDC claims, nonce, multi-audience
azp, optional token age, and optional at_hash binding.