release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+52 -4
View File
@@ -33,6 +33,19 @@ export interface SignOptions {
expiresIn?: number;
/** Override issued-at (seconds). */
now?: number;
issuer?: string;
audience?: string | string[];
jwtId?: string;
/** Key identifier placed in the protected header. */
keyId?: string;
}
export interface VerifyOptions {
now?: number;
clockTolerance?: number;
issuer?: string;
audience?: string | string[];
maxAge?: number;
}
const enc = new TextEncoder();
@@ -86,8 +99,19 @@ export async function signJwt(
const now = options.now ?? Math.floor(Date.now() / 1000);
const claims: JwtClaims = { iat: now, ...payload };
if (options.expiresIn !== undefined) claims.exp = now + options.expiresIn;
if (options.issuer !== undefined) claims.iss = options.issuer;
if (options.audience !== undefined) claims.aud = options.audience;
if (options.jwtId !== undefined) claims.jti = options.jwtId;
const header = b64urlEncode(enc.encode(JSON.stringify({ alg: "HS256", typ: "JWT" })));
const header = b64urlEncode(
enc.encode(
JSON.stringify({
alg: "HS256",
typ: "JWT",
...(options.keyId ? { kid: options.keyId } : {}),
}),
),
);
const body = b64urlEncode(enc.encode(JSON.stringify(claims)));
const data = `${header}.${body}`;
const sig = new Uint8Array(
@@ -100,7 +124,7 @@ export async function signJwt(
export async function verifyJwt<T extends JwtClaims = JwtClaims>(
token: string,
secret: string,
options: { now?: number } = {},
options: VerifyOptions = {},
): Promise<T> {
const parts = token.split(".");
if (parts.length !== 3) throw new JwtError("Malformed token");
@@ -139,8 +163,30 @@ export async function verifyJwt<T extends JwtClaims = JwtClaims>(
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");
const tolerance = Math.max(0, options.clockTolerance ?? 0);
if (typeof claims.exp === "number" && now - tolerance >= claims.exp)
throw new JwtError("Token expired");
if (typeof claims.nbf === "number" && now + tolerance < claims.nbf)
throw new JwtError("Token not yet valid");
if (
options.maxAge !== undefined &&
typeof claims.iat === "number" &&
now - claims.iat > options.maxAge + tolerance
) {
throw new JwtError("Token is too old");
}
if (options.issuer !== undefined && claims.iss !== options.issuer)
throw new JwtError("Invalid issuer");
if (options.audience !== undefined) {
const expected = Array.isArray(options.audience) ? options.audience : [options.audience];
const actual = Array.isArray(claims.aud)
? claims.aud
: typeof claims.aud === "string"
? [claims.aud]
: [];
if (!expected.some((audience) => actual.includes(audience)))
throw new JwtError("Invalid audience");
}
return claims;
}
@@ -183,3 +229,5 @@ function bearerToken(ctx: Context): string | undefined {
function unauthorized(): Response {
return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}
export { decodeJwt, createJwtKeyring, signWithKeyring, verifyWithKeyring } from "./keyring.ts";
export type { JwtKey, JwtKeyring } from "./keyring.ts";