W WRNexusJS
Security · Package reference

@wrnexus/jwt

HS256 JWT signing, verification, and bearer authentication.

v0.8.7Private registrySecurity

Install the package

After WorkRoot approves private registry access, install the release-aligned package:

bun add @wrnexus/jwt@0.8.7

Request preview access. Never put registry tokens in source control.

Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WRNexusJS.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/jwt signs and verifies stateless JSON Web Tokens using the HS256 (HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and verification are implemented directly on the standard Web Crypto API (crypto.subtle), which Bun provides natively. It runs server-side and pairs with the session-based auth in @wrnexus/core, giving you a stateless option for API and mobile clients. Reach for it when you need bearer-token auth rather than cookie sessions.

bun add @wrnexus/jwt
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

Single entry point (@wrnexus/jwt). All functions are async and return Promises.

ExportKindDescription
signJwt(payload, secret, options?)functionSign claims into an HS256 token string.
verifyJwt<T>(token, secret, options?)functionVerify a token and return its claims, or throw.
jwtAuth(options)functionMiddleware that verifies a bearer JWT and sets ctx.user.
JwtErrorclassError thrown on any signature/payload/expiry failure.
JwtClaimsinterfaceClaims shape (sub, iat, exp, nbf, plus arbitrary keys).
SignOptionsinterfaceOptions for signJwt.
JwtAuthOptionsinterfaceOptions for jwtAuth.

signJwt(payload, secret, options?)

function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;

Signs payload with secret using HS256 and returns the encoded token (header.body.signature). An iat (issued-at) claim is always added.

SignOptions:

  • expiresIn?: number — seconds until expiry; sets the exp claim.
  • now?: number — override the issued-at time (seconds), useful for testing.

verifyJwt<T>(token, secret, options?)

function verifyJwt<T extends JwtClaims = JwtClaims>(
  token: string,
  secret: string,
  options?: { now?: number },
): Promise<T>;

Verifies the HS256 signature and returns the decoded claims typed as T. Throws JwtError when the token is malformed, the signature is invalid, the payload is not valid JSON, the token is expired (exp), or not yet valid (nbf). Pass now (seconds) to override the reference time for the exp/nbf checks.

jwtAuth(options)

function jwtAuth(options: JwtAuthOptions): Middleware;

Returns a WRNexusJS Middleware that reads a token, verifies it, and assigns the claims to ctx.user.

JwtAuthOptions:

  • secret: string — the HMAC secret used to verify tokens.
  • getToken?: (ctx: Context) => string | undefined — how to extract the token.
  • Defaults to reading Authorization: Bearer <token>.

  • required?: boolean — when true (default), a missing or invalid token
  • responds with 401 { ok: false, error: "Unauthorized" }. When false, requests pass through and ctx.user is only set if a valid token is present.

Usage

import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";

const secret = process.env.JWT_SECRET!;

// Sign a token that expires in one hour
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
  expiresIn: 3600,
});

// Verify it later
try {
  const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
  console.log(claims.sub, claims.role);
} catch (err) {
  if (err instanceof JwtError) {
    // invalid signature, expired, malformed, etc.
  }
}

Protecting routes with the middleware:

import { jwtAuth } from "@wrnexus/jwt";

// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));

// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));

Requirements / Notes

  • Bun-only. Uses the standard Web Crypto API (crypto.subtle.importKey,
  • sign, verify) plus btoa/atob and TextEncoder/TextDecoder — all provided by Bun. No third-party crypto dependency.

  • Algorithm: HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)
  • are not supported.

  • Integrates with [@wrnexus/core](../core) for Context, Middleware, and
  • ctx.user; it complements the framework's cookie/session auth with a stateless bearer-token flow for API and mobile clients.

import {
  createAccessToken,
  createRefreshToken,
  verifyAccessToken,
  verifyRefreshToken,
  extractBearerToken,
  requireScopes,
  jwtCookie,
} from "@wrnexus/jwt";

The helpers add explicit type: "access" | "refresh" claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. __Host- cookies are rejected unless they use Path=/ and Secure; SameSite=None is rejected without Secure.

0.8 helper kit

import {
  createTokenPair,
  verifyAccessToken,
  verifyRefreshToken,
  extractBearerToken,
  readJwtCookie,
  jwtCookie,
  clearJwtCookie,
  requireScopes,
} from "@wrnexus/jwt";

const pair = await createTokenPair(user.id, {
  accessSecret: process.env.JWT_ACCESS_SECRET!,
  refreshSecret: process.env.JWT_REFRESH_SECRET!,
  scopes: ["profile:read"],
  family: sessionFamily,
});

The helper kit validates __Host- cookie invariants, cookie names and paths, SameSite=None 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:

import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt";

const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json");
const claims = await verifyJwtWithJwks(token, jwks, {
  issuer: "https://issuer.example",
  audience: "my-api",
  maxAge: 300,
});

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 kid so issuer rotation does not wait for cache expiry. Never use decoded-but-unverified claims for an authorization decision.

Complete TypeScript API

Generated from the exact installed package declarations.

import { Context, Middleware } from '@wrnexus/core';

interface JwtKey {
    id: string;
    secret: string;
    active?: boolean;
}
interface JwtKeyring {
    active(): JwtKey;
    resolve(id: string): JwtKey | undefined;
    keys(): JwtKey[];
}
declare function decodeJwt(token: string): {
    header: Record<string, unknown>;
    claims: JwtClaims;
};
declare function createJwtKeyring(keys: JwtKey[]): JwtKeyring;
declare function signWithKeyring(claims: JwtClaims, keyring: JwtKeyring, options?: SignOptions): Promise<string>;
declare function verifyWithKeyring<T extends JwtClaims = JwtClaims>(token: string, keyring: JwtKeyring, options?: VerifyOptions): Promise<T>;

