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

@quilla-be-kit/security

v0.2.11

Published

Security primitives: Token/TokenService, PasswordHasher, SessionStore interfaces, and auth middlewares (Bearer-token verification + authenticated-session enrichment) that plug into @quilla-be-kit/http's Router.

Readme

@quilla-be-kit/security

Interface-only security primitives + the two auth middlewares that plug into @quilla-be-kit/http's Router:

  • Token (extends AuthenticatedToken from http) — verified-credential contract with userId, scopeId, securityStamp, issuedAt, expiresAt, isExpired(now?: Date). The optional now parameter defaults to new Date() — pass an explicit date in tests for deterministic expiry checks.
  • TokenClaims — canonical short-key wire-format type for the JWT payload (u, si, st, s? for scopes). Implementers map between the readable SignTokenPayload / Token shapes and these compact claims at the encode/decode boundary. Tokens travel in every request header — short keys exist for payload size, not security.
  • TokenServicesign(payload, { expiresIn }) + verify(raw) interface. Consumer provides the implementation (JWT via jose/djwt/jsonwebtoken, PASETO, opaque reference tokens — your choice).
  • SessionStore — keyed session record storage. Consumer picks the backend (Redis, Valkey, DynamoDB, Postgres).
  • SessionData — record shape: { securityStamp, displayName, userType }.
  • PasswordHasherhash + compare interface. Consumer provides the implementation (argon2 / bcrypt / scrypt).
  • bearerTokenMiddleware({ tokenService }) — reads Authorization: Bearer ..., calls tokenService.verify, populates HttpAttributes.VERIFIED_TOKEN. Throws UnauthorizedError on any failure.
  • authenticatedSessionMiddleware({ sessionStore, executionContextProvider }) — loads session data, compares securityStamp, enriches ExecutionContext with actorType: 'user' and a populated session: { scopeId, userId } (the AuthSession from @quilla-be-kit/execution-context). Throws UnauthorizedError on stamp mismatch or session miss.

Zero runtime dependencies on external libraries — JWT lib, cache driver, and hashing library are all consumer-owned. Node 22+.

Install

pnpm add @quilla-be-kit/security @quilla-be-kit/http @quilla-be-kit/execution-context @quilla-be-kit/errors

Why interface-only?

JWT algorithm choice, key rotation strategy, cache backend, and password-hashing cost parameters are security decisions that belong in the consumer's composition root, not in a toolkit. Shipping a built-in adapter for any of these would either:

  • Freeze a choice (one hashing algorithm, one JWT library) that teams rightfully contest, or
  • Pull in optional peer deps (bcrypt brings node-gyp, jose/jsonwebtoken bring opinions about crypto primitives) for what amounts to ~4–10 lines of glue.

Same discipline as RequestValidator in @quilla-be-kit/http: ship the interface, let consumers write the 5-line adapter.

Quick start

import { AsyncExecutionContextProvider } from '@quilla-be-kit/execution-context';
import { Router } from '@quilla-be-kit/http';
import { HonoServer } from '@quilla-be-kit/http/adapter/hono';
import {
  authenticatedSessionMiddleware,
  bearerTokenMiddleware,
  type TokenService,
  type SessionStore,
  type Token,
  type TokenClaims,
  type SignTokenPayload,
} from '@quilla-be-kit/security';

// --- Consumer-owned TokenService (example: jose) ---
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

const toClaims = (payload: SignTokenPayload): TokenClaims => ({
  u: payload.userId,
  si: payload.scopeId,
  st: payload.securityStamp,
  ...(payload.scopes ? { s: payload.scopes } : {}),
});

const fromClaims = (claims: TokenClaims): SignTokenPayload => ({
  userId: claims.u,
  scopeId: claims.si,
  securityStamp: claims.st,
  scopes: claims.s,
});

const jwtTokenService: TokenService = {
  async sign(payload: SignTokenPayload, options: { expiresIn: number }): Promise<string> {
    return new SignJWT({ ...toClaims(payload) })
      .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
      .setIssuedAt()
      .setExpirationTime(Math.floor(Date.now() / 1000) + options.expiresIn)
      .sign(secret);
  },
  async verify(raw: string): Promise<Token> {
    const { payload } = await jwtVerify(raw, secret);
    const decoded = fromClaims(payload as unknown as TokenClaims);
    const issuedAt = new Date((payload.iat as number) * 1000);
    const expiresAt = new Date((payload.exp as number) * 1000);
    return {
      ...decoded,
      issuedAt,
      expiresAt,
      isExpired(now = new Date()): boolean {
        return now >= expiresAt;
      },
    };
  },
};

