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

323 lines
21 KiB
Plaintext

page wrnexusjwt {
seo {
title = "@wrnexus/jwt"
description = "HS256 JWT signing, verification, and bearer authentication."
}
view {
<div class="docs-shell">
<a href="#main" class="skip-link">Skip to content</a>
<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.8.7</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/jwt</span></nav><section class="doc-intro"><span class="eyebrow">Security · Package reference</span><h1>@wrnexus/jwt</h1><p>HS256 JWT signing, verification, and bearer authentication.</p><div class="doc-meta"><span>v0.8.7</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/jwt@0.8.7</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 JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WRNexusJS.</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/jwt</code> signs and verifies stateless JSON Web Tokens using the <strong>HS256</strong> (HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and verification are implemented directly on the standard <strong>Web Crypto</strong> API (<code>crypto.subtle</code>), which Bun provides natively. It runs server-side and pairs with the session-based auth in <code>@wrnexus/core</code>, giving you a stateless option for API and mobile clients. Reach for it when you need bearer-token auth rather than cookie sessions.</p>
<pre data-language="bash"><code>bun add @wrnexus/jwt</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>
<p>Single entry point (<code>@wrnexus/jwt</code>). All functions are async and return Promises.</p>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Kind</th><th>Description</th></tr></thead>
<tbody><tr><td><code>signJwt(payload, secret, options?)</code></td><td>function</td><td>Sign claims into an HS256 token string.</td></tr><tr><td><code>verifyJwt&lt;T&gt;(token, secret, options?)</code></td><td>function</td><td>Verify a token and return its claims, or throw.</td></tr><tr><td><code>jwtAuth(options)</code></td><td>function</td><td>Middleware that verifies a bearer JWT and sets <code>ctx.user</code>.</td></tr><tr><td><code>JwtError</code></td><td>class</td><td>Error thrown on any signature/payload/expiry failure.</td></tr><tr><td><code>JwtClaims</code></td><td>interface</td><td>Claims shape (<code>sub</code>, <code>iat</code>, <code>exp</code>, <code>nbf</code>, plus arbitrary keys).</td></tr><tr><td><code>SignOptions</code></td><td>interface</td><td>Options for <code>signJwt</code>.</td></tr><tr><td><code>JwtAuthOptions</code></td><td>interface</td><td>Options for <code>jwtAuth</code>.</td></tr></tbody></table></div>
<h4 id="signjwt-payload-secret-options"><code>signJwt(payload, secret, options?)</code></h4>
<pre data-language="ts"><code>function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise&lt;string&gt;;</code></pre>
<p>Signs <code>payload</code> with <code>secret</code> using HS256 and returns the encoded token (<code>header.body.signature</code>). An <code>iat</code> (issued-at) claim is always added.</p>
<p><code>SignOptions</code>:</p>
<ul>
<li><code>expiresIn?: number</code> — seconds until expiry; sets the <code>exp</code> claim.</li>
<li><code>now?: number</code> — override the issued-at time (seconds), useful for testing.</li>
</ul>
<h4 id="verifyjwt-t-token-secret-options"><code>verifyJwt&lt;T&gt;(token, secret, options?)</code></h4>
<pre data-language="ts"><code>function verifyJwt&lt;T extends JwtClaims = JwtClaims&gt;(
token: string,
secret: string,
options?: &#123; now?: number &#125;,
): Promise&lt;T&gt;;</code></pre>
<p>Verifies the HS256 signature and returns the decoded claims typed as <code>T</code>. Throws <code>JwtError</code> when the token is malformed, the signature is invalid, the payload is not valid JSON, the token is expired (<code>exp</code>), or not yet valid (<code>nbf</code>). Pass <code>now</code> (seconds) to override the reference time for the <code>exp</code>/<code>nbf</code> checks.</p>
<h4 id="jwtauth-options"><code>jwtAuth(options)</code></h4>
<pre data-language="ts"><code>function jwtAuth(options: JwtAuthOptions): Middleware;</code></pre>
<p>Returns a WRNexusJS <code>Middleware</code> that reads a token, verifies it, and assigns the claims to <code>ctx.user</code>.</p>
<p><code>JwtAuthOptions</code>:</p>
<ul>
<li><code>secret: string</code> — the HMAC secret used to verify tokens.</li>
<li><code>getToken?: (ctx: Context) =&gt; string | undefined</code> — how to extract the token.</li>
<p>Defaults to reading <code>Authorization: Bearer &lt;token&gt;</code>.</p>
<li><code>required?: boolean</code> — when <code>true</code> (default), a missing or invalid token</li>
<p>responds with <code>401 &#123; ok: false, error: &quot;Unauthorized&quot; &#125;</code>. When <code>false</code>, requests pass through and <code>ctx.user</code> is only set if a valid token is present.</p>
</ul>
<h3 id="usage">Usage</h3>
<pre data-language="ts"><code>import &#123; signJwt, verifyJwt, jwtAuth, JwtError &#125; from &quot;@wrnexus/jwt&quot;;
const secret = process.env.JWT_SECRET!;
// Sign a token that expires in one hour
const token = await signJwt(&#123; sub: user.id, role: &quot;admin&quot; &#125;, secret, &#123;
expiresIn: 3600,
&#125;);
// Verify it later
try &#123;
const claims = await verifyJwt&lt;&#123; sub: string; role: string &#125;&gt;(token, secret);
console.log(claims.sub, claims.role);
&#125; catch (err) &#123;
if (err instanceof JwtError) &#123;
// invalid signature, expired, malformed, etc.
&#125;
&#125;</code></pre>
<p>Protecting routes with the middleware:</p>
<pre data-language="ts"><code>import &#123; jwtAuth &#125; from &quot;@wrnexus/jwt&quot;;
// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth(&#123; secret: process.env.JWT_SECRET! &#125;));
// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth(&#123; secret: process.env.JWT_SECRET!, required: false &#125;));</code></pre>
<h3 id="requirements-notes">Requirements / Notes</h3>
<ul>
<li><strong>Bun-only.</strong> Uses the standard Web Crypto API (<code>crypto.subtle.importKey</code>,</li>
<p><code>sign</code>, <code>verify</code>) plus <code>btoa</code>/<code>atob</code> and <code>TextEncoder</code>/<code>TextDecoder</code> — all provided by Bun. No third-party crypto dependency.</p>
<li><strong>Algorithm:</strong> HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)</li>
<p>are not supported.</p>
<li>Integrates with [<code>@wrnexus/core</code>](../core) for <code>Context</code>, <code>Middleware</code>, and</li>
<p><code>ctx.user</code>; it complements the framework's cookie/session auth with a stateless bearer-token flow for API and mobile clients.</p>
</ul>
<h3 id="access-refresh-scope-and-cookie-helpers">Access, refresh, scope, and cookie helpers</h3>
<pre data-language="ts"><code>import &#123;
createAccessToken,
createRefreshToken,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
requireScopes,
jwtCookie,
&#125; from &quot;@wrnexus/jwt&quot;;</code></pre>
<p>The helpers add explicit <code>type: &quot;access&quot; | &quot;refresh&quot;</code> claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. <code>__Host-</code> cookies are rejected unless they use <code>Path=/</code> and <code>Secure</code>; <code>SameSite=None</code> is rejected without <code>Secure</code>.</p>
<h3 id="0-8-helper-kit">0.8 helper kit</h3>
<pre data-language="ts"><code>import &#123;
createTokenPair,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
readJwtCookie,
jwtCookie,
clearJwtCookie,
requireScopes,
&#125; from &quot;@wrnexus/jwt&quot;;
const pair = await createTokenPair(user.id, &#123;
accessSecret: process.env.JWT_ACCESS_SECRET!,
refreshSecret: process.env.JWT_REFRESH_SECRET!,
scopes: [&quot;profile:read&quot;],
family: sessionFamily,
&#125;);</code></pre>
<p>The helper kit validates <code>__Host-</code> cookie invariants, cookie names and paths, <code>SameSite=None</code> security, typed access/refresh token types, scope requirements, and no-store token responses. In addition to local HS256 secrets/keyrings, the package verifies standards-based RS256 tokens through bounded remote JWKS caches:</p>
<pre data-language="ts"><code>import &#123; createRemoteJwks, verifyJwtWithJwks &#125; from &quot;@wrnexus/jwt&quot;;
const jwks = createRemoteJwks(&quot;https://issuer.example/.well-known/jwks.json&quot;);
const claims = await verifyJwtWithJwks(token, jwks, &#123;
issuer: &quot;https://issuer.example&quot;,
audience: &quot;my-api&quot;,
maxAge: 300,
&#125;);</code></pre>
<p>JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public keys, and force an immediate refresh for an unknown <code>kid</code> so issuer rotation does not wait for cache expiry. Never use decoded-but-unverified claims for an authorization decision.</p></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>import &#123; Context, Middleware &#125; from '@wrnexus/core';
interface JwtKey &#123;
id: string;
secret: string;
active?: boolean;
&#125;
interface JwtKeyring &#123;
active(): JwtKey;
resolve(id: string): JwtKey | undefined;
keys(): JwtKey[];
&#125;
declare function decodeJwt(token: string): &#123;
header: Record&lt;string, unknown&gt;;
claims: JwtClaims;
&#125;;
declare function createJwtKeyring(keys: JwtKey[]): JwtKeyring;
declare function signWithKeyring(claims: JwtClaims, keyring: JwtKeyring, options?: SignOptions): Promise&lt;string&gt;;
declare function verifyWithKeyring&lt;T extends JwtClaims = JwtClaims&gt;(token: string, keyring: JwtKeyring, options?: VerifyOptions): Promise&lt;T&gt;;
interface AccessTokenClaims extends JwtClaims &#123;
sub: string;
type: &quot;access&quot;;
scopes?: string[];
&#125;
interface RefreshTokenClaims extends JwtClaims &#123;
sub: string;
type: &quot;refresh&quot;;
family?: string;
&#125;
declare function extractBearerToken(value: Headers | Request | Context | string | null | undefined): string | undefined;
declare function tryVerifyJwt&lt;T extends JwtClaims = JwtClaims&gt;(token: string | undefined, secret: string, options?: VerifyOptions): Promise&lt;T | null&gt;;
declare function assertJwtClaims&lt;T extends JwtClaims&gt;(claims: T, requirements?: &#123;
subject?: boolean;
type?: string;
required?: string[];
&#125;): T;
declare function tokenScopes(claims: JwtClaims): string[];
declare function hasScopes(claims: JwtClaims, required: readonly string[], mode?: &quot;all&quot; | &quot;any&quot;): boolean;
declare function requireScopes(required: readonly string[], mode?: &quot;all&quot; | &quot;any&quot;): Middleware;
declare function createAccessToken(subject: string, secret: string, options?: Omit&lt;SignOptions, &quot;expiresIn&quot;&gt; &amp; &#123;
expiresIn?: number;
scopes?: string[];
claims?: JwtClaims;
&#125;): Promise&lt;string&gt;;
declare function createRefreshToken(subject: string, secret: string, options?: Omit&lt;SignOptions, &quot;expiresIn&quot;&gt; &amp; &#123;
expiresIn?: number;
family?: string;
claims?: JwtClaims;
&#125;): Promise&lt;string&gt;;
declare function verifyAccessToken(token: string, secret: string, options?: VerifyOptions): Promise&lt;AccessTokenClaims&gt;;
declare function verifyRefreshToken(token: string, secret: string, options?: VerifyOptions): Promise&lt;RefreshTokenClaims&gt;;
declare function readJwtCookie(value: Headers | Request | string | null | undefined, name?: string): string | undefined;
declare function jwtCookie(token: string, options?: &#123;
name?: string;
maxAge?: number;
secure?: boolean;
sameSite?: &quot;Strict&quot; | &quot;Lax&quot; | &quot;None&quot;;
path?: string;
&#125;): string;
declare function clearJwtCookie(options?: Omit&lt;Parameters&lt;typeof jwtCookie&gt;[1], &quot;maxAge&quot;&gt;): string;
interface JwtTokenPair &#123;
accessToken: string;
refreshToken: string;
tokenType: &quot;Bearer&quot;;
expiresIn: number;
&#125;
declare function createTokenPair(subject: string, input: &#123;
accessSecret: string;
refreshSecret?: string;
accessExpiresIn?: number;
refreshExpiresIn?: number;
scopes?: string[];
family?: string;
accessOptions?: Omit&lt;SignOptions, &quot;expiresIn&quot;&gt;;
refreshOptions?: Omit&lt;SignOptions, &quot;expiresIn&quot;&gt;;
&#125;): Promise&lt;JwtTokenPair&gt;;
declare function jwtResponse(accessToken: string, input?: &#123;
refreshToken?: string;
expiresIn?: number;
tokenType?: string;
scope?: string[];
&#125;): Response;
interface RemoteJwksOptions &#123;
fetch?: typeof fetch;
cacheTtlMs?: number;
maxKeys?: number;
maxBytes?: number;
now?: () =&gt; number;
&#125;
interface RemoteJwks &#123;
resolve(kid: string, alg: string): Promise&lt;CryptoKey&gt;;
refresh(): Promise&lt;void&gt;;
clear(): void;
stats(): &#123;
fetches: number;
hits: number;
keys: number;
expiresAt: number;
&#125;;
&#125;
declare function createRemoteJwks(url: string, options?: RemoteJwksOptions): RemoteJwks;
declare function verifyJwtWithJwks&lt;T extends JwtClaims = JwtClaims&gt;(token: string, jwks: RemoteJwks, options?: VerifyOptions): Promise&lt;T&gt;;
/**
* @wrnexus/jwt — dependency-free JSON Web Tokens (HS256) via WebCrypto, plus a
* bearer-token auth middleware. Pairs with the session auth in @wrnexus/core for
* stateless (API/mobile) authentication.
*
* const token = await signJwt(&#123; sub: user.id, role: &quot;admin&quot; &#125;, secret, &#123; expiresIn: 3600 &#125;);
* const claims = await verifyJwt(token, secret); // throws JwtError if invalid/expired
*/
declare class JwtError extends Error &#123;
constructor(message: string);
&#125;
interface JwtClaims &#123;
/** Subject (user id). */
sub?: string;
/** Issued-at (seconds). */
iat?: number;
/** Expiry (seconds). */
exp?: number;
/** Not-before (seconds). */
nbf?: number;
[key: string]: unknown;
&#125;
interface SignOptions &#123;
/** Seconds until expiry (sets `exp`). */
expiresIn?: number;
/** Override issued-at (seconds). */
now?: number;
issuer?: string;
audience?: string | string[];
jwtId?: string;
/** Key identifier placed in the protected header. */
keyId?: string;
&#125;
interface VerifyOptions &#123;
now?: number;
clockTolerance?: number;
issuer?: string;
audience?: string | string[];
maxAge?: number;
&#125;
/** Sign a payload into a JWT (HS256). */
declare function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise&lt;string&gt;;
/** Verify a JWT and return its claims. Throws `JwtError` on any failure. */
declare function verifyJwt&lt;T extends JwtClaims = JwtClaims&gt;(token: string, secret: string, options?: VerifyOptions): Promise&lt;T&gt;;
interface JwtAuthOptions &#123;
secret: string;
/** Where to read the token. Default: `Authorization: Bearer &lt;token&gt;`. */
getToken?: (ctx: Context) =&gt; string | undefined;
/** Reject unauthenticated requests with 401. Default true. */
required?: boolean;
&#125;
/**
* Middleware that verifies a bearer JWT and sets `ctx.user` to its claims.
* When `required` (default), a missing/invalid token gets a 401.
*/
declare function jwtAuth(options: JwtAuthOptions): Middleware;
export &#123; type AccessTokenClaims, type JwtAuthOptions, type JwtClaims, JwtError, type JwtKey, type JwtKeyring, type JwtTokenPair, type RefreshTokenClaims, type RemoteJwks, type RemoteJwksOptions, type SignOptions, type VerifyOptions, assertJwtClaims, clearJwtCookie, createAccessToken, createJwtKeyring, createRefreshToken, createRemoteJwks, createTokenPair, decodeJwt, extractBearerToken, hasScopes, jwtAuth, jwtCookie, jwtResponse, readJwtCookie, requireScopes, signJwt, signWithKeyring, tokenScopes, tryVerifyJwt, verifyAccessToken, verifyJwt, verifyJwtWithJwks, verifyRefreshToken, verifyWithKeyring &#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; signJwt, verifyJwt, jwtAuth, JwtError &#125; from &quot;@wrnexus/jwt&quot;;
const secret = process.env.JWT_SECRET!;
// Sign a token that expires in one hour
const token = await signJwt(&#123; sub: user.id, role: &quot;admin&quot; &#125;, secret, &#123;
expiresIn: 3600,
&#125;);
// Verify it later
try &#123;
const claims = await verifyJwt&lt;&#123; sub: string; role: string &#125;&gt;(token, secret);
console.log(claims.sub, claims.role);
&#125; catch (err) &#123;
if (err instanceof JwtError) &#123;
// invalid signature, expired, malformed, etc.
&#125;
&#125;</code></pre></article><article class="example-card"><h3>Protecting routes with the middleware</h3><pre data-language="ts"><code>import &#123; jwtAuth &#125; from &quot;@wrnexus/jwt&quot;;
// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth(&#123; secret: process.env.JWT_SECRET! &#125;));
// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth(&#123; secret: process.env.JWT_SECRET!, required: false &#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="#signjwt-payload-secret-options">signJwt(payload, secret, options?)</a><a class="toc-level-4" href="#verifyjwt-t-token-secret-options">verifyJwt&lt;T&gt;(token, secret, options?)</a><a class="toc-level-4" href="#jwtauth-options">jwtAuth(options)</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-3" href="#access-refresh-scope-and-cookie-helpers">Access, refresh, scope, and cookie helpers</a><a class="toc-level-3" href="#0-8-helper-kit">0.8 helper kit</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.8.7</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>
</div>
}
}