@blamejs/core 0.8.42 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/request-helpers.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.requestHelpers
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Request Helpers
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Defensive per-request shape readers — return sane defaults when
|
|
9
|
+
* headers / route / params are missing or garbage. Every primitive
|
|
10
|
+
* in this module sits in the framework's third validation tier:
|
|
11
|
+
* request-shape readers RETURN DEFAULTS, never throw. They run on
|
|
12
|
+
* every request, often inside middleware that has no recovery path;
|
|
13
|
+
* a thrown error here would crash the very request that triggered
|
|
14
|
+
* the read.
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* The contract is uniform: pass any shape (a real Node
|
|
17
|
+
* IncomingMessage, a partially-constructed test fake, `undefined`,
|
|
18
|
+
* a number, an attacker-supplied bag of strings) and get back a
|
|
19
|
+
* sane default. `resolveRoute` falls back to "/", `clientIp` to
|
|
20
|
+
* `null`, `requestProtocol` to "http", `parseListHeader` and
|
|
21
|
+
* `parseQualityList` to `[]`, `safeHeadersDistinct` to a
|
|
22
|
+
* null-prototype empty object, `extractBearer` to `null`. Operators
|
|
23
|
+
* who want strict refusal layer their own check on the result.
|
|
11
24
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
25
|
+
* The single exception is `parseListHeader({ strictToken: true })`,
|
|
26
|
+
* which throws on RFC 9110 §5.6.2 token grammar violations because
|
|
27
|
+
* it's used by config-time entry points (WebSocket subprotocol
|
|
28
|
+
* negotiation etc.) where bad input MUST surface at boot.
|
|
15
29
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* resolveRoute(req)
|
|
19
|
-
* Returns req.routePattern when the router populated it,
|
|
20
|
-
* otherwise the URL with query string stripped.
|
|
21
|
-
*
|
|
22
|
-
* captureResponseStatus(res, onEnd)
|
|
23
|
-
* Wraps res.writeHead + res.end. Calls onEnd(status) once when the
|
|
24
|
-
* response ends, with the final status pulled from writeHead's
|
|
25
|
-
* argument OR from res.statusCode (modern Node handlers set it
|
|
26
|
-
* directly without going through writeHead). Operators wrap their
|
|
27
|
-
* own pre-end logic by passing it as onEnd.
|
|
28
|
-
*
|
|
29
|
-
* Returns the original (unwrapped) `res.end`. Useful for unit tests
|
|
30
|
-
* that need to assert against the original behavior.
|
|
30
|
+
* @card
|
|
31
|
+
* Defensive per-request shape readers — return sane defaults when headers / route / params are missing or garbage.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
// HTTP status codes used across the framework's HTTP-shaped surface.
|
|
@@ -64,19 +65,43 @@ var HTTP_STATUS = Object.freeze({
|
|
|
64
65
|
GATEWAY_TIMEOUT: 0x1F8,
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @primitive b.requestHelpers.extractActorContext
|
|
70
|
+
* @signature b.requestHelpers.extractActorContext(req, override?)
|
|
71
|
+
* @since 0.4.29
|
|
72
|
+
* @related b.requestHelpers.resolveActorWithOverride, b.requestHelpers.resolveRoute
|
|
73
|
+
*
|
|
74
|
+
* Pull the 5 W's from a request for audit chain emission. The
|
|
75
|
+
* WHO/WHERE/HOW columns on `_blamejs_audit_log` are populated from
|
|
76
|
+
* the returned shape `{ ip, userAgent, sessionId, requestId, method,
|
|
77
|
+
* route, userId }`. Every field is best-effort — missing or
|
|
78
|
+
* non-request inputs return an object with whatever could be
|
|
79
|
+
* inferred plus `null` elsewhere. The audit chain treats `null` as
|
|
80
|
+
* "unknown", so partial context is always safe.
|
|
81
|
+
*
|
|
82
|
+
* Caller-supplied `override` (own `userId`, `ip`, …) is merged on
|
|
83
|
+
* top of the request-derived fields — explicit operator override
|
|
84
|
+
* always wins.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* var req = {
|
|
88
|
+
* ip: "203.0.113.4",
|
|
89
|
+
* method: "POST",
|
|
90
|
+
* url: "/api/orders?ref=abc",
|
|
91
|
+
* headers: { "user-agent": "curl/8.7.1", "x-request-id": "req-9f2" },
|
|
92
|
+
* user: { id: "user-42" },
|
|
93
|
+
* };
|
|
94
|
+
* var actor = b.requestHelpers.extractActorContext(req);
|
|
95
|
+
* // → {
|
|
96
|
+
* // ip: "203.0.113.4", userAgent: "curl/8.7.1",
|
|
97
|
+
* // sessionId: null, requestId: "req-9f2",
|
|
98
|
+
* // method: "POST", route: "/api/orders", userId: "user-42",
|
|
99
|
+
* // }
|
|
100
|
+
*
|
|
101
|
+
* // Override beats request-derived fields:
|
|
102
|
+
* var ovr = b.requestHelpers.extractActorContext(req, { userId: "svc-runner" });
|
|
103
|
+
* ovr.userId; // → "svc-runner"
|
|
104
|
+
*/
|
|
80
105
|
function extractActorContext(req, override) {
|
|
81
106
|
var ctx = {
|
|
82
107
|
ip: null,
|
|
@@ -121,19 +146,42 @@ function extractActorContext(req, override) {
|
|
|
121
146
|
return ctx;
|
|
122
147
|
}
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
/**
|
|
150
|
+
* @primitive b.requestHelpers.resolveActorWithOverride
|
|
151
|
+
* @signature b.requestHelpers.resolveActorWithOverride(callerOpts, baseOverride?)
|
|
152
|
+
* @since 0.4.29
|
|
153
|
+
* @related b.requestHelpers.extractActorContext
|
|
154
|
+
*
|
|
155
|
+
* Convenience wrapper for primitives that accept an optional
|
|
156
|
+
* `{ req, context }` shape and want to thread it into an
|
|
157
|
+
* audit-emit `actor` field. Replaces the four near-identical
|
|
158
|
+
* `_actor()` helpers that lived in api-key, cache, seeders, and
|
|
159
|
+
* notify before v0.4.29.
|
|
160
|
+
*
|
|
161
|
+
* `callerOpts` is the operator-supplied `{ req?, context? }` bag
|
|
162
|
+
* (typically a primitive's call-site opts). `baseOverride` seeds
|
|
163
|
+
* default values applied BEFORE `callerOpts.context` is merged, so
|
|
164
|
+
* `context` always wins — `b.apiKey` seeds `{ userId }` here so the
|
|
165
|
+
* resolved key's owner becomes the default actor unless the
|
|
166
|
+
* operator passes their own `context.userId`. Returns the same
|
|
167
|
+
* shape as `b.requestHelpers.extractActorContext`.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* var req = { ip: "198.51.100.7", method: "DELETE", url: "/v1/keys/abc" };
|
|
171
|
+
* var actor = b.requestHelpers.resolveActorWithOverride(
|
|
172
|
+
* { req: req, context: { userId: "ops-admin" } },
|
|
173
|
+
* { userId: "key-owner-default" }
|
|
174
|
+
* );
|
|
175
|
+
* actor.userId; // → "ops-admin"
|
|
176
|
+
* actor.ip; // → "198.51.100.7"
|
|
177
|
+
* actor.method; // → "DELETE"
|
|
178
|
+
*
|
|
179
|
+
* // Falls back to the seed when caller passes no context:
|
|
180
|
+
* var seeded = b.requestHelpers.resolveActorWithOverride(
|
|
181
|
+
* { req: req }, { userId: "key-owner-default" }
|
|
182
|
+
* );
|
|
183
|
+
* seeded.userId; // → "key-owner-default"
|
|
184
|
+
*/
|
|
137
185
|
function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
138
186
|
var override = baseOverride ? Object.assign({}, baseOverride) : {};
|
|
139
187
|
if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
|
|
@@ -148,22 +196,47 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
|
148
196
|
|
|
149
197
|
// ---- Proxy-trust primitives (v0.5.3) ----
|
|
150
198
|
//
|
|
151
|
-
// `X-Forwarded-For` and `X-Forwarded-Proto` are operator-trust
|
|
152
|
-
// behind a sanitizing reverse proxy they carry the
|
|
153
|
-
// scheme; without one they're attacker-forgeable.
|
|
154
|
-
// trust them; operators behind a proxy
|
|
155
|
-
// hop count for multi-hop chains)
|
|
156
|
-
//
|
|
157
|
-
// clientIp(req, { trustProxy }) → string | null
|
|
158
|
-
//
|
|
159
|
-
// trustProxy false (default): socket.remoteAddress only
|
|
160
|
-
// trustProxy true: leftmost x-forwarded-for hop, else socket
|
|
161
|
-
// trustProxy <integer N>: Nth-from-rightmost xff hop (skip-N-trusted-hops)
|
|
162
|
-
//
|
|
163
|
-
// Middleware accepts `trustProxy` as an opt and threads it through;
|
|
164
|
-
// the framework refuses to silently pick up forwarded headers without
|
|
165
|
-
// the operator's explicit acknowledgement.
|
|
199
|
+
// `X-Forwarded-For` and `X-Forwarded-Proto` are operator-trust
|
|
200
|
+
// headers — behind a sanitizing reverse proxy they carry the
|
|
201
|
+
// apparent origin / scheme; without one they're attacker-forgeable.
|
|
202
|
+
// Default is to NOT trust them; operators behind a proxy opt in by
|
|
203
|
+
// passing `trustProxy: true` (or a hop count for multi-hop chains).
|
|
166
204
|
|
|
205
|
+
/**
|
|
206
|
+
* @primitive b.requestHelpers.clientIp
|
|
207
|
+
* @signature b.requestHelpers.clientIp(req, opts?)
|
|
208
|
+
* @since 0.5.3
|
|
209
|
+
* @related b.requestHelpers.requestProtocol, b.requestHelpers.parseListHeader
|
|
210
|
+
*
|
|
211
|
+
* Resolve the originating client IP from a request. Default reads
|
|
212
|
+
* only `req.socket.remoteAddress` — `X-Forwarded-For` is ignored
|
|
213
|
+
* because without a sanitizing reverse proxy it's
|
|
214
|
+
* attacker-forgeable. Behind a trusted proxy, operators opt in via
|
|
215
|
+
* `trustProxy: true` (use the leftmost XFF hop) or
|
|
216
|
+
* `trustProxy: <N>` (skip N trusted hops from the right and return
|
|
217
|
+
* the Nth-from-rightmost). Returns `null` when no address can be
|
|
218
|
+
* read — never throws.
|
|
219
|
+
*
|
|
220
|
+
* @opts
|
|
221
|
+
* trustProxy: boolean | number // false (default) | true | hop count
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* var req = {
|
|
225
|
+
* socket: { remoteAddress: "10.0.0.1" },
|
|
226
|
+
* headers: { "x-forwarded-for": "203.0.113.7, 10.0.0.5" },
|
|
227
|
+
* };
|
|
228
|
+
* b.requestHelpers.clientIp(req);
|
|
229
|
+
* // → "10.0.0.1" (forwarded headers ignored by default)
|
|
230
|
+
*
|
|
231
|
+
* b.requestHelpers.clientIp(req, { trustProxy: true });
|
|
232
|
+
* // → "203.0.113.7" (leftmost XFF hop)
|
|
233
|
+
*
|
|
234
|
+
* b.requestHelpers.clientIp(req, { trustProxy: 1 });
|
|
235
|
+
* // → "10.0.0.5" (1 trusted hop from the right)
|
|
236
|
+
*
|
|
237
|
+
* b.requestHelpers.clientIp(undefined);
|
|
238
|
+
* // → null
|
|
239
|
+
*/
|
|
167
240
|
function clientIp(req, opts) {
|
|
168
241
|
if (!req) return null;
|
|
169
242
|
var trust = opts && opts.trustProxy;
|
|
@@ -182,6 +255,38 @@ function clientIp(req, opts) {
|
|
|
182
255
|
return null;
|
|
183
256
|
}
|
|
184
257
|
|
|
258
|
+
/**
|
|
259
|
+
* @primitive b.requestHelpers.requestProtocol
|
|
260
|
+
* @signature b.requestHelpers.requestProtocol(req, opts?)
|
|
261
|
+
* @since 0.5.3
|
|
262
|
+
* @related b.requestHelpers.clientIp, b.safeRedirect
|
|
263
|
+
*
|
|
264
|
+
* Resolve the inbound transport scheme. Default returns `"https"`
|
|
265
|
+
* when `req.socket.encrypted` is set, otherwise `"http"`. Behind a
|
|
266
|
+
* trusted reverse proxy that terminates TLS, set `trustProxy: true`
|
|
267
|
+
* to read the leftmost `X-Forwarded-Proto` hop instead — without
|
|
268
|
+
* the explicit opt-in the framework refuses to pick up the
|
|
269
|
+
* attacker-forgeable header. Always returns a string; on bad input
|
|
270
|
+
* falls back to `"http"`.
|
|
271
|
+
*
|
|
272
|
+
* @opts
|
|
273
|
+
* trustProxy: boolean // false (default) | true
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* var req = { socket: { encrypted: true } };
|
|
277
|
+
* b.requestHelpers.requestProtocol(req);
|
|
278
|
+
* // → "https"
|
|
279
|
+
*
|
|
280
|
+
* var behindProxy = {
|
|
281
|
+
* socket: { encrypted: false },
|
|
282
|
+
* headers: { "x-forwarded-proto": "https, http" },
|
|
283
|
+
* };
|
|
284
|
+
* b.requestHelpers.requestProtocol(behindProxy, { trustProxy: true });
|
|
285
|
+
* // → "https"
|
|
286
|
+
*
|
|
287
|
+
* b.requestHelpers.requestProtocol(undefined);
|
|
288
|
+
* // → "http"
|
|
289
|
+
*/
|
|
185
290
|
function requestProtocol(req, opts) {
|
|
186
291
|
if (!req) return "http";
|
|
187
292
|
var trust = opts && opts.trustProxy;
|
|
@@ -197,28 +302,53 @@ function requestProtocol(req, opts) {
|
|
|
197
302
|
return "http";
|
|
198
303
|
}
|
|
199
304
|
|
|
200
|
-
// parseListHeader — split a comma-separated header / opt value into a
|
|
201
|
-
// list of trimmed non-empty tokens. Replaces the
|
|
202
|
-
// `String(x).split(",").map(s => s.trim()).filter(Boolean)` chain that
|
|
203
|
-
// was duplicated across cors / compression / scheduler / webhook /
|
|
204
|
-
// websocket / db-schema / cli before v0.5.17.
|
|
205
|
-
//
|
|
206
|
-
// parseListHeader("a, b , ,c") → ["a", "b", "c"]
|
|
207
|
-
// parseListHeader("Foo, Bar", { lowercase: true })
|
|
208
|
-
// → ["foo", "bar"]
|
|
209
|
-
// parseListHeader(undefined) → []
|
|
210
|
-
// parseListHeader("") → []
|
|
211
|
-
//
|
|
212
|
-
// Tolerant read: non-string input returns [] — these are read from
|
|
213
|
-
// request headers that the network might omit. Callers needing stricter
|
|
214
|
-
// checks layer their own validation on the result.
|
|
215
305
|
// RFC 9110 §5.6.2 token grammar — letters, digits, and the
|
|
216
306
|
// punctuation set `!#$%&'*+-.^_`|~`. Used by header-list parsers
|
|
217
|
-
// that consume protocol tokens (Connection, Sec-WebSocket-
|
|
218
|
-
//
|
|
219
|
-
// human-supplied values (Origin lists, etc.) opt out by passing
|
|
220
|
-
// `lax: true`.
|
|
307
|
+
// that consume protocol tokens (Connection, Sec-WebSocket-Protocol,
|
|
308
|
+
// etc.).
|
|
221
309
|
var RFC_9110_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @primitive b.requestHelpers.parseListHeader
|
|
313
|
+
* @signature b.requestHelpers.parseListHeader(value, opts?)
|
|
314
|
+
* @since 0.5.17
|
|
315
|
+
* @related b.requestHelpers.parseQualityList, b.requestHelpers.appendVary
|
|
316
|
+
*
|
|
317
|
+
* Split a comma-separated header / opt value into a list of trimmed
|
|
318
|
+
* non-empty tokens. Replaces the
|
|
319
|
+
* `String(x).split(",").map(s => s.trim()).filter(Boolean)` chain
|
|
320
|
+
* that was duplicated across cors / compression / scheduler /
|
|
321
|
+
* webhook / websocket / db-schema / cli before v0.5.17.
|
|
322
|
+
*
|
|
323
|
+
* Tolerant read: non-string input returns `[]` — these are read
|
|
324
|
+
* from request headers that the network might omit. Callers
|
|
325
|
+
* needing stricter checks layer their own validation on the
|
|
326
|
+
* result. The `strictToken` opt is the one exception — it throws
|
|
327
|
+
* on RFC 9110 §5.6.2 token-grammar violations, used by config-time
|
|
328
|
+
* entry points (WebSocket subprotocol negotiation etc.) where bad
|
|
329
|
+
* input MUST surface at boot.
|
|
330
|
+
*
|
|
331
|
+
* @opts
|
|
332
|
+
* lowercase: boolean // lowercase every token before returning
|
|
333
|
+
* strictToken: boolean // throw on non-RFC 9110 token entries
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* b.requestHelpers.parseListHeader("a, b , ,c");
|
|
337
|
+
* // → ["a", "b", "c"]
|
|
338
|
+
*
|
|
339
|
+
* b.requestHelpers.parseListHeader("Foo, Bar", { lowercase: true });
|
|
340
|
+
* // → ["foo", "bar"]
|
|
341
|
+
*
|
|
342
|
+
* b.requestHelpers.parseListHeader(undefined);
|
|
343
|
+
* // → []
|
|
344
|
+
*
|
|
345
|
+
* try {
|
|
346
|
+
* b.requestHelpers.parseListHeader("chat, bad token", { strictToken: true });
|
|
347
|
+
* } catch (err) {
|
|
348
|
+
* err.message;
|
|
349
|
+
* // → "parseListHeader: 'bad token' is not a valid RFC 9110 token"
|
|
350
|
+
* }
|
|
351
|
+
*/
|
|
222
352
|
function parseListHeader(value, opts) {
|
|
223
353
|
if (value == null) return [];
|
|
224
354
|
opts = opts || {};
|
|
@@ -241,10 +371,34 @@ function parseListHeader(value, opts) {
|
|
|
241
371
|
return out;
|
|
242
372
|
}
|
|
243
373
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
374
|
+
/**
|
|
375
|
+
* @primitive b.requestHelpers.appendVary
|
|
376
|
+
* @signature b.requestHelpers.appendVary(res, value)
|
|
377
|
+
* @since 0.5.17
|
|
378
|
+
* @related b.requestHelpers.parseListHeader
|
|
379
|
+
*
|
|
380
|
+
* Append a token to a `Vary` response header without dropping
|
|
381
|
+
* prior values (compression middleware sets `Vary: Accept-
|
|
382
|
+
* Encoding`, an auth helper might set `Vary: Authorization`, etc.).
|
|
383
|
+
* Idempotent — re-adding an existing token (case-insensitive) is a
|
|
384
|
+
* no-op. Silently no-ops when `res` doesn't expose
|
|
385
|
+
* `getHeader`/`setHeader` so misuse during testing or in non-HTTP
|
|
386
|
+
* contexts never throws.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* var headers = { Vary: "Accept-Encoding" };
|
|
390
|
+
* var res = {
|
|
391
|
+
* getHeader: function (n) { return headers[n]; },
|
|
392
|
+
* setHeader: function (n, v) { headers[n] = v; },
|
|
393
|
+
* };
|
|
394
|
+
*
|
|
395
|
+
* b.requestHelpers.appendVary(res, "Authorization");
|
|
396
|
+
* headers.Vary; // → "Accept-Encoding, Authorization"
|
|
397
|
+
*
|
|
398
|
+
* // Idempotent — re-adding is a no-op:
|
|
399
|
+
* b.requestHelpers.appendVary(res, "accept-encoding");
|
|
400
|
+
* headers.Vary; // → "Accept-Encoding, Authorization"
|
|
401
|
+
*/
|
|
248
402
|
function appendVary(res, value) {
|
|
249
403
|
if (!res || typeof res.getHeader !== "function" || typeof res.setHeader !== "function") return;
|
|
250
404
|
var existing = res.getHeader("Vary");
|
|
@@ -256,6 +410,32 @@ function appendVary(res, value) {
|
|
|
256
410
|
res.setHeader("Vary", tokens.join(", "));
|
|
257
411
|
}
|
|
258
412
|
|
|
413
|
+
/**
|
|
414
|
+
* @primitive b.requestHelpers.resolveRoute
|
|
415
|
+
* @signature b.requestHelpers.resolveRoute(req)
|
|
416
|
+
* @since 0.4.0
|
|
417
|
+
* @related b.requestHelpers.extractActorContext, b.requestHelpers.captureResponseStatus
|
|
418
|
+
*
|
|
419
|
+
* Resolve the route pattern for a request. Prefers
|
|
420
|
+
* `req.routePattern` (set by `b.router` during dispatch — a
|
|
421
|
+
* low-cardinality template like `/users/:id` rather than the
|
|
422
|
+
* concrete URL), and falls back to `req.url` with the query
|
|
423
|
+
* string stripped. Returns `"/"` on missing or non-string input
|
|
424
|
+
* so audit-chain rows / metrics labels never carry `null`.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* b.requestHelpers.resolveRoute({ routePattern: "/users/:id", url: "/users/42" });
|
|
428
|
+
* // → "/users/:id"
|
|
429
|
+
*
|
|
430
|
+
* b.requestHelpers.resolveRoute({ url: "/orders?ref=abc" });
|
|
431
|
+
* // → "/orders"
|
|
432
|
+
*
|
|
433
|
+
* b.requestHelpers.resolveRoute({});
|
|
434
|
+
* // → "/"
|
|
435
|
+
*
|
|
436
|
+
* b.requestHelpers.resolveRoute(undefined);
|
|
437
|
+
* // → "/"
|
|
438
|
+
*/
|
|
259
439
|
function resolveRoute(req) {
|
|
260
440
|
if (req && typeof req.routePattern === "string" && req.routePattern.length > 0) {
|
|
261
441
|
return req.routePattern;
|
|
@@ -266,6 +446,40 @@ function resolveRoute(req) {
|
|
|
266
446
|
return qIdx === -1 ? url : url.slice(0, qIdx);
|
|
267
447
|
}
|
|
268
448
|
|
|
449
|
+
/**
|
|
450
|
+
* @primitive b.requestHelpers.captureResponseStatus
|
|
451
|
+
* @signature b.requestHelpers.captureResponseStatus(res, onEnd)
|
|
452
|
+
* @since 0.4.0
|
|
453
|
+
* @related b.requestHelpers.resolveRoute
|
|
454
|
+
*
|
|
455
|
+
* Wrap a response so observability / audit middleware can learn
|
|
456
|
+
* the final status code at end-of-stream. Patches `res.writeHead`
|
|
457
|
+
* and `res.end`; when `res.end()` fires, invokes `onEnd(status)`
|
|
458
|
+
* with the value passed to `writeHead` (preferred) or
|
|
459
|
+
* `res.statusCode` (fallback) or `200` (default). Errors thrown by
|
|
460
|
+
* the `onEnd` callback are swallowed — instrumentation must never
|
|
461
|
+
* break the response. Returns the original `end` function so
|
|
462
|
+
* callers that want to compose can keep a reference. Throws when
|
|
463
|
+
* either argument is missing — these are config-time wiring
|
|
464
|
+
* errors, surfaced loudly.
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* var headers = {};
|
|
468
|
+
* var sent = null;
|
|
469
|
+
* var res = {
|
|
470
|
+
* statusCode: 200,
|
|
471
|
+
* writeHead: function (s) { this.statusCode = s; sent = "head"; },
|
|
472
|
+
* end: function () { sent = (sent || "end"); },
|
|
473
|
+
* };
|
|
474
|
+
*
|
|
475
|
+
* b.requestHelpers.captureResponseStatus(res, function (status) {
|
|
476
|
+
* console.log("final status:", status);
|
|
477
|
+
* });
|
|
478
|
+
*
|
|
479
|
+
* res.writeHead(204);
|
|
480
|
+
* res.end();
|
|
481
|
+
* // → "final status: 204"
|
|
482
|
+
*/
|
|
269
483
|
function captureResponseStatus(res, onEnd) {
|
|
270
484
|
if (!res || typeof onEnd !== "function") {
|
|
271
485
|
throw new Error("captureResponseStatus: requires (res, onEnd)");
|
|
@@ -288,28 +502,46 @@ function captureResponseStatus(res, onEnd) {
|
|
|
288
502
|
return origEnd;
|
|
289
503
|
}
|
|
290
504
|
|
|
291
|
-
// parseQualityList — RFC 9110 §12.5 Accept-* header parser.
|
|
292
|
-
//
|
|
293
|
-
// Returns `[{ value, q }]` sorted by q descending. Used by content
|
|
294
|
-
// negotiation (`Accept-Encoding`, `Accept-Language`, `Accept`, etc.).
|
|
295
|
-
// Each Accept-* middleware previously had its own copy of this loop;
|
|
296
|
-
// extracting it here keeps the q-value semantics consistent
|
|
297
|
-
// (q=0 = explicit exclusion; clamped to [0, 1]; missing q = 1).
|
|
298
|
-
//
|
|
299
|
-
// parseQualityList("br;q=1.0, gzip;q=0.5, *;q=0")
|
|
300
|
-
// → [{ value: "br", q: 1 }, { value: "gzip", q: 0.5 }, { value: "*", q: 0 }]
|
|
301
|
-
//
|
|
302
|
-
// `value` is lowercased by default; pass `{ caseSensitive: true }` to
|
|
303
|
-
// preserve case (BCP 47 language tags want case preservation since
|
|
304
|
-
// `pt-BR` and `pt-br` resolve identically but operators may match by
|
|
305
|
-
// canonical form themselves).
|
|
306
|
-
//
|
|
307
|
-
// Bad input (non-string, empty) returns []. RFC 9110 says an absent
|
|
308
|
-
// Accept header means "accept anything"; callers handle that absence
|
|
309
|
-
// at their own layer (compression's [{ encoding: "*", q: 1 }] default
|
|
310
|
-
// vs i18n's "fall back to default locale" — different semantics).
|
|
311
505
|
var Q_VALUE_RE = /(?:^|;|\s)q\s*=\s*([0-9]*\.?[0-9]+)/i;
|
|
312
506
|
|
|
507
|
+
/**
|
|
508
|
+
* @primitive b.requestHelpers.parseQualityList
|
|
509
|
+
* @signature b.requestHelpers.parseQualityList(headerValue, opts?)
|
|
510
|
+
* @since 0.5.17
|
|
511
|
+
* @related b.requestHelpers.parseListHeader
|
|
512
|
+
*
|
|
513
|
+
* RFC 9110 §12.5 `Accept-*` header parser. Returns
|
|
514
|
+
* `[{ value, q }]` sorted by q descending. Used by content
|
|
515
|
+
* negotiation (`Accept-Encoding`, `Accept-Language`, `Accept`, …).
|
|
516
|
+
* Each Accept-* middleware previously carried its own copy of this
|
|
517
|
+
* loop; centralizing it keeps the q-value semantics consistent —
|
|
518
|
+
* `q=0` is explicit exclusion, q is clamped to `[0, 1]`, missing q
|
|
519
|
+
* defaults to `1`. `value` is lowercased by default; pass
|
|
520
|
+
* `caseSensitive: true` to preserve case (BCP 47 language tags
|
|
521
|
+
* may need it). Bad input (non-string, empty) returns `[]` —
|
|
522
|
+
* absent Accept-* means "accept anything" but the right default
|
|
523
|
+
* differs by caller, so it's the caller's call to layer.
|
|
524
|
+
*
|
|
525
|
+
* @opts
|
|
526
|
+
* caseSensitive: boolean // preserve original case in `value`
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* b.requestHelpers.parseQualityList("br;q=1.0, gzip;q=0.5, *;q=0");
|
|
530
|
+
* // → [
|
|
531
|
+
* // { value: "br", q: 1 },
|
|
532
|
+
* // { value: "gzip", q: 0.5 },
|
|
533
|
+
* // { value: "*", q: 0 },
|
|
534
|
+
* // ]
|
|
535
|
+
*
|
|
536
|
+
* b.requestHelpers.parseQualityList("en-US,en;q=0.9", { caseSensitive: true });
|
|
537
|
+
* // → [
|
|
538
|
+
* // { value: "en-US", q: 1 },
|
|
539
|
+
* // { value: "en", q: 0.9 },
|
|
540
|
+
* // ]
|
|
541
|
+
*
|
|
542
|
+
* b.requestHelpers.parseQualityList(undefined);
|
|
543
|
+
* // → []
|
|
544
|
+
*/
|
|
313
545
|
function parseQualityList(headerValue, opts) {
|
|
314
546
|
if (typeof headerValue !== "string" || headerValue.length === 0) return [];
|
|
315
547
|
opts = opts || {};
|
|
@@ -339,19 +571,148 @@ function parseQualityList(headerValue, opts) {
|
|
|
339
571
|
return out;
|
|
340
572
|
}
|
|
341
573
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
574
|
+
/**
|
|
575
|
+
* @primitive b.requestHelpers.extractBearer
|
|
576
|
+
* @signature b.requestHelpers.extractBearer(req)
|
|
577
|
+
* @since 0.7.19
|
|
578
|
+
* @related b.requestHelpers.safeHeadersDistinct, b.middleware.bearerAuth, b.guardJwt
|
|
579
|
+
*
|
|
580
|
+
* RFC 6750 §2.1 inbound bearer-token extractor. Reads the
|
|
581
|
+
* `Authorization` request header, validates the case-insensitive
|
|
582
|
+
* `Bearer ` scheme, and returns the trimmed token string. Returns
|
|
583
|
+
* `null` on any malformed shape — defensive by design, since this
|
|
584
|
+
* runs on every authenticated request and a throw here would crash
|
|
585
|
+
* the request itself. Callers that require a token throw their
|
|
586
|
+
* own authentication-shape error when `null` surfaces.
|
|
587
|
+
*
|
|
588
|
+
* Refusal cases (all return `null`): missing Authorization header,
|
|
589
|
+
* non-string value, multiple Authorization headers (CWE-345 trust
|
|
590
|
+
* mismatch), scheme other than `Bearer` (case-insensitive), missing
|
|
591
|
+
* space + token after the scheme, embedded CR / LF / NUL / Tab /
|
|
592
|
+
* other ASCII control bytes (CRLF-injection defense — the token
|
|
593
|
+
* transits log lines + audit metadata), embedded spaces inside the
|
|
594
|
+
* token. Token shape past the scheme word is NOT validated against
|
|
595
|
+
* the RFC 6750 b64token grammar here — `b.guardJwt` /
|
|
596
|
+
* `b.middleware.bearerAuth` own format-specific checks.
|
|
597
|
+
*
|
|
598
|
+
* The outbound counterpart is `b.authHeader.bearer(token)`, which
|
|
599
|
+
* constructs `Authorization: Bearer <token>` for outgoing requests.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* var req = { headers: { authorization: "Bearer eyJhbGciOiJFUzI1NiJ9.payload.sig" } };
|
|
603
|
+
* b.requestHelpers.extractBearer(req);
|
|
604
|
+
* // → "eyJhbGciOiJFUzI1NiJ9.payload.sig"
|
|
605
|
+
*
|
|
606
|
+
* // Case-insensitive scheme:
|
|
607
|
+
* b.requestHelpers.extractBearer({ headers: { authorization: "bearer abc123" } });
|
|
608
|
+
* // → "abc123"
|
|
609
|
+
*
|
|
610
|
+
* // Refusals return null:
|
|
611
|
+
* b.requestHelpers.extractBearer({ headers: { authorization: "Basic dXNlcjpwYXNz" } });
|
|
612
|
+
* // → null
|
|
613
|
+
*
|
|
614
|
+
* b.requestHelpers.extractBearer({ headers: { authorization: "Bearer abc, def" } });
|
|
615
|
+
* // → null
|
|
616
|
+
*
|
|
617
|
+
* b.requestHelpers.extractBearer({});
|
|
618
|
+
* // → null
|
|
619
|
+
*/
|
|
620
|
+
function extractBearer(req) {
|
|
621
|
+
if (!req || typeof req !== "object") return null;
|
|
622
|
+
// Distinct-header scan first — multiple Authorization headers is a
|
|
623
|
+
// trust-mismatch shape (CWE-345); refuse rather than pick one. Node's
|
|
624
|
+
// h1 parser folds duplicate Authorization values with ", " joining
|
|
625
|
+
// by default, so the multi-value detection must look at rawHeaders
|
|
626
|
+
// (or headersDistinct via safeHeadersDistinct).
|
|
627
|
+
if (Array.isArray(req.rawHeaders)) {
|
|
628
|
+
var seen = 0;
|
|
629
|
+
for (var ri = 0; ri + 1 < req.rawHeaders.length; ri += 2) {
|
|
630
|
+
var name = req.rawHeaders[ri];
|
|
631
|
+
if (typeof name === "string" && name.toLowerCase() === "authorization") {
|
|
632
|
+
seen += 1;
|
|
633
|
+
if (seen > 1) return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
var headers = req.headers;
|
|
638
|
+
if (!headers || typeof headers !== "object") return null;
|
|
639
|
+
var raw = headers["authorization"];
|
|
640
|
+
if (raw === undefined) raw = headers["Authorization"];
|
|
641
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
642
|
+
// A pre-folded duplicate (Node h1 default) shows up as a comma in the
|
|
643
|
+
// value — refuse, same trust-mismatch class. Bearer tokens themselves
|
|
644
|
+
// never contain commas (RFC 6750 b64token grammar).
|
|
645
|
+
if (raw.indexOf(",") !== -1) return null;
|
|
646
|
+
// Reject ASCII control characters BEFORE prefix-matching so a header
|
|
647
|
+
// like "Bearer\rinjected" never reaches consumers.
|
|
648
|
+
for (var ci = 0; ci < raw.length; ci += 1) {
|
|
649
|
+
var cc = raw.charCodeAt(ci);
|
|
650
|
+
if (cc === 0x00 || cc === 0x0A || cc === 0x0D || cc === 0x09 || cc < 0x20 || cc === 0x7F) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// RFC 6750 §2.1 — auth-scheme is case-insensitive. The "Bearer "
|
|
655
|
+
// prefix + at least one token byte must be present; the literal
|
|
656
|
+
// 7-byte prefix length (6 letters + space) matches "Bearer " and
|
|
657
|
+
// its case variants.
|
|
658
|
+
if (raw.length < 8) return null; // allow:raw-byte-literal — RFC 6750 §2.1 "Bearer " prefix (7 chars) + ≥1 token byte, char count not bytes
|
|
659
|
+
if (raw.charAt(6) !== " ") return null;
|
|
660
|
+
var schemeLower = raw.slice(0, 6).toLowerCase();
|
|
661
|
+
if (schemeLower !== "bearer") return null;
|
|
662
|
+
var token = raw.slice(7);
|
|
663
|
+
// Trim whitespace per RFC 7230 OWS tolerance — but only the leading /
|
|
664
|
+
// trailing space; embedded whitespace in a Bearer token is not RFC
|
|
665
|
+
// 6750 b64token-shaped and is refused above (cc === 0x09 / 0x20 < cc
|
|
666
|
+
// already covers control + delete; spaces inside the token would
|
|
667
|
+
// pass the control check, so handle explicitly).
|
|
668
|
+
while (token.length > 0 && token.charAt(0) === " ") token = token.slice(1);
|
|
669
|
+
while (token.length > 0 && token.charAt(token.length - 1) === " ") {
|
|
670
|
+
token = token.slice(0, -1);
|
|
671
|
+
}
|
|
672
|
+
if (token.length === 0) return null;
|
|
673
|
+
// Refuse embedded spaces — a properly-formed bearer credential is
|
|
674
|
+
// a single token. Embedded space would slip a second value past
|
|
675
|
+
// operators that read the trailing portion as JWT / opaque-id.
|
|
676
|
+
if (token.indexOf(" ") !== -1) return null;
|
|
677
|
+
return token;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* @primitive b.requestHelpers.safeHeadersDistinct
|
|
682
|
+
* @signature b.requestHelpers.safeHeadersDistinct(req)
|
|
683
|
+
* @since 0.7.0
|
|
684
|
+
* @related b.requestHelpers.extractBearer
|
|
685
|
+
*
|
|
686
|
+
* Defensive replacement for `req.headersDistinct`. Node CVE
|
|
687
|
+
* 2026-21710: `headersDistinct` is implemented as a getter, and
|
|
688
|
+
* reading `__proto__` on the underlying header bag throws
|
|
689
|
+
* synchronously inside the getter. A request bearing a
|
|
690
|
+
* `__proto__:` header therefore escapes any handler-level
|
|
691
|
+
* try/catch — the throw happens at property-access time, not
|
|
692
|
+
* later. This helper computes the same shape (lowercased
|
|
693
|
+
* header-name to array of values) directly from `req.rawHeaders`,
|
|
694
|
+
* skipping `__proto__` / `constructor` / `prototype` keys, and
|
|
695
|
+
* returns a null-prototype object so iteration never inherits
|
|
696
|
+
* `Object.prototype` properties. Always returns an object — never
|
|
697
|
+
* throws.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* var req = {
|
|
701
|
+
* rawHeaders: [
|
|
702
|
+
* "Set-Cookie", "a=1",
|
|
703
|
+
* "Set-Cookie", "b=2",
|
|
704
|
+
* "X-Trace", "abc",
|
|
705
|
+
* "__proto__", "polluted",
|
|
706
|
+
* ],
|
|
707
|
+
* };
|
|
708
|
+
* var headers = b.requestHelpers.safeHeadersDistinct(req);
|
|
709
|
+
* headers["set-cookie"]; // → ["a=1", "b=2"]
|
|
710
|
+
* headers["x-trace"]; // → ["abc"]
|
|
711
|
+
* headers["__proto__"]; // → undefined (prototype-pollution key dropped)
|
|
712
|
+
*
|
|
713
|
+
* b.requestHelpers.safeHeadersDistinct(undefined);
|
|
714
|
+
* // → {} (null-prototype empty object)
|
|
715
|
+
*/
|
|
355
716
|
function safeHeadersDistinct(req) {
|
|
356
717
|
var out = Object.create(null);
|
|
357
718
|
if (!req || !Array.isArray(req.rawHeaders)) return out;
|
|
@@ -384,5 +745,8 @@ module.exports = {
|
|
|
384
745
|
appendVary: appendVary,
|
|
385
746
|
// CVE-2026-21710 wrap — safe alternative to req.headersDistinct
|
|
386
747
|
safeHeadersDistinct: safeHeadersDistinct,
|
|
748
|
+
// RFC 6750 §2.1 inbound bearer-token extractor (returns null on
|
|
749
|
+
// missing / malformed input — symmetric to outbound b.authHeader.bearer)
|
|
750
|
+
extractBearer: extractBearer,
|
|
387
751
|
HTTP_STATUS: HTTP_STATUS,
|
|
388
752
|
};
|