@setu-ts/http-security-plugin

HTTP transport security plugin for Setu-TS. Provides five independent, composable middleware concerns: CORS, security response headers, CSRF, request-size limiting, and IP security.

Zero npm dependencies — depends only on @setu-ts/common and @setu-ts/kernel.

Features

  • Security Headers — ON by default with a secure baseline (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Strict-Transport-Security). Optional CSP and Permissions-Policy.
  • CORS — Full origin matching (string, allowlist, boolean, function), preflight short-circuit (204), credentials support, and Vary header management.
  • CSRF — Stateless Origin/Referer validation for unsafe HTTP methods. The request’s own origin is always implicitly trusted. Optional custom-header requirement for defense-in-depth.
  • Request Size — Enforces Content-Length against a configurable limit (default 1 MiB) with 413 short-circuit before body reading.
  • IP Security — Resolves client IP from proxy headers (when behind a trusted reverse proxy) or socket IP, publishing to ctx.state.get(CLIENT_IP_STATE_KEY).

Installation

npm install @setu-ts/http-security-plugin
# Deno: deno add npm:@setu-ts/http-security-plugin

Usage

Plugin Registration

import { HttpSecurityPlugin } from '@setu-ts/http-security-plugin';
import { createApplication } from '@setu-ts/kernel';

const app = createApplication();

app.register(
  HttpSecurityPlugin({
    // CORS — opt-in via presence of `cors` block
    cors: {
      origin: 'https://example.com',
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE'],
      // Omit `allowedHeaders` and an allowed origin's preflight echoes whatever
      // the browser asked for. List them here to allow only those.
      allowedHeaders: ['content-type', 'authorization', 'idempotency-key', 'x-csrf-token'],
      exposedHeaders: ['idempotent-replayed'],
      maxAge: 86400,
    },
    // Security headers — ON by default; customize here
    headers: {
      strictTransportSecurity: {
        maxAge: 63072000,
        includeSubDomains: true,
        preload: true,
      },
    },
    // CSRF — opt-in
    csrf: {
      trustedOrigins: ['https://example.com'],
      customHeader: 'X-CSRF-Token',
    },
    // Request size — opt-in
    requestSize: {
      maxBodySize: 2_097_152, // 2 MiB
    },
    // IP security — opt-in
    ipSecurity: {
      trustProxy: true,
      ipHeader: 'X-Forwarded-For',
    },
  }),
);

A keyed SDK request carries a non-safelisted header, so the preflight must allow idempotency-key. The page can read Idempotent-Replayed only when it is exposed through exposedHeaders.

Per-Route Middleware

Each middleware is also exported as a standalone factory for per-route use:

import {
  corsMiddleware,
  csrfMiddleware,
  ipSecurityMiddleware,
  requestSizeMiddleware,
  securityHeadersMiddleware,
} from '@setu-ts/http-security-plugin';

app.router.post('/api/data', {
  middleware: [
    corsMiddleware({ origin: 'https://other.com' }),
    requestSizeMiddleware({ maxBodySize: 512_000 }),
  ],
  handler: (ctx) => ctx.response.json({ received: true }),
});

Middleware Priorities

ConcernPriorityDescription
IP Security120Resolves client IP early for downstream stages
Request Size180Rejects oversized bodies before other processing
CORS200Standard CORS handling + preflight short-circuit
Security Headers250Sets response headers before handler execution
CSRF270Validates CSRF after headers, before auth (300)

Defaults

  • Security headers: ON by default (even when headers option is omitted)
  • CORS / CSRF / Request-size / IP-security: Opt-in (register only when the option block is present)
  • Each opt-in concern defaults to enabled: true (so {} enables it, { enabled: false } disables)
  • CORS origin defaults to empty allowlist (deny all cross-origin) when enabled

Options Reference

HttpSecurityPluginOptions

OptionTypeDefaultDescription
cors?CorsOptions—Presence enables CORS
headers?SecurityHeadersOptionsdefaultOmitted → defaults ON
csrf?CsrfOptions—Presence enables CSRF
requestSize?RequestSizeOptions—Presence enables size limiting
ipSecurity?IpSecurityOptions—Presence enables IP resolution

CorsOptions

