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",
routePattern: "/api/orders",
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",
// }
// Without a matched route the actor names no target, since the path
// would carry whatever the client put in it:
b.requestHelpers.extractActorContext({ url: "/reset/CAP" }).route;
// → "(unresolved)"
// Override beats request-derived fields:
var ovr = b.requestHelpers.extractActorContext(req, { userId: "svc-runner" });
ovr.userId; // → "svc-runner"
b.requestHelpers.actorIdentityFields(actor) #
The identity fields an actor carries, in the order b.requestHelpers.ACTOR_IDENTITY_FIELDS lists them, as { field, value } entries. A field is present when the actor owns it and its value is a non-empty string or a finite number.
A primitive deciding what names a principal reads this rather than indexing the field list, because a field can be nested: claims.sub is the subject of the payload b.auth.jwt.verify and its siblings answer with, and actor["claims.sub"] is not it. Every step is an own-property read, so a prototype cannot supply an identity.
var b = require("blamejs");
b.requestHelpers.actorIdentityFields({ id: "u-1", email: "a@b.c" });
// → [ { field: "id", value: "u-1" }, { field: "email", value: "a@b.c" } ]
b.requestHelpers.actorIdentityFields({ claims: { sub: "alice" } });
// → [ { field: "claims.sub", value: "alice" } ]
b.requestHelpers.actorIdentityKey(actor, opts?) #
{
actorKey: function, // (actor) → string | number naming the principal
}
Derive the key that separates one principal from another for a per-actor bucket: an ownership record, a quota, a concurrency slot, a replay scope. Answers b.requestHelpers.ANONYMOUS_ACTOR_KEY when there is no principal at all, a key when the actor can be named, and null when an actor object exists and carries none of the names this framework reads.
A caller that receives null refuses the operation. Folding an unnameable actor onto a shared literal is what lets two authenticated users share one bucket, and a bucket is an ownership record as often as it is a counter.
What an actor is called is the operator's choice: verify and userLoader return whatever the deployment's tokens carry, so opts.actorKey(actor) names the field when it is none of the ones read here (id, userId, username, sub, principalId, email, in that order). Each key is tagged with the field it came from and its length, so { id: "x" } and { userId: "x" } are two principals rather than one, and tenantId scopes the whole key.
var b = require("blamejs");
b.requestHelpers.actorIdentityKey({ sub: "alice", tenantId: "t1" });
// → "13:tenant:s:2:t1|sub:s:5:alice"
b.requestHelpers.actorIdentityKey({ role: "admin" });
// → null (an actor carrying no name this framework reads)
b.requestHelpers.legacyActorKey(actor) #
The identity derivation used before b.requestHelpers.actorIdentityKey: actor.id || actor.userId as written, string or number, and the literal _anonymous for no actor. Answers null for anything else, including every actor the old derivation would have folded onto that literal.
It exists so a primitive can recognize a record its earlier version wrote, rather than stranding it, and it lives here because identity is derived in one module. It is not an answer for a new record: the value it returns is untagged, so { id: "x" } and { userId: "x" } share it, and a caller compares against it only for a record that carries no format version of its own. Comparing a new record against it would let an actor whose literal id spells another owner's encoded key pass for that owner.
var b = require("blamejs");
b.requestHelpers.legacyActorKey({ id: "ada" }); // → "ada"
b.requestHelpers.legacyActorKey({ sub: "ada" }); // → null
b.requestHelpers.actorDisplayName(actor, opts?) #
{
actorKey: function, // (actor) → the name, when the shape is the operator's
}
Answer the name to show a principal by, as the actor wrote it, or null when the actor carries none. Reads the same fields as b.requestHelpers.actorIdentityKey, so a field that names a principal to one is a field that names it to the other, but in the order a reader wants to see: the login name and address before an opaque id.
This is the display answer, so it is the raw value rather than the tagged key: it is not an identity for an access decision.
var b = require("blamejs");
b.requestHelpers.actorDisplayName({ sub: "alice" }); // → "alice"
b.requestHelpers.actorDisplayName({ role: "admin" }); // → null
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
forwardedHeaders: string[] // header family, in order — default: ["x-forwarded-for"]
}
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.
forwardedHeaders names which header carries the address, in preference order — the first one PRESENT on the request is used (present rather than non-empty), and it defaults to ["x-forwarded-for"]. It only has an effect alongside trustProxy; list only headers your proxy sets or overwrites (see trustedClientIp).
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 the forwarded header
forwardedHeaders: string[], // header family, in order — default: ["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), the forwarded header is honored only when the immediate peer is one of them. With neither, only the socket address is used and forwarded headers are ignored.
trustedProxies takes one CIDR as a string or an array of them, and a value that is neither is refused rather than read as an empty list: an empty list means no proxy is trusted, so a mistyped one would leave every forwarded header ignored while the configuration said a proxy was trusted. An empty array does mean no proxies, which is the same as omitting it.
forwardedHeaders names which header carries the address, in preference order — the first one PRESENT on the request is used, present rather than non-empty: a first-listed header that arrives empty says this request carries no forwarded address, rather than deferring to a lower-priority one the client may have set. It defaults to ["x-forwarded-for"]. Cloudflare publishes the client address as CF-Connecting-IP and the common nginx recipe (proxy_set_header X-Real-IP $remote_addr) as X-Real-IP, so a deployment behind either had no way to use this resolver at all: reading the header directly drops the peer gate, and clientIpResolver hands back the whole trust decision — the CIDR matching, the IPv4-mapped-IPv6 folding — while still reporting peerGated. Every listed header is parsed the same way, so a single-address header is simply a one-hop chain.
List ONLY headers your proxy sets or overwrites on every request. The peer gate proves the request arrived THROUGH your proxy; it cannot prove your proxy authored the header. A proxy that passes an unknown header through unchanged lets a client inject it, and naming that header here would honor the injected value — which is why the default stays the single header the chain walk was designed for.
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
var cf = b.requestHelpers.trustedClientIp({
trustedProxies: ["10.0.0.0/8"],
forwardedHeaders: ["cf-connecting-ip"],
});
cf.resolve(req); // same gate, the header the edge actually sets
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 a "network/prefix" string, with every IPv6 group written out rather than ::-compressed, 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:0:0: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.appendVaryValue(existing, value) #
Return the Vary value that carries both existing and value, or null when existing already carries it. existing is a header value (a string, an array of strings, null or undefined); the result is a string. A * on either side is the whole result, because RFC 9110 section 12.5.5 gives the wildcard form its meaning only when it stands alone: a cache reading *, Accept cannot apply the never-reuse that * asks for. Token comparison ignores case.
b.requestHelpers.appendVaryValue("Cookie", "Accept"); // → "Cookie, Accept"
b.requestHelpers.appendVaryValue("cookie", "Cookie"); // → null
b.requestHelpers.appendVaryValue("Cookie, *", "Accept"); // → "*"
b.requestHelpers.appendVaryValue("Cookie", "*"); // → "*"
b.requestHelpers.finalizeHeaders(res, headers) #
Return the copy of a header object that res.writeHead(status, headers) can send without dropping what is already on res. Vary is merged as mergeVary describes, and a Cache-Control in the object (in any case) is passed through b.cdnCacheControl.keepNoStore, so a no-store already set on the response survives a value the caller supplied. res is not modified.
res.setHeader("Cache-Control", "no-store");
res.setHeader("Vary", "Cookie");
var headers = b.requestHelpers.finalizeHeaders(res, {
"Cache-Control": "public, max-age=3600",
Vary: "Accept-Encoding",
});
headers["Cache-Control"]; // → "no-store"
headers.Vary; // → "Cookie, Accept-Encoding"
b.requestHelpers.mergeVary(res, headers) #
Return a copy of a header object for res.writeHead(status, headers) in which Vary lists the tokens already set on res followed by the tokens the object carries, each once (compared case-insensitively). node:http lets a header object passed to writeHead replace a header set with setHeader, so without the merge a writer that sends its own Vary drops the Origin or Accept-Encoding an earlier middleware added. A * on either side makes the result *. An object with no Vary key (in any case) is returned as a copy unchanged, and res is not modified.
res.setHeader("Vary", "Origin");
var headers = b.requestHelpers.mergeVary(res, { "Content-Type": "text/html", Vary: "Accept-Language" });
headers.Vary; // → "Origin, Accept-Language"
res.writeHead(200, headers);
b.requestHelpers.resolveRoute(req) #
Resolve the route pattern for a request. Prefers req.routePattern, which b.router sets during dispatch and which is a low-cardinality template like /users/:id rather than the concrete URL, then req.routeLabel, which b.router sets when it resolves a request against its own route table before dispatch. With neither, the answer is "(unresolved)".
The concrete URL is never the answer. A path segment carries whatever the client put there, including a reset or invite capability, and stripping the query string does not remove it.
b.requestHelpers.resolveRoute({ routePattern: "/users/:id", url: "/users/42" });
// → "/users/:id"
b.requestHelpers.resolveRoute({ routeLabel: "/orders/:id", url: "/orders/42" });
// → "/orders/:id"
b.requestHelpers.resolveRoute({ url: "/orders?ref=abc" });
// → "(unresolved)"
b.requestHelpers.resolveRoute(undefined);
// → "(unresolved)"
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();
// the hook writes: 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);
b.requestHelpers.failAfterHeaders(res) #
End a response that has already sent its status line, in a way that tells the client the transfer is incomplete. Returns true when it handled the response, false when the caller still owns it and can write a normal error.
Once the headers are on the wire they cannot be replaced, and every attempt to do so makes things worse rather than better: writeHead throws ERR_HTTP_HEADERS_SENT, and a caller that catches that and falls back to res.end("Internal Server Error") appends those words to whatever partial body the client already has. The client then sees a 200 with a plausible final row. Silent truncation presented as success is a data-integrity failure, so the honest signal is an incomplete transfer.
Which signal that is depends on the protocol, which is why this is one primitive rather than a line repeated at each error path:
- **HTTP/1.1** — destroy the socket, so a chunked response ends without its terminating chunk and the client reports a failed download. - **HTTP/2** — destroying with no argument closes the stream with RST_STREAM(NO_ERROR), which a client reads as a clean end: the truncated body arrives as a complete 200. The stream is closed with INTERNAL_ERROR instead so the failure survives the protocol. - **A response that cannot carry a body** — HEAD, 204, 304 — is already complete once its headers are sent. There is nothing to truncate, so it is ended normally; destroying would throw away a valid response.
if (!b.requestHelpers.failAfterHeaders(res)) {
b.render.json(res, { error: "internal" }, { status: 500 });
}