interface AccessTokenClaims extends JwtClaims {
    sub: string;
    type: "access";
    scopes?: string[];
}
interface RefreshTokenClaims extends JwtClaims {
    sub: string;
    type: "refresh";
    family?: string;
}
declare function extractBearerToken(value: Headers | Request | Context | string | null | undefined): string | undefined;
declare function tryVerifyJwt<T extends JwtClaims = JwtClaims>(token: string | undefined, secret: string, options?: VerifyOptions): Promise<T | null>;
declare function assertJwtClaims<T extends JwtClaims>(claims: T, requirements?: {
    subject?: boolean;
    type?: string;
    required?: string[];
}): T;
declare function tokenScopes(claims: JwtClaims): string[];
declare function hasScopes(claims: JwtClaims, required: readonly string[], mode?: "all" | "any"): boolean;
declare function requireScopes(required: readonly string[], mode?: "all" | "any"): Middleware;
declare function createAccessToken(subject: string, secret: string, options?: Omit<SignOptions, "expiresIn"> & {
    expiresIn?: number;
    scopes?: string[];
    claims?: JwtClaims;
}): Promise<string>;
declare function createRefreshToken(subject: string, secret: string, options?: Omit<SignOptions, "expiresIn"> & {
    expiresIn?: number;
    family?: string;
    claims?: JwtClaims;
}): Promise<string>;
declare function verifyAccessToken(token: string, secret: string, options?: VerifyOptions): Promise<AccessTokenClaims>;
declare function verifyRefreshToken(token: string, secret: string, options?: VerifyOptions): Promise<RefreshTokenClaims>;
declare function readJwtCookie(value: Headers | Request | string | null | undefined, name?: string): string | undefined;
declare function jwtCookie(token: string, options?: {
    name?: string;
    maxAge?: number;
    secure?: boolean;
    sameSite?: "Strict" | "Lax" | "None";
    path?: string;
}): string;
declare function clearJwtCookie(options?: Omit<Parameters<typeof jwtCookie>[1], "maxAge">): string;
interface JwtTokenPair {
    accessToken: string;
    refreshToken: string;
    tokenType: "Bearer";
    expiresIn: number;
}
declare function createTokenPair(subject: string, input: {
    accessSecret: string;
    refreshSecret?: string;
    accessExpiresIn?: number;
    refreshExpiresIn?: number;
    scopes?: string[];
    family?: string;
    accessOptions?: Omit<SignOptions, "expiresIn">;
    refreshOptions?: Omit<SignOptions, "expiresIn">;
}): Promise<JwtTokenPair>;
declare function jwtResponse(accessToken: string, input?: {
    refreshToken?: string;
    expiresIn?: number;
    tokenType?: string;
    scope?: string[];
}): Response;

interface RemoteJwksOptions {
    fetch?: typeof fetch;
    cacheTtlMs?: number;
    maxKeys?: number;
    maxBytes?: number;
    now?: () => number;
}
interface RemoteJwks {
    resolve(kid: string, alg: string): Promise<CryptoKey>;
    refresh(): Promise<void>;
    clear(): void;
    stats(): {
        fetches: number;
        hits: number;
        keys: number;
        expiresAt: number;
    };
}
declare function createRemoteJwks(url: string, options?: RemoteJwksOptions): RemoteJwks;
declare function verifyJwtWithJwks<T extends JwtClaims = JwtClaims>(token: string, jwks: RemoteJwks, options?: VerifyOptions): Promise<T>;

/**
 * @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({ sub: user.id, role: "admin" }, secret, { expiresIn: 3600 });
 *   const claims = await verifyJwt(token, secret); // throws JwtError if invalid/expired
 */

declare class JwtError extends Error {
    constructor(message: string);
}
interface JwtClaims {
    /** Subject (user id). */
    sub?: string;
    /** Issued-at (seconds). */
    iat?: number;
    /** Expiry (seconds). */
    exp?: number;
    /** Not-before (seconds). */
    nbf?: number;
    [key: string]: unknown;
}
interface SignOptions {
    /** 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;
}
interface VerifyOptions {
    now?: number;
    clockTolerance?: number;
    issuer?: string;
    audience?: string | string[];
    maxAge?: number;
}
/** Sign a payload into a JWT (HS256). */
declare function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
/** Verify a JWT and return its claims. Throws `JwtError` on any failure. */
declare function verifyJwt<T extends JwtClaims = JwtClaims>(token: string, secret: string, options?: VerifyOptions): Promise<T>;
interface JwtAuthOptions {
    secret: string;
    /** Where to read the token. Default: `Authorization: Bearer <token>`. */
    getToken?: (ctx: Context) => string | undefined;
    /** Reject unauthenticated requests with 401. Default true. */
    required?: boolean;
}
/**
 * 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 { 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 };

Examples

Copy-ready examples from the installed package documentation.

Typical usage

import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";

const secret = process.env.JWT_SECRET!;

// Sign a token that expires in one hour
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
  expiresIn: 3600,
});

// Verify it later
try {
  const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
  console.log(claims.sub, claims.role);
} catch (err) {
  if (err instanceof JwtError) {
    // invalid signature, expired, malformed, etc.
  }
}

Protecting routes with the middleware

import { jwtAuth } from "@wrnexus/jwt";

// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));

// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));