// --- Consumer-owned SessionStore (example: Redis via ioredis) ---
import Redis from 'ioredis';

const redis = new Redis();
const sessionStore: SessionStore = {
  async set(userId, data, ttlSeconds) {
    await redis.setex(`session:${userId}`, ttlSeconds, JSON.stringify(data));
  },
  async get(userId) {
    const raw = await redis.get(`session:${userId}`);
    return raw ? JSON.parse(raw) : null;
  },
  async delete(userId) {
    await redis.del(`session:${userId}`);
  },
};

// --- Wire into Router ---
const provider = new AsyncExecutionContextProvider();

const router = new Router({
  executionContext: { provider },
  authStacks: {
    bearer: {
      credentialVerification: bearerTokenMiddleware({ tokenService: jwtTokenService }),
      sessionLoad: authenticatedSessionMiddleware({
        sessionStore,
        executionContextProvider: provider,
      }),
    },
  },
  defaultAuthStack: 'bearer',
  modules: registry.getAll(),
});

const server = new HonoServer({ port: 3000, router, serve });

Middleware chain

On a non-public route, Router runs the route's resolved auth stack:

system executionContext bootstrap (http-owned)
  → globalMiddlewares[]
    → bearerTokenMiddleware           (security — credentialVerification phase)
      → authenticatedSessionMiddleware  (security — sessionLoad phase, if present)
        → route middlewares
          → handler

Which stack a route resolves to is decided by @Get(path, { authStack }) ?? @Controller(prefix, { authStack }) ?? HttpModuleMeta.authStack ?? defaultAuthStack — see the @quilla-be-kit/http README. A single-audience service declares one stack and never thinks about it again.

On a *Public route, the auth stack is skipped entirely. The system execution-context bootstrap always runs, so even public handlers can call provider.getContext().

The typed AuthMiddlewareStack shape (from @quilla-be-kit/http) enforces phase ordering at the type level — credentialVerification runs before sessionLoad regardless of how keys are declared. The phase is named for the credential rather than the token because a stack may verify a JWT, an opaque token, an API key, or a client certificate; the attribute it populates keeps the VERIFIED_TOKEN name.

Middleware options

Both middlewares take a single minimal options bag:

type BearerTokenMiddlewareOptions = {
  readonly tokenService: TokenService;
};

type AuthenticatedSessionMiddlewareOptions = {
  readonly sessionStore: SessionStore;
  readonly executionContextProvider: ExecutionContextProvider;
};

No header override, no timeout, no key prefix — all of those are implementation choices inside your TokenService / SessionStore adapters (where they belong). If you need a different token header or scheme, swap the whole middleware — see "Custom token schemes" below.

Where implementations live

The interfaces (TokenService, SessionStore, PasswordHasher) belong in your consumer project, not in @quilla-be-kit/security. Typical placement:

src/
├── security/
│   ├── jwt-token-service.ts        // implements TokenService (jose / jsonwebtoken)
│   ├── redis-session-store.ts      // implements SessionStore (ioredis / upstash)
│   └── argon2-password-hasher.ts   // implements PasswordHasher (@node-rs/argon2)
└── composition-root.ts             // wires middlewares with the implementations above

The composition root is where you inject your concrete adapters into bearerTokenMiddleware({ tokenService }) and authenticatedSessionMiddleware({ sessionStore, executionContextProvider }), then pass the pair to new Router({ authStacks: { bearer: { ... } }, defaultAuthStack: 'bearer' }).

Custom credential schemes

Write your own credentialVerification middleware to support a mechanism other than Bearer — an API key header, an mTLS client cert, OAuth introspection. The only contract it must honor: on success, populate HttpAttributes.VERIFIED_TOKEN with something satisfying AuthenticatedToken (for @AuthorizeScope) — typically a full Token, so a session-load phase can load the matching session.

import type { HttpMiddleware } from '@quilla-be-kit/http';
import { HttpAttributes } from '@quilla-be-kit/http';
import { UnauthorizedError } from '@quilla-be-kit/errors';