OptionTypeDefaultDescription
enabled?booleantrueToggle CORS
origin?boolean | string | string[] | fn[] (deny all)Origin matching configuration
credentials?booleanfalseAllow credentials
methods?string[]all standard methodsAllowed methods for preflight
allowedHeaders?string[]echo the requestAllowed headers for preflight
exposedHeaders?string[][]Exposed response headers
maxAge?number—Preflight cache max age (seconds)

allowedHeaders omitted vs []. Omitting it echoes the preflight’s own Access-Control-Request-Headers back and adds Vary: Access-Control-Request-Headers; an explicit list allows only those headers, and an explicit [] allows none.

Echoing is the default because the alternative was incoherent: methods defaults to every standard verb, so the preflight advertised POST/PUT/PATCH/DELETE and then refused content-type — the one header a JSON body needs — and every browser blocked every JSON request. It does not widen the security boundary: the ORIGIN allowlist is what decides, it is unchanged, and a caller reaching this branch has already been admitted while asking for a header it is already sending. A denied origin echoes nothing.

CsrfOptions

The implicitly trusted own origins are the request URL origin, the public origin from requestOriginOf(ctx), and, only when the browser sends Sec-Fetch-Site: same-origin, the validated leftmost X-Forwarded-Proto/X-Forwarded-Host candidate (falling back to the URL host). Thus csrf: {} accepts same-origin browser mutations behind a standard TLS-terminating proxy without extra configuration. The Sec-Fetch-Site gate is load-bearing: X-Forwarded-* are not forbidden request headers, and the CORS middleware echoes requested headers by default, so without it a script on any credentialed CORS origin could name its own host and pass. Browsers that do not send Sec-Fetch-Site fall back to the URL and published origins. A request whose Sec-Fetch-Site is present and anything but same-origin is checked against the URL origin and trustedOrigins only: a browser cannot send the target’s real public origin cross-site, and a trusted proxy that passes a client’s X-Forwarded-Host through would otherwise publish a host the attacker then matches. This comparison grants no proxy trust to IP resolution, cookies or public URLs.

Origin: null is accepted only with Sec-Fetch-Site: same-origin (M119c, V10-1). Under this plugin’s default Referrer-Policy: no-referrer, a browser sends Origin: null on a same-origin HTML form POST, so before 0.11.0 every such form answered 403 unless referrerPolicy was changed. The browser also sends Sec-Fetch-Site: same-origin, which page script cannot set, so that pair is accepted — the Fetch Metadata rule Go’s CrossOriginProtection and Hono’s secFetchSite use. A null origin with any other Sec-Fetch-Site value, or without the header (older browsers), is still refused: a sandboxed frame, a data: or file: page and a cross-site redirect all report cross-site. trustedOrigins: ['null'] is never implied. To refuse every null origin as before 0.11.0, set csrf: { acceptNullOriginSameOrigin: false } (and send a real origin with headers: { referrerPolicy: 'same-origin' }).

OptionTypeDefaultDescription
enabled?booleantrueToggle CSRF
trustedOrigins?string[][]Additional trusted origins beyond self-origin
customHeader?string—Required custom header for unsafe methods
exclude?PathPattern[][]Paths exempt from the CSRF check, checked before the method test
acceptNullOriginSameOrigin?booleantrueAccept Origin: null with Sec-Fetch-Site: same-origin; false refuses it (0.11.0)

exclude is for a protocol endpoint, not an application route. An excluded path is checked FIRST — before the method test — and is never inspected. Use it only where the endpoint’s own defences make the CSRF check redundant: the SAML ACS, whose signed assertion, single-use request and browser-binding cookie are the check this middleware would otherwise add. Do not use it to bypass CSRF for a form route.

exclude exists because an origin allowlist cannot admit an IdP that serves Referrer-Policy: no-referrer (Keycloak does): the browser posts the ACS with Origin: null, and trustedOrigins: ['null'] admits every opaque-origin POST on every route. The documented SAML recipe is exclude: ['/auth/<provider>/acs'] here AND in the session plugin’s form CSRF (CsrfFormOptions.exclude); keep trustedOrigins for an IdP that sends a real origin.

RequestSizeOptions

OptionTypeDefaultDescription
enabled?booleantrueToggle size limiting
maxBodySize?number1_048_576Maximum declared body size in bytes (1 MiB)

