@samveys/security

Versi
1.0.35 Nov 2026
Pasang
pnpm add @samveys/security

Dari packages/security/README.md di repository framework.

Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.

CSRF protection, CORS and rate limiting as HTTP middleware. No runtime dependencies.

ts
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:

ts
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:

  1. api: true and a POST with a simple content type (text/plain, urlencoded, multipart) → rejected.
  2. Sec-Fetch-Site: same-origin or none → allowed. same-site or cross-site → rejected unless the Origin is in allowOrigins.
  3. No Sec-Fetch-Site: Origin must equal the server's own origin, or be in trustedOrigins / allowOrigins.
  4. 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

ts
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 first X-Forwarded-For hop only when http({ trustProxy }) trusts that peer, so clients cannot pick their own bucket. Pass key: (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: true adds RateLimit and RateLimit-Policy. Experimental: they follow an IETF draft that may change.