Raises the typescript devDependency across the workspace, bumps package versions, re-adds ignoreDeprecations, and repoints the @wrnexus registry. These were pre-existing working-tree changes, committed as-is rather than authored here. The .npmrc change redirects @wrnexus publishes from registry.npmjs.org to registry.workroot.in — confirm that is intended before publishing from this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/jwt
Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WrNexus.
Part of the WrNexus 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.
Installation
bun add @wrnexus/jwt
Private package — the machine must be authenticated to the
wrnexusnpm 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.
| Export | Kind | Description |
|---|---|---|
signJwt(payload, secret, options?) |
function | Sign claims into an HS256 token string. |
verifyJwt<T>(token, secret, options?) |
function | Verify a token and return its claims, or throw. |
jwtAuth(options) |
function | Middleware that verifies a bearer JWT and sets ctx.user. |
JwtError |
class | Error thrown on any signature/payload/expiry failure. |
JwtClaims |
interface | Claims shape (sub, iat, exp, nbf, plus arbitrary keys). |
SignOptions |
interface | Options for signJwt. |
JwtAuthOptions |
interface | Options 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 theexpclaim.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 WrNexus 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 readingAuthorization: Bearer <token>.required?: boolean— whentrue(default), a missing or invalid token responds with401 { ok: false, error: "Unauthorized" }. Whenfalse, requests pass through andctx.useris 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) plusbtoa/atobandTextEncoder/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/coreforContext,Middleware, andctx.user; it complements the framework's cookie/session auth with a stateless bearer-token flow for API and mobile clients.
Access, refresh, scope, and cookie helpers
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.