maxBodySize bounds a declared Content-Length and nothing else — a chunked request declares none, and the body read happens inside the handler, after this middleware has returned. The bound no client can disable lives where the body is consumed: RuntimePlugin({ maxBodyBytes }). Set both, and set maxBodyBytes to the same value or higher. See “Bounding the request body” in PUBLIC_API.md.

IpSecurityOptions

OptionTypeDefaultDescription
enabled?booleantrueToggle IP resolution
trustProxy?booleanfalseRead IP from proxy header
ipHeader?stringX-Forwarded-ForProxy header name
forwardedHeader?'x-forwarded' | 'forwarded''x-forwarded'Read exactly one header family; 'forwarded' conflicts with ipHeader
trustedProxies?readonly string[]—Proxy addresses or IPv4 CIDR blocks; resolves rightmost-untrusted
proxyHops?number—The nth entry from the right, when proxies have no fixed address

trustedProxies and proxyHops are mutually exclusive — supplying both throws at middleware construction. So does a proxyHops that is not a non-negative integer (0 is the rightmost entry): without that guard a negative, a fraction, or the NaN that Number() yields for an unset environment variable resolves undefined for every caller, which silently degrades a rate limiter keyed on the client IP to one shared bucket. A trustedProxies entry whose CIDR width is not plain digits is refused the same way: Number('') is 0, so '10.0.0.1/' would otherwise compile to a /0 matcher that trusts every IPv4 address. Omit the slash to compare an entry literally, which is already how a bare address and an IPv6 CIDR are handled.

Node, Deno and Bun populate IRequest.ip with the socket peer, which behind a proxy is the proxy. Without trustProxy, that peer is published as CLIENT_IP_STATE_KEY. With trustedProxies, the peer is appended to the chain before walking right to left: a direct client cannot choose its identity by forging X-Forwarded-For. Every published address is normalized: an IPv4-mapped peer or hop such as ::ffff:10.1.2.3 (or its hexadecimal spelling ::ffff:a01:203) becomes 10.1.2.3, and an IPv6 address is published in canonical lowercase compressed form (2001:DB8:0::1 becomes 2001:db8::1). Native IPv6 proxies must be listed literally; a literal is normalized the same way before comparison. IPv6 CIDR expansion is not supported. proxyHops and the leftmost default keep their header-only selection and do not check the peer; prefer trustedProxies for stable proxy addresses.

Every examined X-Forwarded-For or custom ipHeader entry must be an IP address. Numeric ports are stripped from IPv4 and bracketed IPv6 entries; an unbracketed IPv6 entry never carries a port. An invalid entry is a boundary: resolution stops with CLIENT_IP_STATE_KEY set to undefined, without falling back to the socket peer, and the origin stays the URL origin.

forwardedHeader: 'forwarded' reads only RFC 7239 Forwarded, including quoted IPv6 addresses and ports, and ignores every X-Forwarded-* header. The default reads X-Forwarded-* only. A malformed Forwarded element is a boundary, not a void: the elements to its right were written by later hops and are still read, while the trust walk stops at the boundary and never continues left of it, so it cannot cross into client-written text. Everything left of the boundary is one unreadable entry. An oversized field (over 8 KiB or 64 elements) is read from the right within those bounds instead of voiding the field, so a trusted proxy that appends its element still resolves the client behind a padded or malformed prefix. A trusted proxy that writes a comma inside a quoted value to the right of malformed input loses that element to the boundary and resolves no client (fail closed). A direct client stays identified by its own address, and a boundary with only trusted entries to its right yields no client rather than the proxy’s own address. Likewise in the default family a proxyHops header too short for the declared chain, or a chain of only trusted proxies, yields no client rather than the proxy’s own address, and in trustedProxies mode so does a request from a trusted proxy carrying no forwarding header (or an empty one) in either family. This describes CLIENT_IP_STATE_KEY only: defaultRateLimitKey in @setu-ts/auth-plugin then falls back to IRequest.ip, the socket peer, so such requests share the proxy’s rate-limit bucket. unknown and obfuscated for values stop the trust walk without a client.