const apiKeyVerification: HttpMiddleware = async (request, next) => {
  const key = request.getHeader('x-api-key');
  if (!key || !(await isValid(key))) {
    throw new UnauthorizedError({ message: 'Invalid API key' });
  }
  request.setAttribute(HttpAttributes.VERIFIED_TOKEN, await loadTokenForKey(key));
  await next();
};

If the service authenticates only this way, declare it as the single stack. If it serves two audiences — human routes on JWTs plus a bounded family of machine routes on API keys — declare both and select per route. The two never interact: a route runs exactly one stack.

const router = new Router({
  executionContext: { provider },
  authStacks: {
    bearer: {
      credentialVerification: bearerTokenMiddleware({ tokenService }),
      sessionLoad: authenticatedSessionMiddleware({ sessionStore, executionContextProvider: provider }),
    },
    apiKey: {
      credentialVerification: apiKeyVerification,
      sessionLoad: machineSessionLoad,   // yours — see the two warnings below
    },
  },
  defaultAuthStack: 'bearer',
  modules: registry.getAll(),
});
@Controller('/mcp', { authStack: 'apiKey' })
class McpController {
  @Get('/tools')
  async tools(req: HttpRequest) { ... }
}

Everything else composes unchanged: @AuthorizeScope reads VERIFIED_TOKEN without caring which stack populated it, and HttpAttributes.AUTH_STACK carries the resolved stack name if a guard needs to assert which one authenticated the caller.

Each stack needs its own sessionLoad

Do not reuse authenticatedSessionMiddleware for a second stack. It keys the store on token.userId alone and hardcodes actorType: 'user'. If a machine credential resolves to an existing user:

  • the machine has no session record of its own — it collides with the human's;
  • a human logging out (sessionStore.delete(userId)) silently revokes machine access, and a securityStamp rotation locks the machine out;
  • the context can never carry actorType: 'machine'.

Write a sessionLoad that checks that credential's revocation source — its own expiry and revoked flag — and enters a nested runWithContext with its own actorType. It must populate ExecutionContext.session (scopeId + userId): @ValidateRequest injects auth-derived scopeId/userId only when session is present, so a stack that sets actorType but omits session fails open — the handler runs with an undefined scope rather than being rejected.

Stacks must not share a credential verifier or signing key

TokenService.verify(token) takes no audience, and TokenClaims carries no aud, so a credential is verifiable by any verifier holding the same key. Per-route stack selection is the only thing keeping audiences apart — sharing a verifier between two stacks collapses that boundary, and unlike the revocation trap above it fails open. Give each stack its own verifier and key material. Scope strings must likewise be globally unique across stacks, since @AuthorizeScope reads one flat token.scopes namespace.

Session revocation

securityStamp is the revocation mechanism. Rotate the stored stamp whenever the session should be invalidated:

  • Explicit logoutsessionStore.delete(userId).
  • Password change / force revoke → rotate securityStamp in your user record and sessionStore.set(userId, { ...session, securityStamp: newStamp }, ttl).

The next request with a token carrying the old stamp fails authenticatedSessionMiddleware's stamp comparison and is rejected as UnauthorizedError — without any change to the token itself.

This mechanism belongs to the bearer stack. Revocation is per stack: another stack's sessionLoad checks its own source (an API key's revoked flag and expiry, a certificate revocation list), and must not route through the same SessionStore — see Each stack needs its own sessionLoad.

Role as the rule-of-three harness

@quilla-be-kit/security is the first consumer that exercises every other quilla-be-kit package in concert:

  • @quilla-be-kit/errorsUnauthorizedError surfaces through the http error handler.
  • @quilla-be-kit/execution-context — enriched context propagates through AsyncLocalStorage to downstream code.
  • @quilla-be-kit/httpHttpRequest/HttpMiddleware/HttpAttributes/AuthenticatedToken/AuthMiddlewareStack are all load-bearing here.
  • @quilla-be-kit/observability — the UnauthorizedError cause chain preserves the underlying verification failure for structured logs.

If these primitives can be composed into a clean auth module without bending any quilla-be-kit interface, the substrate is right. The moment it starts bending, we catch it here.

Status

Interface + middleware surface implemented. No bundled TokenService, SessionStore, or PasswordHasher adapters — by design.