@samveys/security
From packages/security/README.md in the framework repository.
CSRF protection, CORS and rate limiting as HTTP middleware. No runtime dependencies.
import { cache, defineCacheConfig, memoryStore } from '@samveys/cache'
import { http } from '@samveys/http'
import { security } from '@samveys/security'
export const plugins = [
http(),
cache(defineCacheConfig({ default: 'm', stores: { m: memoryStore() } })),
security({
csrf: { api: false },
cors: { origins: ['https://app.example.com'], credentials: true },
rateLimit: { limit: 10, windowMs: 60_000 },
}),
]The plugin registers the named middleware csrf, cors and rateLimit. Use them on a route or a group:
import { get, post } from '@samveys/http'
export const login = post('/login', { use: ['csrf', 'rateLimit'] }, () => ({ ok: true }))
export const feed = get('/feed', { use: ['cors'] }, () => ({ items: [] }))rateLimit needs the @samveys/cache plugin; the app fails at boot with SVS_E020 without it.
The middleware functions csrf(), cors() and rateLimit() are also exported for direct use in use: [...].
CSRF
State-changing requests (anything but GET, HEAD, OPTIONS) are checked in this order:
api: trueand aPOSTwith a simple content type (text/plain, urlencoded, multipart) → rejected.Sec-Fetch-Site: same-originornone→ allowed.same-siteorcross-site→ rejected unless theOriginis inallowOrigins.- No
Sec-Fetch-Site:Originmust equal the server's own origin, or be intrustedOrigins/allowOrigins. - Neither header: a valid form token is required.
Rejections are 403 with SVS_E091. Every response carries Vary: Sec-Fetch-Site, Origin.
mode: 'log' lets rejected requests through and logs them with logger.warn, for a safe roll-out.
Behind a proxy that terminates TLS the server sees http://… while browsers send https://…: list the public origin in trustedOrigins.
Form tokens
import { csrfToken } from '@samveys/security'
import { get } from '@samveys/http'
export const form = get('/form', { use: ['csrf'] }, (ctx) => {
const token = csrfToken(ctx)
return new Response(`<form method="post" action="/submit"><input type="hidden" name="_csrf" value="${token}"></form>`, {
headers: { 'content-type': 'text/html' },
})
})csrfToken() sets the __Host-svs-csrf cookie (Secure; HttpOnly; SameSite=Lax; Path=/) holding a random
value and returns its HMAC. Send the token in the x-csrf-token header or the _csrf field of a urlencoded form.
Set csrf.secret in production; without it a random key is made per process, so tokens stop working after a restart
and across several instances.
CORS
origins are matched exactly. origins: ['*'] together with credentials: true throws when security() is created.
A preflight (OPTIONS with Access-Control-Request-Method) from an allowed origin is answered 204 before routing.
Actual responses to an allowed origin get Access-Control-Allow-Origin, Vary: Origin and, with credentials,
Access-Control-Allow-Credentials. A disallowed origin gets no Access-Control-* header.
Rate limiting
rateLimit: { limit, windowMs } counts requests per key in fixed windows with the cache's atomic increment.
Over the limit the answer is 429 with SVS_E092 and Retry-After (seconds).
- The key defaults to
ctx.clientIp, so each client gets its own bucket. That is the socket peer, or the firstX-Forwarded-Forhop only whenhttp({ trustProxy })trusts that peer, so clients cannot pick their own bucket. Passkey: (ctx) => …for a user id or API token. - Use a
databaseStore()(store: 'name') to share counts between processes. - If the cache store fails, requests are let through and a warning is logged.
experimentalHeaders: trueaddsRateLimitandRateLimit-Policy. Experimental: they follow an IETF draft that may change.