IP security also publishes the public origin for requestOriginOf(ctx) and publicRequestUrl(ctx) from @setu-ts/common. Forwarded origin components are believed only in trustedProxies mode, when the request has a socket peer, that peer is trusted, and the selected client came from the forwarded chain. Leftmost, proxyHops and custom-ipHeader modes without trustedProxies, and any request with no peer (Workers, plain fetch), never publish a forwarded origin: nothing checked who wrote the headers. In the default family the leftmost X-Forwarded-Proto and X-Forwarded-Host are used, so the trusted proxy must overwrite both rather than pass a client’s copy through (nginx forwards a client’s X-Forwarded-Host unless told otherwise); with Forwarded, proto and host come from the selected client element. Only http/https protocols, and hosts that are DNS names (letters, digits, inner hyphens, no trailing dot) or bracketed IPv6 literals with an optional numeric port (the scheme default port is dropped), are accepted; rejected or absent components fall back to the request URL. Session secure: 'auto' and React Router loaders share this decision.

Workers and plain adapter.fetch() calls have no socket peer. On Cloudflare Workers use ipSecurity: { trustProxy: true, ipHeader: 'CF-Connecting-IP' }; Cloudflare overwrites that header. It vouches for the client address only: the public origin there comes from the request URL, which Workers already receives as the public one. Build security-sensitive links, such as password resets, from a configured base URL.

SecurityHeadersOptions

OptionTypeDefaultDescription
enabled?booleantrueToggle all security headers
contentSecurityPolicy?ContentSecurityPolicyOptions | false— (none)CSP configuration (no default)
strictTransportSecurity?StrictTransportSecurityOptions | falsemax-age=31536000; includeSubDomainsHSTS configuration
xFrameOptions?string | falseDENYX-Frame-Options value
xContentTypeOptions?string | falsenosniffX-Content-Type-Options value
referrerPolicy?string | falseno-referrerReferrer-Policy value
permissionsPolicy?string | false— (none)Permissions-Policy value

Security Considerations

  • IP Security: trustProxy: true on its own trusts the header’s leftmost entry, which is safe only behind a proxy that OVERWRITES it. The standard nginx idiom (proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for) appends, so a request arriving with a forged X-Forwarded-For: 7.7.7.7 reaches the application as 7.7.7.7, 198.51.100.9 and the leftmost entry is the value the caller chose — which is then what defaultRateLimitKey counts against. Behind an appending proxy, set trustedProxies (or proxyHops) so the client is resolved from the right.
  • CSRF: The stateless Origin/Referer check is the OWASP-recommended stateless CSRF defense. Non-browser clients (which send neither header) pass through by design. Use customHeader for defense-in-depth on API-style clients.
  • Security Headers: Use headers: { enabled: false } to disable the entire set, or per-header false to omit individual headers that conflict with your existing configuration.

No Capability Token

This plugin is middleware-only — it registers no service and no capability token. Each middleware is added to the global pipeline via ctx.middleware.add(...) and is also available as a standalone factory for per-route use. This follows the same pattern as rateLimitMiddleware from the auth-plugin.

Exports

ExportKind
corsMiddlewarefunction
csrfMiddlewarefunction
HttpSecurityPluginfunction
ipSecurityMiddlewarefunction
requestSizeMiddlewarefunction
securityHeadersMiddlewarefunction
ContentSecurityPolicyOptionsinterface
CorsOptionsinterface
CsrfOptionsinterface
HttpSecurityPluginOptionsinterface
IpSecurityOptionsinterface
RequestSizeOptionsinterface
SecurityHeadersOptionsinterface
StrictTransportSecurityOptionsinterface
CorsOriginMatchertype

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.

Full API

Every export and option is documented in PUBLIC_API.md.

API reference

HttpSecurityPlugin() (@setu-ts/http-security-plugin)

Provides HTTP transport security as a middleware-only plugin: CORS, security response headers, CSRF (stateless Origin/Referer validation), request-size limiting, and IP resolution. Registers no capability token and no service — each concern is registered as global middleware via ctx.middleware.add(...) and also exported as a standalone factory for per-route use.

Defaults: Security headers are ON by default; CORS, CSRF, request-size, and IP-security are opt-in via their option blocks. Each concern is secure-by-default when enabled.

Registration

import { HttpSecurityPlugin } from '@setu-ts/http-security-plugin';

