@exortek/security
Framework-agnostic defensive HTTP layer for Node.js 22+. CSRF, rate limiting, helmet-style headers, CORS, safe
redirects, and 17 focused helpers — everything built on node:crypto, node:path, and (only if you import the Fastify
adapter) fastify-plugin.
One install replaces the usual pile: helmet, csrf-csrf, express-rate-limit + rate-limit-redis,
express-slow-down, cors, hpp, express-mongo-sanitize. Adapters for Fastify, Express, Hono, and
Elysia ship in the box.
What problem does this solve?
The middleware every Node app needs is scattered across a dozen packages with mismatched APIs and subtle gaps:
- helmet has no CSP nonce helper. Rolling your own nonce + templating it into the
Content-Security-Policyheader is a boilerplate everyone rewrites badly. csurfwas archived. The community migrated tocsrf-csrf, but its API is Express-first — no Fastify / Hono / Elysia story.express-rate-limitneeds an external Redis adapter.rate-limit-redisis a separate package; the wire-up (Lua, key namespacing,EVALSHAoptimisation) is left to you.- Open-redirect defence is folklore. Everyone knows
//evil.comis bad; fewer people catch\evil.com,https://[email protected], orjavascript:submitted through a whitelisted-scheme allowlist. - Prototype-pollution guards are a two-line pattern you’re expected to remember on every JSON.parse.
@exortek/security ships one framework-agnostic API for all of them — CSRF, rate limiting, headers, CORS, safe
redirects, and a set of sharp-edged helpers — plus adapter modules that reduce integration to a single
app.use(securityMiddleware({...})).
Is it safe to trust?
- Every primitive is a thin wrapper around
node:cryptoor standard URL/path parsers. No hand-rolled crypto; the code you’re trusting is what Node itself is built on. - Timing-safe compare everywhere. CSRF verification, webhook signature check,
constantTimeEqualhelper — all usecrypto.timingSafeEqualon equal-length Buffers. - Every input is validated at the boundary. Bad-shape config throws
SecurityErrorwith an actionable message before your app boots. - Guard rails.
credentials: truewithorigin: '*'throws (browsers reject that combo). HSTSpreload: truerequires a 1-yearmax-age.javascript:/data:schemes stay blocked insafeRedirecteven if a caller opts them intoallowedSchemes. X-Frame-OptionsALLOW-FROMis rejected — modern browsers ignore it. - True LRU memory store. A hot key can never evict itself under cap pressure (a subtle rate-limit bypass in most in-process implementations).
- Tests: 241 currently passing, plus 10 gated on a live Redis instance for the Redis store.
@exortek/security gives you the primitives and the wiring; it doesn’t turn off your brain. CSRF still requires a
secure cookie path. Rate-limit still needs a sensible keyGenerator behind a proxy. Read the module page carefully
before wiring up secrets.
Install
npm install @exortek/security
yarn add @exortek/security
pnpm add @exortek/securityRequires Node.js 22 or newer.
Quick start
import Fastify from 'fastify';
import fastifyCookie from '@fastify/cookie';
import { securityPlugin } from '@exortek/security/fastify';
import { rateLimit } from '@exortek/security';
const app = Fastify();
await app.register(fastifyCookie);
await app.register(securityPlugin, {
headers: {}, // secure defaults
cors: { origin: ['https://app.example.com'], credentials: true },
csrf: { secret: process.env.CSRF_SECRET }, // ≥ 32 bytes
rateLimit: {
limiter: rateLimit.sliding({
requests: 100,
window: '1m',
store: rateLimit.stores.memory(),
}),
},
});
app.get('/', async () => ({ ok: true }));
await app.listen({ port: 3000 });Same shape on Express, Hono, Elysia — swap securityPlugin for securityMiddleware. See
middleware for the adapter-specific bits.
Setup — ESM, CJS, or TypeScript
The package ships both ESM and CJS builds with the same public surface. Types are emitted from JSDoc.
ESM (`import`)
import { csrf, cors, headers, rateLimit } from '@exortek/security'The default in every example on this site.
Modules at a glance
Every module has a dedicated page with full API reference.
| Module | Purpose | Key exports |
|---|---|---|
| csrf | signed / unsigned / session-bound tokens | generate · verify · generateUnsigned · verifyUnsigned · generateForSession · verifyForSession |
| rate-limit | fixed / sliding / token-bucket / leaky-bucket + multi + withBan | fixed · sliding · tokenBucket · leakyBucket · multi · withBan · stores.memory · stores.redis · stores.custom |
| headers | CSP, HSTS, COOP/COEP/CORP, Referrer, Permissions, frameguard, noSniff | headers · cspNonce |
| cors | origin allowlist + preflight + async predicates | cors |
| redirect | open-redirect guard | safeRedirect |
| helpers | 17 defensive utilities | getClientIp · bearer · checkOrigin · webhookVerify · sanitizeBody · sanitizeParams · safeJoin · sanitizeFilename · freezePrototypes · timeout · bodyLimit · honeypot · slowDown · safeJsonParse · constantTimeEqual · parseCspReport |
| middleware | Fastify · Express · Hono · Elysia adapters | securityMiddleware bundle + per-concern middleware |
| errors | typed error surface | SecurityError · ErrorCode |
Common use cases
| I want to… | Reach for |
|---|---|
| Protect state-changing routes from CSRF | csrf.generate / csrf.verify, or the middleware bundle |
| Rate-limit an endpoint | rateLimit.sliding + store |
| Layer 100/min AND 1k/hour AND ban abusers | rateLimit.multi + withBan |
| Set the security headers helmet would | headers() |
| Allow a browser origin cross-site with cookies | cors({ origin, credentials: true }) |
Safely redirect a user-supplied ?next= | safeRedirect |
| Verify a Stripe / GitHub webhook | webhookVerify |
| Get the real client IP behind an LB | getClientIp |
Refuse __proto__ in a JSON payload | safeJsonParse |
Drop $where / dotted keys from a body | sanitizeBody |
| Slow down brute-forcers instead of denying | slowDown |
| Wire up every guard in one line | middleware |
Import styles
Three styles, all supported, all tree-shakable.
Named, top level
import { csrf, cors, headers, rateLimit, safeRedirect }
from '@exortek/security'Most ergonomic — one import line for the whole surface.
Errors
Every entry point throws SecurityError with a stable code. Branch on code, not on the message — see the
errors page for the full enum.
import { SecurityError, ErrorCode, csrf } from '@exortek/security';
try {
csrf.generate('too-short');
} catch (err) {
if (!(err instanceof SecurityError)) throw err;
if (err.code === ErrorCode.INVALID_ARGUMENT) {
// config bug — fix at boot
}
}Node version, platforms, ESM/CJS
- Node 22 LTS or newer. Uses the native test runner internally, and WHATWG
URL,Buffer,cryptothroughout. - Pure ESM source with a matching CJS output. Both work.
- Types.
.d.tsgenerated from JSDoc at build. Strict TypeScript projects consume it cleanly. - Runtime footprint.
node:crypto+node:path+ (only under/fastify)fastify-plugin(~3 KB). Every framework itself is an optional peer.
What this package is not
- Not a WAF. Rate limiting is per-key; sophisticated bot / anomaly detection is out of scope. Pair with Cloudflare / DataDome / Bunny etc. if you need that layer.
- Not an HTML sanitizer. Writing a safe DOMPurify equivalent takes a full-time team; we don’t ship a half-good one. Use DOMPurify at the render layer.
- Not a session store. CSRF-session-bound mode reads a session id you already have; it doesn’t manage sessions for you.
- Not a WebAuthn / OAuth / passkey library. Those are separate protocols with their own packages in this repo (roadmap).
License
MIT — see the LICENSE file .