Request Helpers
Defensive per-request shape readers — return sane defaults when headers / route / params are missing or garbage. Every primitive in this module sits in the framework's third validation tier: request-shape readers RETURN DEFAULTS, never throw. They run on every request, often inside middleware that has no recovery path; a thrown error here would crash the very request that triggered the read.
The contract is uniform: pass any shape (a real Node IncomingMessage, a partially-constructed test fake, undefined, a number, an attacker-supplied bag of strings) and get back a sane default. resolveRoute falls back to "/", clientIp to null, requestProtocol to "http", parseListHeader and parseQualityList to [], safeHeadersDistinct to a null-prototype empty object, extractBearer to null. Operators who want strict refusal layer their own check on the result.
The single exception is parseListHeader({ strictToken: true }), which throws on RFC 9110 §5.6.2 token grammar violations because it's used by config-time entry points (WebSocket subprotocol negotiation etc.) where bad input MUST surface at boot.
b.requestHelpers.extractActorContext(req, override?) #
Pull the 5 W's from a request for audit chain emission. The WHO/WHERE/HOW columns on _blamejs_audit_log are populated from the returned shape { ip, userAgent, sessionId, requestId, method, route, userId }. Every field is best-effort — missing or non-request inputs return an object with whatever could be inferred plus null elsewhere. The audit chain treats null as "unknown", so partial context is always safe.
Caller-supplied override (own userId, ip, …) is merged on top of the request-derived fields — explicit operator override always wins.
var req = {
ip: "203.0.113.4",
method: "POST",
url: "/api/orders?ref=abc",
headers: { "user-agent": "curl/8.7.1", "x-request-id": "req-9f2" },
user: { id: "user-42" },
};
var actor = b.requestHelpers.extractActorContext(req);
// → {
// ip: "203.0.113.4", userAgent: "curl/8.7.1",
// sessionId: null, requestId: "req-9f2",
// method: "POST", route: "/api/orders", userId: "user-42",
// }
// Override beats request-derived fields:
var ovr = b.requestHelpers.extractActorContext(req, { userId: "svc-runner" });
ovr.userId; // → "svc-runner"
b.requestHelpers.resolveActorWithOverride(callerOpts, baseOverride?) #
Convenience wrapper for primitives that accept an optional { req, context } shape and want to thread it into an audit-emit actor field. Replaces the four near-identical _actor() helpers that lived in api-key, cache, seeders, and notify before v0.4.29.
callerOpts is the operator-supplied { req?, context? } bag (typically a primitive's call-site opts). baseOverride seeds default values applied BEFORE callerOpts.context is merged, so context always wins — b.apiKey seeds { userId } here so the resolved key's owner becomes the default actor unless the operator passes their own context.userId. Returns the same shape as b.requestHelpers.extractActorContext.
var req = { ip: "198.51.100.7", method: "DELETE", url: "/v1/keys/abc" };
var actor = b.requestHelpers.resolveActorWithOverride(
{ req: req, context: { userId: "ops-admin" } },
{ userId: "key-owner-default" }
);
actor.userId; // → "ops-admin"
actor.ip; // → "198.51.100.7"
actor.method; // → "DELETE"
// Falls back to the seed when caller passes no context:
var seeded = b.requestHelpers.resolveActorWithOverride(
{ req: req }, { userId: "key-owner-default" }
);
seeded.userId; // → "key-owner-default"
b.requestHelpers.clientIp(req, opts?) #
{
trustProxy: boolean | number | function // false (default) | predicate (peer-gated) | legacy true/hop-count
}
Resolve the originating client IP from a request. Default reads only req.socket.remoteAddress — X-Forwarded-For is ignored because without a sanitizing reverse proxy it's attacker-forgeable.
For an access-control decision (allowlist, rate-limit key, IP-bound grant), pass trustProxy as a PREDICATE function(addr) => boolean naming your trusted reverse proxies. The header is then honored only when the immediate TCP peer is itself a trusted proxy, and the client is the first untrusted address walking the chain right-to-left. A direct attacker cannot forge it — this is the only peer-gated form.
The legacy trustProxy: true (leftmost XFF hop) and trustProxy: (Nth-from-rightmost) forms do NOT verify the peer: a client connecting directly can forge any value. They are safe only when an upstream you control terminates and rewrites X-Forwarded-For on every request — never for a security decision on an internet-facing listener. Prefer the predicate form. Returns null when no address can be read — never throws.
var req = {
socket: { remoteAddress: "10.0.0.1" },
headers: { "x-forwarded-for": "203.0.113.7, 10.0.0.5" },
};
b.requestHelpers.clientIp(req);
// → "10.0.0.1" (forwarded headers ignored by default)
var fromTrusted = function (a) { return a.indexOf("10.") === 0; };
b.requestHelpers.clientIp(req, { trustProxy: fromTrusted });
// → "203.0.113.7" (peer 10.0.0.1 trusted; first untrusted hop)
var forged = { socket: { remoteAddress: "198.51.100.66" },
headers: { "x-forwarded-for": "203.0.113.7" } };
b.requestHelpers.clientIp(forged, { trustProxy: fromTrusted });
// → "198.51.100.66" (peer untrusted → forged header ignored)
b.requestHelpers.clientIp(undefined);
// → null
b.requestHelpers.trustedClientIp(opts?) #
{
trustedProxies: string | string[], // CIDRs — peer-gate X-Forwarded-For
clientIpResolver: function(req): string|null, // own resolution entirely
}
Build a peer-gated client-IP resolver for an access-control decision (allowlist, rate-limit key, IP-bound grant). The bare trustProxy forms of clientIp are forgeable; this is the shape every gate shares so the trust model is identical across them. Returns { resolve(req), peerGated }: resolve reads the client IP, peerGated is true when trustedProxies or clientIpResolver was supplied — a gate uses it to refuse a bare trustProxy at construction (fail closed).
With clientIpResolver(req) the operator owns resolution entirely. With trustedProxies (CIDRs of the reverse proxies), X-Forwarded-For is honored only when the immediate peer is one of them. With neither, only the socket address is used and forwarded headers are ignored.
var tip = b.requestHelpers.trustedClientIp({ trustedProxies: ["10.0.0.0/8"] });
var ip = tip.resolve(req); // peer-gated; forged XFF from a direct caller ignored
b.requestHelpers.trustedIdentityHeaders(opts) #
{
headers: object, // { field: "Header-Name", ... } — the family to trust (required)
trustedProxies: string | string[], // CIDRs of the reverse proxies — peer-gate the family
peerTrust: function(req): boolean, // own the peer-trust decision entirely (instead of trustedProxies)
as: string, // req property to set the identity on (default: "proxyIdentity")
}
Resolve an identity-injecting reverse proxy's headers under the SAME peer-gate as trustedClientIp — the mirror of the X-Forwarded-For discipline for identity-header families (Cloudflare Access Cf-Access-*, oauth2-proxy X-Forwarded-User, Tailscale Serve Tailscale-User-*). A configured header family is trusted ONLY when the immediate socket peer is a trusted proxy; from every OTHER peer the family is defensively stripped from req.headers so downstream code cannot read a forged value. A naive trust of these headers is a full impersonation bypass — so this reuses the trustedProxies gate rather than opening a second, looser trust path.
Returns { resolve(req), middleware, headerNames, peerGated }. resolve(req) → { trusted, identity } (identity is {} unless the peer is trusted). middleware(req, res, next) sets req[as] to the identity when trusted and DELETES every family header from req.headers when not. With no trustedProxies/peerTrust the peer is never trusted (fail-closed: the family is always stripped and peerGated is false).
Header VALUES are surfaced raw — RFC 2047 name decoding and capability-JSON parsing are the consumer's job, not the trust boundary's.
var ident = b.requestHelpers.trustedIdentityHeaders({
trustedProxies: ["127.0.0.1/32"],
headers: { login: "Tailscale-User-Login", name: "Tailscale-User-Name" },
});
app.use(ident.middleware);
// req.proxyIdentity = { login, name } from the trusted sidecar; a forged
// Tailscale-User-Login from a direct client is stripped, never trusted.
b.requestHelpers.ipPrefix(ip, opts?) #
{
v4Bits: number, // IPv4 mask width in bits (default 24; valid 0..32)
v6Bits: number, // IPv6 mask width in bits (default 64; valid 0..128)
}
Mask a client IP to its subnet bucket: a /24 for IPv4 (the carrier-NAT pool stride) and a /64 for IPv6 (the customer-LAN prefix RIRs allocate, RFC 4291 §2.5.4). Returns the canonical "network/prefix" string, or "" for a non-string / empty / unparseable input. An IPv4-mapped IPv6 address (::ffff:1.2.3.4) folds to its dotted form so it buckets the same regardless of how a proxy reported it.
This is the masking the session device-fingerprint's built-in clientIpPrefix field hashes (so roaming carriers that flip the public IP within a subnet don't log a user out). Exposed so an operator who drops to a function-form fingerprint field — for a custom mask width, or to combine the prefix with other signals — reuses this exact algorithm instead of re-deriving the /24 + /64 masking (and silently diverging). Pass opts.v4Bits / opts.v6Bits to override the mask widths (e.g. a device fingerprint that buckets at /48 so a client roaming within its allocation but across a /64 doesn't drift); an out-of-range or absent value falls back to the /24 + /64 default.
b.requestHelpers.ipPrefix("203.0.113.47"); // → "203.0.113.0/24"
b.requestHelpers.ipPrefix("2001:db8::1"); // → "2001:db8:0:0/64"
b.requestHelpers.ipKey(ip, opts?) #
{
ipv6Bits: number, // IPv6 mask width in bits (default 64; valid 0..128)
}
Derive a stable rate-limit / blocklist key from a client IP: the IPv4 address verbatim (one IPv4 is one host) but the IPv6 address collapsed to its routing-significant /64 prefix. A single IPv6 end-site is allocated a whole /64 (RFC 6177 / RFC 4291 §2.5.4) and freely rotates the low 64 bits, so keying on the full 128-bit address lets one site mint unlimited fresh keys — defeating a per-IP throttle and an exact-address block. Keying on the /64 closes that while still distinguishing real end-sites. Unlike ipPrefix (which masks IPv4 to a /24 pool), this keeps IPv4 exact — a rate limiter wants per-host IPv4 granularity.
Returns the canonical key string, or "" for a non-string / empty / unparseable input (caller falls back to its own bucket). An IPv4-mapped IPv6 address (::ffff:1.2.3.4) folds to its dotted IPv4 form so a client keys the same however a proxy reported it. Pass opts.ipv6Bits to override the IPv6 mask width (default 64).
b.requestHelpers.ipKey("203.0.113.47"); // → "203.0.113.47" (exact)
b.requestHelpers.ipKey("2001:db8:1:2:dead:beef:0:1"); // → "2001:db8:1:2:0:0:0:0/64"
b.requestHelpers.trustedProtocol(opts?) #
{
trustedProxies: string | string[],
protocolResolver: function(req): "http"|"https",
}
Peer-gated companion to trustedClientIp for the request scheme. The Secure-cookie / HSTS / secure-context decisions hinge on whether a request arrived over HTTPS; behind a TLS-terminating proxy that comes from X-Forwarded-Proto, which is forgeable unless the immediate peer is a trusted proxy. Returns { resolve(req)=>"http"|"https", peerGated }. With trustedProxies (CIDRs) the header is honored only from a trusted peer; with protocolResolver(req) the operator owns the decision; with neither only the real TLS socket is consulted (forwarded headers ignored).
var tp = b.requestHelpers.trustedProtocol({ trustedProxies: ["10.0.0.0/8"] });
tp.resolve(req); // "https" only when X-Forwarded-Proto came via a trusted peer
b.requestHelpers.requestProtocol(req, opts?) #
{
trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true
}
Resolve the inbound transport scheme. Default returns "https" when req.socket.encrypted is set, otherwise "http". Behind a trusted reverse proxy that terminates TLS, pass trustProxy as a PREDICATE function(addr)=>boolean naming your proxies: X-Forwarded-Proto is then honored only when the immediate peer is a trusted proxy, so a direct caller can't forge it (use b.requestHelpers.trustedProtocol to build this). The legacy trustProxy: true reads the leftmost hop without checking the peer — forgeable, safe only behind an edge that rewrites the header. Always returns a string; on bad input falls back to "http".
var req = { socket: { encrypted: true } };
b.requestHelpers.requestProtocol(req);
// → "https"
var behindProxy = {
socket: { encrypted: false },
headers: { "x-forwarded-proto": "https, http" },
};
b.requestHelpers.requestProtocol(behindProxy, { trustProxy: true });
// → "https"
b.requestHelpers.requestProtocol(undefined);
// → "http"
b.requestHelpers.trustedHost(opts?) #
{
trustedProxies: string | string[],
hostResolver: function(req): string|null,
}
Peer-gated companion to trustedProtocol for the request authority (host). Reconstructing the absolute request URL — the DPoP htu, an origin/issuer string, a redirect base — depends on the host the client addressed; behind a proxy that comes from X-Forwarded-Host, which is forgeable unless the immediate peer is a trusted proxy. Returns { resolve(req)=>string|null, peerGated }. With trustedProxies (CIDRs) X-Forwarded-Host is honored only from a trusted peer; with hostResolver(req) the operator owns it; with neither only the request's own Host header is used (forwarded host ignored).
var th = b.requestHelpers.trustedHost({ trustedProxies: ["10.0.0.0/8"] });
th.resolve(req); // X-Forwarded-Host only when it came via a trusted peer
b.requestHelpers.requestHost(req, opts?) #
{
trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true
}
Resolve the inbound authority (host[:port]). Default returns the request's own Host header. Behind a trusted reverse proxy that rewrites the host, pass trustProxy as a PREDICATE function(addr)=>boolean (build it via b.requestHelpers.trustedHost): X-Forwarded-Host is then honored only when the immediate peer is a trusted proxy, so a direct caller can't forge it. The legacy trustProxy: true reads the leftmost forwarded hop without checking the peer — forgeable. Returns the host string, or null when absent.
b.requestHelpers.requestHost({ headers: { host: "app.example.com" } });
// → "app.example.com"
b.requestHelpers.parseListHeader(value, opts?) #
{
lowercase: boolean // lowercase every token before returning
strictToken: boolean // throw on non-RFC 9110 token entries
}
Split a comma-separated header / opt value into a list of trimmed non-empty tokens. Replaces the String(x).split(",").map(s => s.trim()).filter(Boolean) chain that was duplicated across cors / compression / scheduler / webhook / websocket / db-schema / cli before v0.5.17.
Tolerant read: non-string input returns [] — these are read from request headers that the network might omit. Callers needing stricter checks layer their own validation on the result. The strictToken opt is the one exception — it throws on RFC 9110 §5.6.2 token-grammar violations, used by config-time entry points (WebSocket subprotocol negotiation etc.) where bad input MUST surface at boot.
b.requestHelpers.parseListHeader("a, b , ,c");
// → ["a", "b", "c"]
b.requestHelpers.parseListHeader("Foo, Bar", { lowercase: true });
// → ["foo", "bar"]
b.requestHelpers.parseListHeader(undefined);
// → []
try {
b.requestHelpers.parseListHeader("chat, bad token", { strictToken: true });
} catch (err) {
err.message;
// → "parseListHeader: 'bad token' is not a valid RFC 9110 token"
}
b.requestHelpers.appendVary(res, value) #
Append a token to a Vary response header without dropping prior values (compression middleware sets Vary: Accept- Encoding, an auth helper might set Vary: Authorization, etc.). Idempotent — re-adding an existing token (case-insensitive) is a no-op. Silently no-ops when res doesn't expose getHeader/setHeader so misuse during testing or in non-HTTP contexts never throws.
var headers = { Vary: "Accept-Encoding" };
var res = {
getHeader: function (n) { return headers[n]; },
setHeader: function (n, v) { headers[n] = v; },
};
b.requestHelpers.appendVary(res, "Authorization");
headers.Vary; // → "Accept-Encoding, Authorization"
// Idempotent — re-adding is a no-op:
b.requestHelpers.appendVary(res, "accept-encoding");
headers.Vary; // → "Accept-Encoding, Authorization"
b.requestHelpers.resolveRoute(req) #
Resolve the route pattern for a request. Prefers req.routePattern (set by b.router during dispatch — a low-cardinality template like /users/:id rather than the concrete URL), and falls back to req.url with the query string stripped. Returns "/" on missing or non-string input so audit-chain rows / metrics labels never carry null.
b.requestHelpers.resolveRoute({ routePattern: "/users/:id", url: "/users/42" });
// → "/users/:id"
b.requestHelpers.resolveRoute({ url: "/orders?ref=abc" });
// → "/orders"
b.requestHelpers.resolveRoute({});
// → "/"
b.requestHelpers.resolveRoute(undefined);
// → "/"
b.requestHelpers.makeSkipMatcher(opts, label) #
{
skipPaths: Array, // string = segment-boundary match; RegExp = .test(path)
exact: boolean, // string entries match whole-path only (no descendant). default false
skip: function, // (req) => boolean, optional route-aware predicate
}
Build a (req) => boolean path-match predicate shared by the state-change guards (csrfProtect / fetchMetadata / botGuard / rateLimit) AND the route-exemption / mount checks in auth.accessLock, middleware.ageGate, middleware.botDisclose, and middleware.dailyByteQuota — so a single route can be exempted (or a middleware mounted on a path subset) without each caller re-rolling the loop. opts.skipPaths entries are validated at build time — each must be a string or a RegExp — so an operator typo dies at boot, not on the first request; the optional opts.skip(req) predicate is validated the same way.
A STRING entry matches on a SEGMENT BOUNDARY, not a raw prefix: "/api" matches /api and /api/x but NOT /apixyz — a raw startsWith would skip the guard on an unintended sibling path (a guard-bypass class). An entry that already ends in / is itself a segment prefix. Pass exact: true to require a whole-path match (no descendant). A RegExp entry uses .test(path). The tested path is req.pathname || req.url || req.originalUrl || "/" with the query string stripped (matching is on the path, never the query). A skip predicate that throws is treated as "do not skip", so a buggy exemption can only keep the guard ON, never silently bypass it.
var shouldSkip = b.requestHelpers.makeSkipMatcher(
{ skipPaths: ["/healthz", /^\/webhooks\//] }, "middleware.csrfProtect");
if (shouldSkip(req)) return next();
b.requestHelpers.captureResponseStatus(res, onEnd) #
Wrap a response so observability / audit middleware can learn the final status code at end-of-stream. Patches res.writeHead and res.end; when res.end() fires, invokes onEnd(status) with the value passed to writeHead (preferred) or res.statusCode (fallback) or 200 (default). Errors thrown by the onEnd callback are swallowed — instrumentation must never break the response. Returns the original end function so callers that want to compose can keep a reference. Throws when either argument is missing — these are config-time wiring errors, surfaced loudly.
var headers = {};
var sent = null;
var res = {
statusCode: 200,
writeHead: function (s) { this.statusCode = s; sent = "head"; },
end: function () { sent = (sent || "end"); },
};
b.requestHelpers.captureResponseStatus(res, function (status) {
console.log("final status:", status);
});
res.writeHead(204);
res.end();
// → "final status: 204"
b.requestHelpers.parseQualityList(headerValue, opts?) #
{
caseSensitive: boolean // preserve original case in `value`
}
RFC 9110 §12.5 Accept-* header parser. Returns [{ value, q }] sorted by q descending. Used by content negotiation (Accept-Encoding, Accept-Language, Accept, …). Each Accept-* middleware previously carried its own copy of this loop; centralizing it keeps the q-value semantics consistent — q=0 is explicit exclusion, q is clamped to [0, 1], missing q defaults to 1. value is lowercased by default; pass caseSensitive: true to preserve case (BCP 47 language tags may need it). Bad input (non-string, empty) returns [] — absent Accept-* means "accept anything" but the right default differs by caller, so it's the caller's call to layer.
b.requestHelpers.parseQualityList("br;q=1.0, gzip;q=0.5, *;q=0");
// → [
// { value: "br", q: 1 },
// { value: "gzip", q: 0.5 },
// { value: "*", q: 0 },
// ]
b.requestHelpers.parseQualityList("en-US,en;q=0.9", { caseSensitive: true });
// → [
// { value: "en-US", q: 1 },
// { value: "en", q: 0.9 },
// ]
b.requestHelpers.parseQualityList(undefined);
// → []
b.requestHelpers.extractBearer(req) #
RFC 6750 §2.1 inbound bearer-token extractor. Reads the Authorization request header, validates the case-insensitive Bearer scheme, and returns the trimmed token string. Returns null on any malformed shape — defensive by design, since this runs on every authenticated request and a throw here would crash the request itself. Callers that require a token throw their own authentication-shape error when null surfaces.
Refusal cases (all return null): missing Authorization header, non-string value, multiple Authorization headers (CWE-345 trust mismatch), scheme other than Bearer (case-insensitive), missing space + token after the scheme, embedded CR / LF / NUL / Tab / other ASCII control bytes (CRLF-injection defense — the token transits log lines + audit metadata), embedded spaces inside the token. Token shape past the scheme word is NOT validated against the RFC 6750 b64token grammar here — b.guardJwt / b.middleware.bearerAuth own format-specific checks.
The outbound counterpart is b.authHeader.bearer(token), which constructs Authorization: Bearer for outgoing requests.
var req = { headers: { authorization: "Bearer eyJhbGciOiJFUzI1NiJ9.payload.sig" } };
b.requestHelpers.extractBearer(req);
// → "eyJhbGciOiJFUzI1NiJ9.payload.sig"
// Case-insensitive scheme:
b.requestHelpers.extractBearer({ headers: { authorization: "bearer abc123" } });
// → "abc123"
// Refusals return null:
b.requestHelpers.extractBearer({ headers: { authorization: "Basic dXNlcjpwYXNz" } });
// → null
b.requestHelpers.extractBearer({ headers: { authorization: "Bearer abc, def" } });
// → null
b.requestHelpers.extractBearer({});
// → null
b.requestHelpers.safeHeadersDistinct(req) #
Defensive replacement for req.headersDistinct. Node CVE 2026-21710: headersDistinct is implemented as a getter, and reading __proto__ on the underlying header bag throws synchronously inside the getter. A request bearing a __proto__: header therefore escapes any handler-level try/catch — the throw happens at property-access time, not later. This helper computes the same shape (lowercased header-name to array of values) directly from req.rawHeaders, skipping __proto__ / constructor / prototype keys, and returns a null-prototype object so iteration never inherits Object.prototype properties. Always returns an object — never throws.
var req = {
rawHeaders: [
"Set-Cookie", "a=1",
"Set-Cookie", "b=2",
"X-Trace", "abc",
"__proto__", "polluted",
],
};
var headers = b.requestHelpers.safeHeadersDistinct(req);
headers["set-cookie"]; // → ["a=1", "b=2"]
headers["x-trace"]; // → ["abc"]
headers["__proto__"]; // → undefined (prototype-pollution key dropped)
b.requestHelpers.safeHeadersDistinct(undefined);
// → {} (null-prototype empty object)
b.requestHelpers.makeResourceAuditEmitter(sink, resourceKind, idFor?) #
Build a drop-silent audit emitter (action, key, outcome, metadata, req) for a request-scoped resource. The emitter is disabled when sink is falsy (the operator supplied no audit instance), so a primitive can wire it unconditionally and let the operator opt in by passing opts.audit. Each event carries resource: { kind, id } and, when a request is passed, the actor extracted from it (extractActorContext); a throwing sink is swallowed so audit emission can never break the request the event describes.
The auth lockout / bot-challenge and session device-binding primitives emit this exact shape, varying only in the resource kind and how the id derives from the per-call key. idFor(key) maps the per-call key to the resource id (default: the key verbatim); pass it when the id needs a prefix or transform.
var emitAudit = b.requestHelpers.makeResourceAuditEmitter(
opts.audit, "auth.lockout", function (key) { return ns + ":" + key; });
emitAudit("locked", key, "denied", { attempts: n }, req);
Last updated 2026-08-08T16:39:49.652Z by seeder.