app.register(
  HttpSecurityPlugin({
    cors: { origin: 'https://example.com', credentials: true },
    csrf: { trustedOrigins: ['https://example.com'] },
    requestSize: { maxBodySize: 2_097_152 },
    ipSecurity: { trustProxy: true },
  }),
);

Exports

ExportDescription
HttpSecurityPluginPlugin factory
corsMiddlewareCORS middleware factory
securityHeadersMiddlewareSecurity headers middleware factory
csrfMiddlewareCSRF middleware factory
requestSizeMiddlewareRequest-size middleware factory
ipSecurityMiddlewareIP security middleware factory
HttpSecurityPluginOptionsPlugin factory options (type)
CorsOptionsCORS middleware options (type)
CorsOriginMatcherDynamic origin matcher function (type)
SecurityHeadersOptionsSecurity headers options (type)
ContentSecurityPolicyOptionsCSP directive options (type)
StrictTransportSecurityOptionsHSTS options (type)
CsrfOptionsCSRF middleware options (type)
RequestSizeOptionsRequest-size options (type)
IpSecurityOptionsIP security options (type)

Options

OptionTypeDefaultDescription
cors?CorsOptions—Presence enables CORS (priority 200).
headers?SecurityHeadersOptionsdefault secure setOmitted → defaults ON. { enabled: false } → off.
csrf?CsrfOptions—Presence enables CSRF (priority 270).
requestSize?RequestSizeOptions—Presence enables size limiting (priority 180).
ipSecurity?IpSecurityOptions—Presence enables IP resolution (priority 120).

Per-concern Behavior

CORS (corsMiddleware)

Origin matching via origin (boolean/string/array/function). Preflight (OPTIONS + Origin + Access-Control-Request-Method) → 204 short-circuit with Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and (when configured) Access-Control-Max-Age. Credentials reflect specific origin (never *). Non-preflight disallowed origins call next() without CORS headers (browser enforces block).

Access-Control-Allow-Headers follows allowedHeaders, and omitting it is NOT the same as passing []. Omitted, an allowed origin’s preflight is answered by echoing that request’s own Access-Control-Request-Headers, and the response also carries Vary: Access-Control-Request-Headers — mandatory rather than cosmetic, since the answer now depends on a request header and a shared cache would otherwise serve one caller’s preflight to a caller asking for different headers. An explicit list allows exactly those headers; an explicit [] allows none. A denied origin echoes nothing. Echoing is the default because the previous empty-list default advertised every standard method and then refused content-type, so every browser blocked every JSON request; it does not widen the boundary, which the origin allowlist alone decides.

Vary: Origin is appended to every response for a request carrying an Origin header — including a denied one — so a shared cache cannot serve an allowed origin’s response to a denied origin or the reverse.

origin: true (reflect any origin) combined with credentials: true throws at construction: reflecting an arbitrary origin while allowing credentials lets any site the user visits read credentialed responses. List the origins, or pass a CorsOriginMatcher.

Security Headers (securityHeadersMiddleware)

Sets headers before next() so they persist through handler and downstream short-circuits. Default set: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Strict-Transport-Security: max-age=31536000; includeSubDomains. CSP and Permissions-Policy have no default (explicitly configure to enable). Per-header false omits that header.

CSRF (csrfMiddleware)

Stateless Origin/Referer validation for unsafe methods (POST, PUT, PATCH, DELETE). The request’s URL origin, requestOriginOf(ctx).origin, and — only when the request carries Sec-Fetch-Site: same-origin — the validated leftmost X-Forwarded-Proto/X-Forwarded-Host candidate (URL host when absent) are implicitly trusted. The gate is required: X-Forwarded-* are not forbidden request headers and the CORS middleware echoes requested headers by default, so a script on a credentialed CORS origin could otherwise name its own host; scripts cannot set Sec-Fetch-Site. A request whose Sec-Fetch-Site is present and anything but same-origin is checked against the URL origin and trustedOrigins only: a browser cannot send the target’s real public origin cross-site, and a trusted proxy that passes a client’s X-Forwarded-Host through would otherwise publish a host the attacker then matches. It does not grant proxy trust to other consumers. Since 0.11.0 (M119c) Origin: null passes when the request also carries Sec-Fetch-Site: same-origin — the pair a browser sends for a same-origin form POST under Referrer-Policy: no-referrer; acceptNullOriginSameOrigin?: boolean (default true; a non-boolean throws TypeError when the middleware is built) set to false restores the earlier refusal of every null origin. trustedOrigins adds further allowed origins. Both headers absent → pass through (non-browser clients). Optional customHeader requires that header on unsafe methods (403 when absent). exclude lists paths (exact string or RegExp) that skip the check entirely — checked before the method test, so an excluded path is never inspected. It is for a protocol endpoint whose own defences make the check redundant (the SAML ACS: its signed assertion, single-use request and binding cookie), not for an application form route. An origin allowlist cannot admit an IdP that serves Referrer-Policy: no-referrer (Keycloak does) — the browser posts with Origin: null, and trustedOrigins: ['null'] admits every opaque-origin POST on every route — so the documented SAML recipe is exclude: ['/auth/<provider>/acs'] here and in the session plugin’s form CSRF.

