npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@triadjs/jwt

v0.2.2

Published

JWT authentication BeforeHandler factory for Triad endpoints wrapping jose

Readme

@triadjs/jwt

JWT verification for Triad endpoints. A tiny BeforeHandler factory that wraps jose so any endpoint becomes auth-protected with a handful of lines and zero middleware magic.

Install

npm install @triadjs/jwt jose

jose is a peer dependency. @triadjs/jwt does not bundle it, does not pin its version, and loads it lazily via dynamic import — you bring your own version (5.x or 6.x today).

Quick start — HS256 with a shared secret

import { endpoint, t } from '@triadjs/core';
import { requireJWT } from '@triadjs/jwt';

const User = t.model('User', {
  id: t.string(),
  email: t.string({ format: 'email' }),
});

const ApiError = t.model('ApiError', {
  code: t.string(),
  message: t.string(),
});

const requireAuth = requireJWT({
  secret: process.env.JWT_SECRET!,
  algorithms: ['HS256'],
  issuer: 'my-api',
  audience: 'my-api-users',
  extractUser: (claims) => ({
    id: claims.sub as string,
    email: claims.email as string,
  }),
});

export const getMe = endpoint({
  method: 'GET',
  path: '/me',
  beforeHandler: requireAuth,
  responses: {
    200: { schema: User, description: 'Current user' },
    401: { schema: ApiError, description: 'Not authenticated' },
  },
  handler: async (ctx) => ctx.respond[200](ctx.state.user),
});

ctx.state.user is fully typed — the generic TUser is inferred from extractUser's return type without any annotations on the handler.

Quick start — JWKS with RS256 (production shape)

const requireAuth = requireJWT({
  jwksUri: 'https://my-auth.example.com/.well-known/jwks.json',
  issuer: 'https://my-auth.example.com',
  audience: 'my-api',
  algorithms: ['RS256'],
  extractUser: (claims) => ({
    id: claims.sub!,
    roles: (claims['https://my-app.com/roles'] as string[]) ?? [],
  }),
});

requireJWT caches the JWKS set on first use and reuses it across requests — one HTTP fetch per key rotation, not one per request.

Provider recipes

For full walkthroughs covering Auth0, Clerk, WorkOS, Firebase, Supabase, NextAuth, API keys, session cookies, RBAC and multi-tenancy, see docs/guides/auth.md.

Options

| Field | Type | Default | Notes | | ---------------- | -------------------------------------- | ---------------------------------- | -------------------------------------------------------------------- | | jwksUri | string | — | Required unless secret is set. Mutually exclusive with secret. | | secret | string \| Uint8Array | — | Required unless jwksUri is set. UTF-8 encoded internally. | | issuer | string \| string[] | — | Expected iss. Mismatch → 401. | | audience | string \| string[] | — | Expected aud. Do not skip. | | algorithms | string[] | ['RS256', 'ES256', 'HS256'] (jose default) | Restrict to what your issuer uses. | | clockTolerance | number (seconds) | 5 | Tolerance for exp/nbf. | | extractUser | (claims) => TUser | — | Required. Maps verified claims onto your domain user type. | | onVerified | (claims, user) => void \| Promise | — | Post-verification hook for audit logging or revocation checks. |

extractUser — why you define the user shape

Triad does not ship an opinion about what a user is. Your JWT may carry sub, email, and roles under https://my-app.com/roles; another app may embed a tenant id, a plan tier, or a feature flag list. extractUser is the single seam where you translate issuer-specific claims into your application's domain User type.

Throwing from extractUser produces a typed 401. Use this to enforce "the token verified, but it doesn't describe a user we'll accept" — for example, when sub is missing or a required custom claim is absent.

onVerified — audit logging and revocation

const requireAuth = requireJWT({
  jwksUri: '...',
  extractUser: (claims) => ({ id: claims.sub! }),
  onVerified: async (claims, user) => {
    if (await revocationCache.has(claims.jti!)) {
      throw new Error('token revoked');
    }
    auditLog.push({ at: Date.now(), userId: user.id, jti: claims.jti });
  },
});

Throwing from onVerified maps to a 401 — the request is rejected before the main handler runs.

Typed ctx.state.user

interface Me { id: string; tenantId: string; roles: string[]; }

const requireAuth = requireJWT({
  jwksUri: '...',
  extractUser: (claims): Me => ({
    id: claims.sub!,
    tenantId: claims['tenant_id'] as string,
    roles: claims['roles'] as string[],
  }),
});

// In the handler:
ctx.state.user.tenantId  // ✓ typed as string
ctx.state.user.whatever  // ✗ compile error

The generic TUser flows through BeforeHandler<{ user: TUser }, ...> into HandlerContext.state, so no cast or annotation is needed at the handler site.

Security notes

  • Never log raw tokens. Log the jti or a hash if you need correlation.
  • Prefer JWKS over shared secrets in production. JWKS gives you key rotation for free; a leaked static HS256 secret forces a manual rotation across every verifier.
  • Always validate audience. Missing audience validation lets a token minted for service A be replayed at service B — a classic CVE pattern.
  • Keep clockTolerance small. Five seconds is plenty. Pushing past 60 should require a comment explaining why.
  • Do not mix HS256 with RS256 in the allowed algorithms list. The "algorithm confusion" class of attacks relies on a verifier that accepts either.
  • Rotate secrets on a schedule. Even if nothing has leaked.

v1 non-goals

  • No session-based auth. For SSR apps with server-side sessions, see the cookies pattern in docs/guides/auth.md.
  • No token revocation list. Use onVerified plus your own cache / database if you need short-notice revocation.
  • No automatic refresh. Clients handle refresh on 401 — that's the right layering for an API package.
  • No OAuth dance. Triad verifies already-issued tokens. The authorization code flow, device flow, and PKCE are handled by your identity provider (Auth0, Clerk, …) or a dedicated library.

See also

  • docs/guides/auth.md — the full auth cookbook with provider recipes and pattern discussion.
  • @triadjs/coreBeforeHandler, HandlerContext, checkOwnership.
  • jose — the underlying verification library.