# @exortek/security

> Framework-agnostic defensive HTTP layer for Node.js 22+ — built on `node:crypto`.

[![npm](https://img.shields.io/npm/v/@exortek/security.svg?color=cb3837)](https://www.npmjs.com/package/@exortek/security)
[![tests](https://github.com/ExorTek/auth/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ExorTek/auth/actions/workflows/ci.yml)
[![node](https://img.shields.io/node/v/@exortek/security.svg?color=339933)](https://nodejs.org)
[![install size](https://packagephobia.com/badge?p=@exortek/security)](https://packagephobia.com/result?p=@exortek/security)
[![types](https://img.shields.io/badge/types-included-3178C6)](./dist/index.d.ts)
[![license](https://img.shields.io/npm/l/@exortek/security.svg?color=blue)](https://github.com/ExorTek/auth/blob/master/LICENSE)

CSRF, rate limiting, helmet-style headers, CORS, safe redirects, and 17 focused defensive helpers — one install replaces
`helmet` + `csrf-csrf` + `express-rate-limit` + `express-slow-down` + `cors` + `hpp` + `express-mongo-sanitize`. Adapters
for **Fastify** and **Express**.

📖 **Docs:** [**auth.memet.dev/security**](https://auth.memet.dev/security)

## Why

The defensive middleware most Node apps need is scattered across a dozen packages with mismatched APIs, drifting
maintainers, and subtle gaps (helmet has no CSP nonce helper, `csurf` was archived, `express- rate-limit`'s Redis story
is external). `@exortek/security` ships them once, correctly, framework-agnostically:

- **One API surface.** `csrf`, `rateLimit`, `headers`, `cors`, `safeRedirect` + 17 helpers — all pure functions. The
  framework adapters are a thin layer of glue on top.
- **Framework-agnostic.** Fastify and Express, each with a bundle (`securityMiddleware`) **and** per-concern middleware so
  you can pick just CORS or just rate-limit if that's what you need. Adding a fifth framework is one file — build an
  `AdapterContext` (see `src/middleware/core.js`) and wire it to `runHeaders` / `runCors` / `runRateLimit` / `runCsrf`.
- **Small footprint.** Runtime touches `node:crypto` and `node:path` — zero npm dependencies. Every framework itself is
  an **optional peer**.
- **JSDoc → `.d.ts`.** Pure JavaScript source, TypeScript types emitted at build. IDE hints without a `.ts` in sight.

## Install

```bash
npm  install @exortek/security
yarn add     @exortek/security
pnpm add     @exortek/security
```

Requires **Node.js 22 or newer**.

## Quick start

```js
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(),
    }),
  },
});
```

Same shape works on Express (`securityMiddleware`) — see the docs, or the runnable demo servers at
[`examples/express-server.js`](./examples/express-server.js) and
[`examples/fastify-server.js`](./examples/fastify-server.js).

## Modules

| Module                           | Purpose                                                                                                                                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`csrf`](./src/csrf)             | signed / unsigned / session-bound CSRF tokens                                                                                                                                                                                                                        |
| [`rate-limit`](./src/rate-limit) | fixed / sliding / token-bucket / leaky-bucket + `multi` + `withBan` — over memory / Redis / custom stores                                                                                                                                                            |
| [`headers`](./src/headers)       | CSP (+ nonce), HSTS, COOP/COEP/CORP, Referrer, Permissions, frameguard, noSniff, XSS-Protection                                                                                                                                                                      |
| [`cors`](./src/cors)             | origin allowlist with preflight handling and async predicates                                                                                                                                                                                                        |
| [`redirect`](./src/redirect)     | open-redirect guard + `extractReturnUrl` + `isSameOrigin`                                                                                                                                                                                                            |
| [`helpers`](./src/helpers)       | `getClientIp` · `bearer` · `checkOrigin` · `webhookVerify` · `webhookVerifyStripe` · `sanitizeBody` · `sanitizeParams` · `safeJoin` · `sanitizeFilename` · `freezePrototypes` · `timeout` · `bodyLimit` · `honeypot` · `slowDown` · `safeJsonParse` · `constantTimeEqual` · `parseCspReport` |
| middleware                       | `fastify` · `express` — each with `securityMiddleware` bundle **or** per-concern middleware                                                                                                                                                                          |

## Import styles

```js
// 1. Named at the top level — most ergonomic
import { cors, headers, safeRedirect, rateLimit } from '@exortek/security';

// 2. Named from a subpath — smallest bundle
import { cors } from '@exortek/security/cors';
import { rateLimit } from '@exortek/security/rate-limit';
import { safeRedirect } from '@exortek/security/redirect';

// 3. Framework middleware — one line for the whole stack
import { securityPlugin } from '@exortek/security/fastify';
import { securityMiddleware } from '@exortek/security/express';
```

## Error handling

Every recoverable failure throws `SecurityError` with a stable `ErrorCode`. Branch on the code, never on the message.

```js
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 */
  }
}
```

Codes: `INVALID_ARGUMENT`, `PATH_TRAVERSAL`, `BODY_TOO_LARGE`, `REQUEST_TIMEOUT`.

## Highlights

- **CSRF that just works.** Signed double-submit by default — HMAC-based, timing-safe. Also session-bound (no
  per-request storage) and unsigned modes.
- **`rateLimit.multi(...)` + `withBan(...)`.** Layer 100/min AND 1000/hour AND after-5-denials-ban-for-1h without
  writing custom logic.
- **True LRU memory store.** Access refreshes recency, so a hot key can never evict itself under cap pressure (a subtle
  bypass in most in-process rate-limiters).
- **Async CORS predicates.** `origin: async (o) => db.hasOrigin(o)` — `check()` stays sync for static allowlists,
  becomes async only when it needs to.
- **`safeRedirect(next, { allowedHosts })`.** Catches every classic open-redirect vector: `//evil.com`, `javascript:`,
  `data:`, userinfo tricks, backslash tricks, protocol-relative, control chars.
- **`safeJsonParse(body)`.** JSON parse that refuses `__proto__` payloads — closes the prototype-pollution door at the
  request boundary. Pair with `freezePrototypes({ exclude: ['Date', 'RegExp'] })` for global defence without breaking
  polyfills.
- **`webhookVerifyStripe(payload, header, secret, { tolerance })`.** Parses Stripe's `t=<ts>,v1=<hex>` envelope,
  verifies the HMAC-SHA-256 over `${t}.${payload}`, and rejects timestamps outside `|now − t| ≤ tolerance` (default
  300s). Supports secret rotation (`secret: [newest, ...older]`) and multiple `v1=<hex>` candidates in one header.

## Links

- **Source:** [github.com/ExorTek/auth](https://github.com/ExorTek/auth)
- **Issues & discussions:** [github.com/ExorTek/auth/issues](https://github.com/ExorTek/auth/issues)
- **Changelog:** [CHANGELOG.md](./CHANGELOG.md)

## License

MIT © ExorTek — see [LICENSE](https://github.com/ExorTek/auth/blob/master/LICENSE).
