Skip to content

@nodalite/middleware ​

First-party security and utility middleware. Zero runtime dependencies of its own — depends only on @nodalite/core and jose (for JWT).

npm install @nodalite/middleware

cors() ​

Cross-Origin Resource Sharing middleware.

ts
import { cors } from '@nodalite/middleware';

app.use('*', cors({ origin: 'https://app.example.com' }));

Options ​

OptionTypeDefaultDescription
originstring | string[]—Allowed origin(s). Not set by default (no ACAO header sent)
allowMethodsstring[]['GET','POST','PUT','PATCH','DELETE','OPTIONS']Allowed methods
allowHeadersstring[]['content-type','authorization']Allowed headers
exposeHeadersstring[][]Exposed headers
credentialsbooleanfalseAllow credentials
maxAgenumber—Preflight cache TTL (seconds)

securityHeaders() ​

OWASP-recommended security headers.

ts
import { securityHeaders } from '@nodalite/middleware';

app.use('*', securityHeaders());
app.use('*', securityHeaders({ contentSecurityPolicy: "default-src 'self'" }));

Default headers ​

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Strict-Transport-Securitymax-age=63072000; includeSubDomains
Content-Security-Policydefault-src 'self' (configurable)

rateLimit() ​

Request rate limiting with configurable window and max.

ts
import { rateLimit, MemoryRateLimitStore } from '@nodalite/middleware';

app.use('/api/*', rateLimit({ max: 100, windowMs: 60000 }));

Options ​

OptionTypeDefaultDescription
maxnumber60Max requests per window
windowMsnumber60000Window duration (ms)
messagestring"Too many requests"Response body on limit reached
storeRateLimitStoreMemoryRateLimitStoreCustom store (use Redis for production)

WARNING

MemoryRateLimitStore is not sufficient on serverless or multi-instance deployments. Each instance has its own memory. Implement RateLimitStore against Redis, Upstash, or DynamoDB for distributed rate limiting.

RateLimitStore interface ​

ts
interface RateLimitStore {
  increment(key: string, windowMs: number): Promise<{ count: number; ttl: number }>;
}

jwtAuth() / signJwt() ​

JWT authentication and token signing — built on jose (WebCrypto-based).

ts
import { jwtAuth, signJwt } from '@nodalite/middleware';

// Protect routes
app.use('/api/*', jwtAuth({ secret: new TextEncoder().encode(process.env.JWT_SECRET!) }));

// Sign tokens
const token = await signJwt(
  { sub: user.id, role: 'admin' },
  secret,
  { expiresIn: '1h' }
);

Options (jwtAuth) ​

OptionTypeDefaultDescription
secretUint8Array—HMAC secret key (required)
algorithmsstring[]['HS256']Allowed algorithms

Options (signJwt) ​

OptionTypeDefaultDescription
expiresInstring | number—Expiration duration ('1h', 3600)
algorithmstring'HS256'Signing algorithm

logger() ​

Request logging middleware.

ts
import { logger } from '@nodalite/middleware';

app.use('*', logger());
app.use('*', logger({ exclude: ['/health'] }));

Options ​

OptionTypeDefaultDescription
excludestring[][]Paths to exclude from logging

bodyLimit() ​

Reject oversized request bodies by Content-Length before buffering.

ts
import { bodyLimit } from '@nodalite/middleware';

app.use('*', bodyLimit({ max: 100_000 })); // 100 KB

Options ​

OptionTypeDefaultDescription
maxnumber1_000_000Max body size in bytes

csrf() ​

Double-submit cookie CSRF protection. Works across all runtimes without server-side sessions: the server sets a random token as a cookie, and the client must echo it back in a header or body field. Safe methods (GET, HEAD, OPTIONS, QUERY) are skipped by default.

ts
import { csrf } from '@nodalite/middleware';

app.use('*', csrf());

Options ​

