@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -1,33 +1,34 @@
1
1
  "use strict";
2
2
  /**
3
- * request-helpers — small shared utilities for HTTP request middleware.
3
+ * @module b.requestHelpers
4
+ * @nav HTTP
5
+ * @title Request Helpers
4
6
  *
5
- * The framework's metrics + tracing requestMiddleware both label by
6
- * route TEMPLATE and capture the final response status. They had
7
- * identical implementations of:
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
- * 1. Reading req.routePattern with a URL-fallback
10
- * 2. Wrapping res.writeHead + reading res.statusCode at res.end
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
- * This module owns the two helpers so the duplication doesn't drift —
13
- * if either pattern changes (e.g. handle res.statusMessage), it changes
14
- * once.
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
- * Public API:
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
- // extractActorContext(req) — pull the 5 W's from a request for audit
68
- // chain emission. WHO/WHERE/HOW columns on _blamejs_audit_log are
69
- // populated from this shape:
70
- //
71
- // { ip, userAgent, sessionId, requestId, method, route, userId }
72
- //
73
- // Every field is best-effort: missing or non-request inputs return
74
- // an object with whatever could be inferred plus null elsewhere.
75
- // Audit chain treats null as "unknown", so partial context is safe.
76
- //
77
- // Caller-supplied actor (existing actor.userId, actor.ip, etc.) is
78
- // merged on top of the request-derived fields explicit operator
79
- // override always wins.
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
- // Convenience wrapper for primitives that accept an optional
125
- // `{ req, context }` shape and want to thread it into an audit-emit
126
- // `actor` field. Replaces the four near-identical `_actor()` helpers
127
- // that lived in api-key, cache, seeders, and notify before v0.4.29.
128
- //
129
- // callerOpts: operator-supplied `{ req?, context? }` (e.g. the
130
- // primitive's call-site opts bag)
131
- // baseOverride: optional seed values applied BEFORE callerOpts.context
132
- // so `context` wins. api-key seeds `{ userId }` here so
133
- // the resolved key's owner becomes the default actor
134
- // unless the operator passes their own context.userId.
135
- //
136
- // Returns the same shape as extractActorContext.
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 headers —
152
- // behind a sanitizing reverse proxy they carry the apparent origin /
153
- // scheme; without one they're attacker-forgeable. Default is to NOT
154
- // trust them; operators behind a proxy set `trustProxy: true` (or a
155
- // hop count for multi-hop chains) per-middleware to opt in.
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
- // Protocol, etc.). Operator handlers parsing comma-separated
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
- // Append a token to a `Vary` response header without dropping prior
245
- // values (compression middleware sets `Vary: Accept-Encoding`, an
246
- // auth helper might set `Vary: Authorization`, etc.). Idempotent —
247
- // re-adding an existing token is a no-op.
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
- // safeHeadersDistinct(req) — defensive accessor for req.headersDistinct.
343
- //
344
- // Node CVE-2026-21710: req.headersDistinct is a getter; reading
345
- // __proto__ on the underlying header bag throws synchronously inside
346
- // the getter, so a request bearing a __proto__: header escapes any
347
- // handler-level try/catch (the throw happens at property-access time,
348
- // not later). This helper computes the same shape (lowercased header-
349
- // name array of values) directly from req.rawHeaders, bypassing the
350
- // faulty getter entirely.
351
- //
352
- // Returns a null-prototype object so framework code can iterate its
353
- // keys without inheriting Object.prototype properties the same shape
354
- // Node's headersDistinct produces, minus the throwing getter.
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
  };