first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
/**
* @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
*/
import type { Context, Middleware } from "@wrnexus/core";
export class JwtError extends Error {
constructor(message: string) {
super(message);
this.name = "JwtError";
}
}
export interface JwtClaims {
/** Subject (user id). */
sub?: string;
/** Issued-at (seconds). */
iat?: number;
/** Expiry (seconds). */
exp?: number;
/** Not-before (seconds). */
nbf?: number;
[key: string]: unknown;
}
export interface SignOptions {
/** Seconds until expiry (sets `exp`). */
expiresIn?: number;
/** Override issued-at (seconds). */
now?: number;
}
const enc = new TextEncoder();
const MAX_CACHED_KEYS = 32;
const keyCache = new Map<string, Promise<CryptoKey>>();
function b64urlEncode(bytes: Uint8Array): string {
let bin = "";
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64urlDecode(str: string): Uint8Array {
const pad = str.length % 4 === 0 ? "" : "=".repeat(4 - (str.length % 4));
const bin = atob(str.replace(/-/g, "+").replace(/_/g, "/") + pad);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
async function hmacKey(secret: string): Promise<CryptoKey> {
const cached = keyCache.get(secret);
if (cached) {
// Refresh insertion order so frequently used secrets stay hot.
keyCache.delete(secret);
keyCache.set(secret, cached);
return cached;
}
const imported = crypto.subtle.importKey(
"raw",
enc.encode(secret) as BufferSource,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
keyCache.set(secret, imported);
if (keyCache.size > MAX_CACHED_KEYS) keyCache.delete(keyCache.keys().next().value!);
try {
return await imported;
} catch (error) {
keyCache.delete(secret);
throw error;
}
}
/** Sign a payload into a JWT (HS256). */
export async function signJwt(
payload: JwtClaims,
secret: string,
options: SignOptions = {},
): Promise<string> {
const now = options.now ?? Math.floor(Date.now() / 1000);
const claims: JwtClaims = { iat: now, ...payload };
if (options.expiresIn !== undefined) claims.exp = now + options.expiresIn;
const header = b64urlEncode(enc.encode(JSON.stringify({ alg: "HS256", typ: "JWT" })));
const body = b64urlEncode(enc.encode(JSON.stringify(claims)));
const data = `${header}.${body}`;
const sig = new Uint8Array(
await crypto.subtle.sign("HMAC", await hmacKey(secret), enc.encode(data) as BufferSource),
);
return `${data}.${b64urlEncode(sig)}`;
}
/** Verify a JWT and return its claims. Throws `JwtError` on any failure. */
export async function verifyJwt<T extends JwtClaims = JwtClaims>(
token: string,
secret: string,
options: { now?: number } = {},
): Promise<T> {
const parts = token.split(".");
if (parts.length !== 3) throw new JwtError("Malformed token");
const [header, body, sig] = parts as [string, string, string];
try {
const parsed = JSON.parse(new TextDecoder().decode(b64urlDecode(header))) as {
alg?: unknown;
typ?: unknown;
};
if (parsed.alg !== "HS256" || (parsed.typ !== undefined && parsed.typ !== "JWT")) {
throw new JwtError("Unsupported token header");
}
} catch (error) {
if (error instanceof JwtError) throw error;
throw new JwtError("Invalid token header");
}
let valid: boolean;
try {
valid = await crypto.subtle.verify(
"HMAC",
await hmacKey(secret),
b64urlDecode(sig) as BufferSource,
enc.encode(`${header}.${body}`) as BufferSource,
);
} catch {
throw new JwtError("Invalid signature");
}
if (!valid) throw new JwtError("Invalid signature");
let claims: T;
try {
claims = JSON.parse(new TextDecoder().decode(b64urlDecode(body))) as T;
} catch {
throw new JwtError("Invalid payload");
}
const now = options.now ?? Math.floor(Date.now() / 1000);
if (typeof claims.exp === "number" && now >= claims.exp) throw new JwtError("Token expired");
if (typeof claims.nbf === "number" && now < claims.nbf) throw new JwtError("Token not yet valid");
return claims;
}
export 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.
*/
export function jwtAuth(options: JwtAuthOptions): Middleware {
const getToken = options.getToken ?? bearerToken;
const required = options.required !== false;
return async (ctx, next) => {
const token = getToken(ctx);
if (token) {
try {
ctx.user = await verifyJwt(token, options.secret);
} catch {
if (required) return unauthorized();
}
} else if (required) {
return unauthorized();
}
return next();
};
}
function bearerToken(ctx: Context): string | undefined {
const header = ctx.req.headers.get("authorization") ?? "";
const m = /^Bearer\s+(.+)$/i.exec(header);
return m ? m[1] : undefined;
}
function unauthorized(): Response {
return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}