OptionTypeDefaultDescription
cookieNamestring"XSRF-TOKEN"Cookie name for the CSRF token
headerNamestring"X-XSRF-Token"Header name the client must send
bodyFieldstring"_csrf"Request body field (fallback)
safeMethodsstring[]['GET','HEAD','OPTIONS','QUERY']Methods that skip validation
generateToken() => stringcrypto.randomUUID()Custom token generator
cookieobject—Cookie options (httpOnly, secure, sameSite, path, maxAge)

requestId() ​

Generates or propagates a unique request ID for every request. Essential for distributed tracing, log correlation, and debugging across services. If the client sends an X-Request-ID header and trustUpstream is true, that value is reused.

ts
import { requestId } from '@nodalite/middleware';

app.use('*', requestId());
app.get('/anything', (c) => {
  const id = c.get('requestId');
  return c.json({ requestId: id });
});

Options ​

OptionTypeDefaultDescription
headerNamestring"X-Request-ID"Header name for ID propagation
generate() => stringcrypto.randomUUID()Custom ID generator
trustUpstreambooleantrueForward upstream request IDs
contextKeystring"requestId"Context key for the stored ID

apiKey() ​

API key authentication middleware. Validates incoming API keys against a pluggable store. Extracts the key from a header, query parameter, or both.

ts
import { apiKey, MemoryApiKeyStore } from '@nodalite/middleware';

const store = new MemoryApiKeyStore();
store.add('my-secret-key', { plan: 'pro' });

app.use('/api/*', apiKey({ store }));
app.get('/api/data', (c) => {
  const key = c.get('apiKey');
  return c.json({ plan: key?.metadata?.plan });
});

Options ​

OptionTypeDefaultDescription
extractFrom'header' | 'query' | 'both''header'Where to read the key from
headerNamestring"X-API-Key"Header name
queryParamstring"api_key"Query parameter name
storeApiKeyStore—Key store (required)
contextKeystring"apiKey"Context key for validated key info

ApiKeyStore interface ​

ts
interface ApiKeyStore {
  validate(key: string): Promise<{ id: string; metadata?: Record<string, unknown> } | null>;
}

MemoryApiKeyStore ​

In-memory store for development and single-process deployments. Methods: add(key, metadata?), remove(key), validate(key), destroy().

WARNING

MemoryApiKeyStore is not sufficient on serverless or multi-instance deployments. Implement ApiKeyStore against Redis, DynamoDB, etc. for production.

ipGuard() ​

IP allowlisting/blocklisting middleware. Supports individual IPs and CIDR notation (e.g., 192.168.0.0/16).

ts
import { ipGuard } from '@nodalite/middleware';

app.use('*', ipGuard({ mode: 'deny', list: ['10.0.0.0/8', '192.168.0.0/16'] }));
app.use('/admin/*', ipGuard({ mode: 'allow', list: ['203.0.113.50'] }));

Options ​

OptionTypeDefaultDescription
mode'allow' | 'deny''deny'Allow-only listed or block listed
liststring[]—IPs or CIDR ranges (required)
keyGenerator(c) => stringplatform IP or x-forwarded-forHow to derive the client IP
messagestring"Access denied"Custom rejection message

contentTypeGuard() ​

Validates that incoming requests have an allowed Content-Type header. Rejects with 415 Unsupported Media Type on mismatch.

ts
import { contentTypeGuard } from '@nodalite/middleware';

app.post('/data', handler, [contentTypeGuard({ required: ['application/json'] })]);
app.use('/upload/*', contentTypeGuard({ required: ['multipart/*', 'application/json'] }));

Options ​

