Files
WRNexusJSDoc/app/pages/packages/oauth.wrn
T

321 lines
19 KiB
Plaintext

page wrnexusoauth {
seo {
title = "@wrnexus/oauth"
description = "OAuth 2.0, PKCE, provider presets, and profile mapping."
}
view {
<div class="docs-shell">
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
<header class="topbar">
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.5.10</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
</header>
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
<main class="portal-main docs-layout">
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/oauth</span></nav><section class="doc-intro"><span class="eyebrow">Security · Package reference</span><h1>@wrnexus/oauth</h1><p>OAuth 2.0, PKCE, provider presets, and profile mapping.</p><div class="doc-meta"><span>v0.5.10</span><span>Private registry</span><span>Security</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/oauth@0.5.10</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><blockquote>Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.</blockquote>
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
<h3 id="overview">Overview</h3>
<p><code>@wrnexus/oauth</code> implements the OAuth 2.0 Authorization Code flow (with PKCE) for server-side sign-in. It ships ready-made provider presets and a <code>defineProvider</code> helper for custom providers, then gives you two flow functions — <code>startAuth</code> (build the redirect) and <code>completeAuth</code> (exchange the code and fetch the user's profile). It has no runtime dependencies: it uses the platform <code>fetch</code> and WebCrypto only. Pairs naturally with <code>@wrnexus/core</code>'s <code>logIn</code> to establish a session once you have a normalized profile.</p>
<pre data-language="bash"><code>bun add @wrnexus/oauth</code></pre>
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
<h3 id="api">API</h3>
<h4 id="providers">Providers</h4>
<p>Each preset takes <code>ProviderCredentials</code> and returns an <code>OAuthProvider</code>.</p>
<pre data-language="ts"><code>interface ProviderCredentials &#123;
clientId: string;
clientSecret: string;
scopes?: string[]; // override the preset's default scopes
&#125;</code></pre>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Default scopes</th><th>Notes</th></tr></thead>
<tbody><tr><td><code>google(creds)</code></td><td><code>openid</code>, <code>email</code>, <code>profile</code></td><td>Sets <code>access_type: offline</code> for refresh tokens.</td></tr><tr><td><code>github(creds)</code></td><td><code>read:user</code>, <code>user:email</code></td><td>Maps <code>name</code> (falls back to <code>login</code>) and <code>avatar_url</code>.</td></tr><tr><td><code>discord(creds)</code></td><td><code>identify</code>, <code>email</code></td><td>Builds the avatar CDN URL from the user id + hash.</td></tr><tr><td><code>defineProvider(config)</code></td><td>—</td><td>Pass a full <code>OAuthProvider</code> to define a custom OAuth 2.0 provider.</td></tr></tbody></table></div>
<p>An <code>OAuthProvider</code> describes the endpoints, scopes, credentials, optional extra authorize params, and a <code>mapProfile</code> normalizer:</p>
<pre data-language="ts"><code>interface OAuthProvider &#123;
name: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
clientId: string;
clientSecret: string;
authorizeParams?: Record&lt;string, string&gt;; // e.g. access_type, prompt
mapProfile: (raw: Record&lt;string, unknown&gt;) =&gt; OAuthProfile;
&#125;</code></pre>
<h4 id="flow">Flow</h4>
<h4 id="startauth-provider-options-promise-startauthresult"><code>startAuth(provider, options): Promise&lt;StartAuthResult&gt;</code></h4>
<p>Builds the authorize redirect URL with a generated PKCE challenge and CSRF <code>state</code>. Store the returned <code>state</code> and <code>verifier</code> (session/cookie), then 302 the user to <code>url</code>.</p>
<pre data-language="ts"><code>interface StartAuthOptions &#123;
redirectUri: string;
state?: string; // reuse a state instead of generating one
params?: Record&lt;string, string&gt;; // extra authorize params, merged last
&#125;
interface StartAuthResult &#123;
url: string; // authorize URL to redirect to
state: string; // CSRF state — verify on callback
verifier: string; // PKCE code verifier — pass to completeAuth
&#125;</code></pre>
<h4 id="completeauth-provider-options-promise-tokens-profile"><code>completeAuth(provider, options): Promise&lt;&#123; tokens, profile &#125;&gt;</code></h4>
<p>On the callback: exchanges the authorization <code>code</code> for tokens, then fetches and normalizes the user profile. Convenience wrapper over <code>exchangeCode</code> + <code>fetchProfile</code>.</p>
<pre data-language="ts"><code>interface CompleteAuthOptions &#123;
code: string;
redirectUri: string;
verifier?: string; // the PKCE verifier from startAuth
fetch?: typeof fetch; // inject a fetch implementation (tests)
&#125;</code></pre>
<h4 id="lower-level-helpers">Lower-level helpers</h4>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
<tbody><tr><td><code>exchangeCode(provider, options)</code></td><td><code>→ Promise&lt;OAuthTokens&gt;</code></td><td>Exchange an authorization code for tokens.</td></tr><tr><td><code>fetchProfile(provider, tokens, fetch?)</code></td><td><code>→ Promise&lt;OAuthProfile&gt;</code></td><td>Fetch + normalize the user's profile.</td></tr><tr><td><code>randomToken(bytes?)</code></td><td><code>→ string</code></td><td>Random URL-safe token (default 32 bytes) for <code>state</code>/verifiers.</td></tr></tbody></table></div>
<h4 id="types">Types</h4>
<pre data-language="ts"><code>interface OAuthTokens &#123;
access_token: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
id_token?: string;
scope?: string;
&#125;
interface OAuthProfile &#123;
id: string;
email?: string;
name?: string;
avatar?: string;
raw: Record&lt;string, unknown&gt;;
&#125;</code></pre>
<h3 id="usage">Usage</h3>
<pre data-language="ts"><code>import &#123; google, startAuth, completeAuth &#125; from &quot;@wrnexus/oauth&quot;;
import &#123; logIn &#125; from &quot;@wrnexus/core&quot;;
const provider = google(&#123;
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
&#125;);
const redirectUri = &quot;https://example.com/auth/callback&quot;;
// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) &#123;
const &#123; url, state, verifier &#125; = await startAuth(provider, &#123; redirectUri &#125;);
// Persist state + verifier in the session, then redirect.
ctx.session.set(&quot;oauth_state&quot;, state);
ctx.session.set(&quot;oauth_verifier&quot;, verifier);
return Response.redirect(url, 302);
&#125;
// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) &#123;
if (state !== ctx.session.get(&quot;oauth_state&quot;)) throw new Error(&quot;bad state&quot;);
const &#123; profile &#125; = await completeAuth(provider, &#123;
code,
redirectUri,
verifier: ctx.session.get(&quot;oauth_verifier&quot;),
&#125;);
logIn(ctx, &#123; id: profile.id, email: profile.email &#125;);
&#125;</code></pre>
<p>Custom provider with <code>defineProvider</code>:</p>
<pre data-language="ts"><code>import &#123; defineProvider, startAuth &#125; from &quot;@wrnexus/oauth&quot;;
const gitlab = defineProvider(&#123;
name: &quot;gitlab&quot;,
authorizeUrl: &quot;https://gitlab.com/oauth/authorize&quot;,
tokenUrl: &quot;https://gitlab.com/oauth/token&quot;,
userInfoUrl: &quot;https://gitlab.com/api/v4/user&quot;,
scopes: [&quot;read_user&quot;],
clientId: process.env.GITLAB_CLIENT_ID!,
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
mapProfile: (raw) =&gt; (&#123;
id: String(raw.id),
email: raw.email as string | undefined,
name: raw.name as string | undefined,
avatar: raw.avatar_url as string | undefined,
raw,
&#125;),
&#125;);</code></pre>
<h3 id="requirements-notes">Requirements / Notes</h3>
<ul>
<li><strong>Bun-only.</strong> Relies on the global <code>fetch</code> and WebCrypto (<code>crypto.getRandomValues</code>,</li>
<p><code>crypto.subtle.digest</code>) — no other runtime dependencies.</p>
<li>The flow is stateless by design: you are responsible for storing <code>state</code> and</li>
<p><code>verifier</code> between <code>startAuth</code> and <code>completeAuth</code> (session or signed cookie).</p>
<li>Pairs with [<code>@wrnexus/core</code>](../core) — feed the normalized <code>OAuthProfile</code> into</li>
<p><code>logIn</code> to establish a session.</p>
</ul></section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>interface OAuthStateRecord &#123;
state: string;
verifier: string;
redirectUri: string;
returnTo?: string;
expiresAt: number;
&#125;
interface OAuthStateStore &#123;
set(record: OAuthStateRecord): Promise&lt;void&gt;;
consume(state: string): Promise&lt;OAuthStateRecord | null&gt;;
&#125;
declare function memoryOAuthStateStore(now?: () =&gt; number): OAuthStateStore;
declare function createOAuthState(store: OAuthStateStore, input: Omit&lt;OAuthStateRecord, &quot;state&quot; | &quot;expiresAt&quot;&gt; &amp; &#123;
ttlMs?: number;
&#125;): Promise&lt;OAuthStateRecord&gt;;
declare function refreshOAuthTokens(provider: OAuthProvider, refreshToken: string, fetchImpl?: typeof fetch): Promise&lt;OAuthTokens&gt;;
interface OidcDiscovery &#123;
issuer: string;
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint?: string;
jwks_uri: string;
revocation_endpoint?: string;
&#125;
declare function discoverOidc(issuer: string, fetchImpl?: typeof fetch): Promise&lt;OidcDiscovery&gt;;
declare function validateOAuthReturnTo(value: string | undefined, origin: string, fallback?: string): string;
/**
* @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(&#123; clientId, clientSecret &#125;);
* // 1. send the user to the provider:
* const &#123; url, state, verifier &#125; = await startAuth(provider, &#123; redirectUri &#125;);
* // (store `state` + `verifier` in the session, then 302 to `url`)
* // 2. on the callback:
* const &#123; profile &#125; = await completeAuth(provider, &#123; code, redirectUri, verifier &#125;);
* logIn(ctx, &#123; id: profile.id, email: profile.email &#125;);
*/
interface OAuthTokens &#123;
access_token: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
id_token?: string;
scope?: string;
&#125;
interface OAuthProfile &#123;
id: string;
email?: string;
name?: string;
avatar?: string;
raw: Record&lt;string, unknown&gt;;
&#125;
interface OAuthProvider &#123;
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&lt;string, string&gt;;
/** Normalize the provider's raw userinfo into an OAuthProfile. */
mapProfile: (raw: Record&lt;string, unknown&gt;) =&gt; OAuthProfile;
&#125;
interface ProviderCredentials &#123;
clientId: string;
clientSecret: string;
scopes?: string[];
&#125;
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 &#123;
redirectUri: string;
/** Provide to reuse a state (else one is generated). */
state?: string;
/** Extra authorize params (merged over the provider's). */
params?: Record&lt;string, string&gt;;
&#125;
interface StartAuthResult &#123;
/** 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;
&#125;
/** Build the authorize redirect (with PKCE + state). */
declare function startAuth(provider: OAuthProvider, options: StartAuthOptions): Promise&lt;StartAuthResult&gt;;
interface CompleteAuthOptions &#123;
code: string;
redirectUri: string;
/** The PKCE verifier from `startAuth`. */
verifier?: string;
/** Inject a fetch implementation (tests). */
fetch?: FetchLike;
&#125;
/** Exchange the authorization code for tokens, then fetch the user profile. */
declare function completeAuth(provider: OAuthProvider, options: CompleteAuthOptions): Promise&lt;&#123;
tokens: OAuthTokens;
profile: OAuthProfile;
&#125;&gt;;
/** Exchange an authorization code for tokens. */
declare function exchangeCode(provider: OAuthProvider, options: CompleteAuthOptions): Promise&lt;OAuthTokens&gt;;
/** Fetch + normalize the user's profile from the provider. */
declare function fetchProfile(provider: OAuthProvider, tokens: OAuthTokens, fetchImpl?: FetchLike): Promise&lt;OAuthProfile&gt;;
export &#123; type CompleteAuthOptions, type OAuthProfile, type OAuthProvider, type OAuthStateRecord, type OAuthStateStore, type OAuthTokens, type OidcDiscovery, type ProviderCredentials, type StartAuthOptions, type StartAuthResult, completeAuth, createOAuthState, defineProvider, discord, discoverOidc, exchangeCode, fetchProfile, github, google, memoryOAuthStateStore, randomToken, refreshOAuthTokens, startAuth, validateOAuthReturnTo &#125;;
</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>Typical usage</h3><pre data-language="ts"><code>import &#123; google, startAuth, completeAuth &#125; from &quot;@wrnexus/oauth&quot;;
import &#123; logIn &#125; from &quot;@wrnexus/core&quot;;
const provider = google(&#123;
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
&#125;);
const redirectUri = &quot;https://example.com/auth/callback&quot;;
// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) &#123;
const &#123; url, state, verifier &#125; = await startAuth(provider, &#123; redirectUri &#125;);
// Persist state + verifier in the session, then redirect.
ctx.session.set(&quot;oauth_state&quot;, state);
ctx.session.set(&quot;oauth_verifier&quot;, verifier);
return Response.redirect(url, 302);
&#125;
// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) &#123;
if (state !== ctx.session.get(&quot;oauth_state&quot;)) throw new Error(&quot;bad state&quot;);
const &#123; profile &#125; = await completeAuth(provider, &#123;
code,
redirectUri,
verifier: ctx.session.get(&quot;oauth_verifier&quot;),
&#125;);
logIn(ctx, &#123; id: profile.id, email: profile.email &#125;);
&#125;</code></pre></article><article class="example-card"><h3>Custom provider with defineProvider</h3><pre data-language="ts"><code>import &#123; defineProvider, startAuth &#125; from &quot;@wrnexus/oauth&quot;;
const gitlab = defineProvider(&#123;
name: &quot;gitlab&quot;,
authorizeUrl: &quot;https://gitlab.com/oauth/authorize&quot;,
tokenUrl: &quot;https://gitlab.com/oauth/token&quot;,
userInfoUrl: &quot;https://gitlab.com/api/v4/user&quot;,
scopes: [&quot;read_user&quot;],
clientId: process.env.GITLAB_CLIENT_ID!,
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
mapProfile: (raw) =&gt; (&#123;
id: String(raw.id),
email: raw.email as string | undefined,
name: raw.name as string | undefined,
avatar: raw.avatar_url as string | undefined,
raw,
&#125;),
&#125;);</code></pre></article></div></section></article>
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#providers">Providers</a><a class="toc-level-4" href="#flow">Flow</a><a class="toc-level-4" href="#startauth-provider-options-promise-startauthresult">startAuth(provider, options): Promise&lt;StartAuthResult&gt;</a><a class="toc-level-4" href="#completeauth-provider-options-promise-tokens-profile">completeAuth(provider, options): Promise&lt;&#123; tokens, profile &#125;&gt;</a><a class="toc-level-4" href="#lower-level-helpers">Lower-level helpers</a><a class="toc-level-4" href="#types">Types</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
</main>
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.5.10</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
<BackToTop />
</div>
}
}