Request Size (requestSizeMiddleware)

Checks Content-Length against maxBodySize (default 1 MiB). Over limit → 413 short-circuit without reading body. Absent or malformed Content-Length → pass through.

IP Security (ipSecurityMiddleware)

Resolves client IP and publishes to ctx.state.set(CLIENT_IP_STATE_KEY, ip). With trustProxy: false (default), it uses the socket peer. With true, ipHeader defaults to X-Forwarded-For. trustedProxies walks right to left starting at the socket peer when available; an untrusted peer is the client and forwarded entries are ignored. With no peer, it walks the header alone. IPv4 CIDR is expanded; native IPv6 proxy addresses are compared literally. Every published address is normalized: an IPv4-mapped address becomes dotted IPv4, and an IPv6 address is published in canonical lowercase compressed form; a trusted literal is normalized the same way before comparison. proxyHops and the leftmost default retain their header-only selection and do not check the peer. Prefer trustedProxies for stable proxy addresses. trustedProxies and proxyHops are mutually exclusive; proxyHops must be a non-negative safe integer. Never short-circuits.

IpSecurityOptions.forwardedHeader?: 'x-forwarded' | 'forwarded' defaults to 'x-forwarded'. 'forwarded' reads only RFC 7239 Forwarded and conflicts with ipHeader; invalid values and the conflict throw at construction. Its parser handles quoted strings, escapes, IPv6 and IPv4 ports. A malformed element is a boundary, not a void: the elements to its right were written by later hops and are still read, while the trust walk stops at the boundary and never continues left of it; everything left of the boundary is one unreadable entry. An oversized field (over 8 KiB or 64 elements) is read from the right within those bounds instead of voiding the field, so a trusted proxy that appends its element still resolves the client behind a padded or malformed prefix. A trusted proxy that writes a comma inside a quoted value to the right of malformed input loses that element to the boundary and resolves no client (fail closed). A direct client keeps its own address, and a boundary with only trusted entries to its right yields no client rather than the proxy’s own address. In the default family a proxyHops header too short for the chain, or a chain of only trusted proxies, also resolves undefined, never the socket peer. Unknown or obfuscated hops cannot match a trusted proxy and are never returned as clients. The two header families are never merged.

The middleware publishes { protocol, host } under REQUEST_ORIGIN_STATE_KEY. Forwarded values are believed only with trustedProxies, a socket peer present and trusted, and the selected client from the forwarded chain; leftmost, proxyHops and any request without a peer publish the URL origin. The values are the leftmost X-Forwarded-Proto and X-Forwarded-Host for the default family (the trusted proxy must overwrite both), or proto and host from the selected Forwarded element. Protocol must be http or https (case-insensitive); host must be a DNS name (letters, digits, inner hyphens, no trailing dot) or a bracketed IPv6 literal, with an optional numeric port (the scheme default port is dropped, as a browser Origin never carries it), at most 259 characters, and must round-trip through new URL('http://' + host) unchanged except for case. Rejected or absent components fall back to the URL. requestOriginOf(ctx) and publicRequestUrl(ctx) in common expose that decision to session secure: 'auto', CSRF and the React Router bridge. Security-sensitive links should use a configured base URL. Workers has no peer; use trustProxy: true with ipHeader: 'CF-Connecting-IP', which Cloudflare overwrites; that header vouches for the client address only, and the public origin stays the request URL.


Agent card · TypeDoc API reference