OptionTypeDefaultDescription
requiredstring[]—Allowed Content-Types (supports wildcards like multipart/*)
methodsstring[]['POST','PUT','PATCH','QUERY']Methods to enforce on
messagestring—Custom rejection message

requestTimeout() ​

Enforces a per-request timeout using Promise.race. If the handler chain doesn't complete within the specified duration, returns 408 Request Timeout.

ts
import { requestTimeout } from '@nodalite/middleware';

app.use('*', requestTimeout({ ms: 10_000 })); // 10 second timeout

Options ​

OptionTypeDefaultDescription
msnumber—Timeout in milliseconds (required)
messagestring—Custom rejection message

xssSanitize() ​

Sanitizes string values in JSON request bodies to prevent stored XSS. Encodes HTML entities (<, >, ", ', etc.) by default. The sanitized body is stored in context — use sanitizedBody() to retrieve it.

ts
import { xssSanitize, sanitizedBody } from '@nodalite/middleware';

app.post('/comments', async (c) => {
  const body = sanitizedBody<{ text: string }>(c);
  return c.json({ text: body.text });
}, [xssSanitize()]);

WARNING

Handlers must use sanitizedBody<T>(c) instead of c.req.json() to get sanitized data. The middleware stores the result in context; the original request body is not modified.

Options ​

OptionTypeDefaultDescription
fieldsstring[]—Specific fields to sanitize (all strings if omitted)
sanitizeQuerybooleanfalseAlso sanitize query parameters
sanitizer(value: string) => stringHTML entity encodingCustom sanitizer function

sanitizedBody<T>(c) ​

Helper function to retrieve the sanitized body from context. Returns the sanitized object typed as T.

ssrfGuard() ​

SSRF (Server-Side Request Forgery) protection. Validates that user-supplied URLs don't point to internal/private IP ranges, cloud metadata endpoints, or other non-routable addresses. Resolves hostnames via DNS and checks against blocked CIDR ranges.

ts
import { ssrfGuard } from '@nodalite/middleware';

app.post('/fetch-url', handler, [ssrfGuard()]);
app.post('/webhook', handler, [ssrfGuard({ allowPrivate: true })]);

Default blocked ranges include RFC 1918 private networks, link-local, loopback, and cloud metadata endpoints (169.254.0.0/16).

Options ​

OptionTypeDefaultDescription
blockListstring[][]Additional IPs/CIDRs to block
allowPrivatebooleanfalseAllow requests to private networks
extractUrl(c) => Promise<string | null>reads url from JSON bodyHow to extract the target URL
messagestring—Custom rejection message

INFO

On edge runtimes (Cloudflare Workers), DNS resolution is unavailable — IP-level checks are skipped but localhost and protocol validation still apply.

sessions() ​

Cookie-based session middleware with HMAC-signed session IDs. Session data lives in the store (in-memory for dev, Redis/database for production). Mutations to the session object are automatically persisted after the response.

ts
import { sessions } from '@nodalite/middleware';

app.use('*', sessions({ secret: process.env.SESSION_SECRET! }));

app.get('/login', async (c) => {
  const session = c.get('session');
  session.userId = '123';
  return c.json({ loggedIn: true });
});

app.get('/me', (c) => {
  const session = c.get('session');
  return c.json({ userId: session?.userId });
});

Options ​

OptionTypeDefaultDescription
cookieNamestring"sid"Cookie name
secretstring—HMAC secret for signing session IDs (required)
maxAgenumber86400Session max age in seconds
storeSessionStoreMemorySessionStoreSession store
contextKeystring"session"Context key for session data
cookieobject—Cookie options (httpOnly, secure, sameSite, path)

SessionStore interface ​

ts
interface SessionStore {
  get(id: string): Promise<Record<string, unknown> | null>;
  set(id: string, data: Record<string, unknown>, maxAge: number): Promise<void>;
  destroy(id: string): Promise<void>;
}

MemorySessionStore ​

In-memory store with automatic expired-session cleanup. Methods: get(id), set(id, data, maxAge), destroy(id), destroy_() (release timer + clear).

WARNING

MemorySessionStore is not sufficient on serverless or multi-instance deployments. Implement SessionStore against Redis, DynamoDB, etc. for production.

Released under the MIT License.