@blamejs/core 0.8.43 → 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 +92 -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
@@ -34,6 +34,40 @@ var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
34
34
  var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
35
35
  var audit = lazyRequire(function () { return require("../audit"); });
36
36
 
37
+ /**
38
+ * @primitive b.middleware.openapiServe
39
+ * @signature b.middleware.openapiServe(opts)
40
+ * @since 0.1.0
41
+ * @related b.middleware.asyncapiServe, b.openapi.create
42
+ *
43
+ * Serves an OpenAPI 3.1 document built via `b.openapi.create` at a
44
+ * configurable JSON + YAML mount point. GET/HEAD only; everything
45
+ * else falls through. SHA3-512 ETag enables conditional 304. With
46
+ * `accessControl: "public"` (default) emits
47
+ * `Access-Control-Allow-Origin: *` so external doc tooling can
48
+ * fetch; `same-origin` omits the CORS header for internal-only docs.
49
+ *
50
+ * @opts
51
+ * {
52
+ * document: object, // builder from b.openapi.create()
53
+ * pathJson: string, // default "/openapi.json"
54
+ * pathYaml: string, // default "/openapi.yaml"
55
+ * pretty: boolean,
56
+ * cacheControl: string, // default "public, max-age=300"
57
+ * accessControl: "public"|"same-origin"|{ allowOrigin: string },
58
+ * audit: boolean,
59
+ * }
60
+ *
61
+ * @example
62
+ * var b = require("@blamejs/core");
63
+ * var app = b.router.create();
64
+ * var doc = b.openapi.create({ title: "api", version: "1.0.0" });
65
+ * app.use(b.middleware.openapiServe({
66
+ * document: doc,
67
+ * pretty: true,
68
+ * cacheControl: "public, max-age=300",
69
+ * }));
70
+ */
37
71
  function create(opts) {
38
72
  opts = opts || {};
39
73
  validateOpts(opts, [
@@ -3,20 +3,27 @@
3
3
  * Rate-limit middleware — pluggable backend, default in-memory.
4
4
  *
5
5
  * Per-IP by default; key extractor is configurable (per-user, per-API-key,
6
- * per-route). Two built-in backends:
6
+ * per-route). Two built-in backends, two in-memory algorithms:
7
7
  *
8
- * - 'memory' (default) — token-bucket, in-process. Each key gets
9
- * `burst` tokens up front; tokens refill at `refillPerSecond`;
10
- * each request costs 1 token. Single-process accuracy only.
8
+ * - 'memory' (default) — in-process counter. The `algorithm` opt
9
+ * selects the shape:
10
+ * 'token-bucket' (default) each key gets `burst` tokens up
11
+ * front; tokens refill at `refillPerSecond`; each request
12
+ * costs 1 token. Smooths bursty traffic.
13
+ * 'fixed-window' — per-key counter resets at the start of each
14
+ * window (`windowMs`); allow up to `max` per window. Matches
15
+ * the cluster backend's algorithm without an SQL hop. Cheaper
16
+ * per request than token-bucket; tradeoff is the boundary
17
+ * burst at window edges (worst case 2*max in 1*windowMs).
11
18
  *
12
19
  * - 'cluster' — fixed-window counter shared across the cluster
13
20
  * via `_blamejs_rate_limit_counters`. Atomic INSERT...ON CONFLICT
14
21
  * increments per key within a window and rolls over when the
15
22
  * window advances. Multi-process / multi-node accurate.
16
23
  *
17
- * Cluster opt-in switches the algorithm shape from token-bucket
18
- * to fixed-window because that's what models cleanly in SQL
19
- * the operator-facing config keys change accordingly.
24
+ * Cluster opt-in implies fixed-window because that's what models
25
+ * cleanly in SQL the operator-facing config keys for the
26
+ * cluster backend match the fixed-window memory shape.
20
27
  *
21
28
  * Operators can also pass a custom `{ take, reset }` object as the
22
29
  * backend for Redis / Memcached / etc.
@@ -30,15 +37,21 @@
30
37
  * skipPaths: [] // string-prefix or regex matchers
31
38
  * scope: 'global' | 'per-route' (default 'global')
32
39
  * backend: 'memory' (default) | 'cluster' | { take, reset, gc? }
40
+ * algorithm: 'token-bucket' (default) | 'fixed-window'
41
+ * // memory backend only; ignored
42
+ * // for cluster backend (which is
43
+ * // always fixed-window) and custom
44
+ * // backend objects (operator decides)
33
45
  *
34
- * // Memory-backend tuning (token bucket):
46
+ * // Memory backend, token-bucket algorithm:
35
47
  * burst: 60 // initial token bucket size
36
48
  * refillPerSecond: 10 // sustained throughput
37
49
  *
38
- * // Cluster-backend tuning (fixed window):
39
- * limit: 60 // max requests per window
50
+ * // Memory backend, fixed-window algorithm + cluster backend:
51
+ * max: 60 // max requests per window (memory only)
52
+ * limit: 60 // alias of `max` (cluster backend uses this name)
40
53
  * windowMs: C.TIME.minutes(1) // window duration
41
- * pruneIntervalMs: C.TIME.minutes(5) // how often the leader prunes expired rows
54
+ * pruneIntervalMs: C.TIME.minutes(5) // cluster: how often the leader prunes expired rows
42
55
  * }
43
56
  *
44
57
  * Audit: every limit hit emits system.ratelimit.block with the key + path.
@@ -74,9 +87,25 @@ function _requirePositiveNumber(name, value) {
74
87
  }
75
88
  }
76
89
 
77
- // ---- Memory backend (token bucket) ----
90
+ // ---- Memory backend ----
91
+ //
92
+ // `algorithm: "token-bucket"` (default) — smoothed throughput.
93
+ // `algorithm: "fixed-window"` — per-key counter resets at the start of
94
+ // each window. Boundary-burst tradeoff
95
+ // in exchange for matching the cluster
96
+ // backend's shape without an SQL hop.
78
97
 
79
98
  function _memoryBackend(opts) {
99
+ var algorithm = opts.algorithm || "token-bucket";
100
+ if (algorithm !== "token-bucket" && algorithm !== "fixed-window") {
101
+ throw new Error("middleware.rateLimit: algorithm must be 'token-bucket' or 'fixed-window', got " +
102
+ JSON.stringify(algorithm));
103
+ }
104
+ if (algorithm === "fixed-window") return _memoryFixedWindowBackend(opts);
105
+ return _memoryTokenBucketBackend(opts);
106
+ }
107
+
108
+ function _memoryTokenBucketBackend(opts) {
80
109
  // Default 1-per-second tokens fully refilled in 1 minute = 60 tokens.
81
110
  var burst = opts.burst != null ? opts.burst : C.TIME.minutes(1) / C.TIME.seconds(1);
82
111
  var refillPerSecond = opts.refillPerSecond != null ? opts.refillPerSecond : 10;
@@ -138,6 +167,71 @@ function _memoryBackend(opts) {
138
167
  return { take: take, reset: reset, close: close };
139
168
  }
140
169
 
170
+ // Fixed-window in-memory algorithm — per-key counter that resets at the
171
+ // start of each window. Same shape as the cluster backend but without
172
+ // the SQL hop, so single-process apps that want fixed-window semantics
173
+ // (e.g. matching a cluster-backend deploy in dev) avoid setting up a DB.
174
+ function _memoryFixedWindowBackend(opts) {
175
+ // `max` is the memory-fixed-window operator-facing name. `limit` is
176
+ // accepted as an alias so a config can switch from the cluster
177
+ // backend to memory + fixed-window without renaming opts.
178
+ var max = opts.max != null ? opts.max
179
+ : opts.limit != null ? opts.limit
180
+ : C.TIME.minutes(1) / C.TIME.seconds(1);
181
+ var windowMs = opts.windowMs != null ? opts.windowMs : C.TIME.minutes(1);
182
+ _requirePositiveNumber("max", max);
183
+ _requirePositiveNumber("windowMs", windowMs);
184
+
185
+ var counters = new Map();
186
+
187
+ // Periodic GC of stale counters so the map doesn't grow unbounded.
188
+ var gcInterval = safeAsync.repeating(function () {
189
+ var now = Date.now();
190
+ for (var k of counters.keys()) {
191
+ var c = counters.get(k);
192
+ if (c.windowStart + windowMs * 2 < now) counters.delete(k);
193
+ }
194
+ }, C.TIME.minutes(5), { name: "rate-limit-fixed-window-gc" });
195
+
196
+ // Synchronous take — same hot-path shape as the token-bucket backend
197
+ // so the middleware doesn't pay a microtask cost when memory-fixed
198
+ // is selected.
199
+ function take(key, _cost) {
200
+ var now = Date.now();
201
+ var windowStart = Math.floor(now / windowMs) * windowMs;
202
+ var c = counters.get(key);
203
+ if (!c || c.windowStart !== windowStart) {
204
+ c = { windowStart: windowStart, count: 0 };
205
+ counters.set(key, c);
206
+ }
207
+ c.count += 1;
208
+ if (c.count <= max) {
209
+ return {
210
+ allowed: true,
211
+ limit: max,
212
+ remaining: Math.max(0, max - c.count),
213
+ retryAfter: 0,
214
+ };
215
+ }
216
+ var retryMs = (windowStart + windowMs) - now;
217
+ return {
218
+ allowed: false,
219
+ limit: max,
220
+ remaining: 0,
221
+ retryAfter: Math.max(1, Math.ceil(retryMs / C.TIME.seconds(1))),
222
+ };
223
+ }
224
+
225
+ function reset(key) { counters.delete(key); }
226
+
227
+ function close() {
228
+ try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
229
+ counters.clear();
230
+ }
231
+
232
+ return { take: take, reset: reset, close: close };
233
+ }
234
+
141
235
  // ---- Cluster backend (fixed-window counter, SQL-backed) ----
142
236
 
143
237
  function _clusterBackend(opts) {
@@ -241,15 +335,61 @@ function _resolveBackend(opts) {
241
335
  "' (must be 'memory', 'cluster', or { take, reset })");
242
336
  }
243
337
 
338
+ /**
339
+ * @primitive b.middleware.rateLimit
340
+ * @signature b.middleware.rateLimit(req, res, next)
341
+ * @since 0.1.0
342
+ * @related b.middleware.dailyByteQuota, b.middleware.botGuard
343
+ *
344
+ * Pluggable-backend rate limiter. Constructed via
345
+ * `b.middleware.rateLimit(opts)`; the resulting middleware has the
346
+ * `(req, res, next)` shape shown above. Default `memory` backend offers
347
+ * `token-bucket` (smooths bursts) and `fixed-window` algorithms;
348
+ * `cluster` backend uses `_blamejs_rate_limit_counters` for
349
+ * multi-node accurate fixed-window counts. Operators bring their
350
+ * own `{ take, reset }` for Redis / Memcached. Per-IP by default;
351
+ * `keyFn(req)` overrides for per-user / per-API-key / per-route.
352
+ * Refuses with HTTP 429 + `X-RateLimit-*` headers and emits
353
+ * `system.ratelimit.block` audit on every hit.
354
+ *
355
+ * @opts
356
+ * {
357
+ * keyFn: function(req): string,
358
+ * statusOnLimit: number, // default 429
359
+ * bodyOnLimit: string, // default "Too Many Requests"
360
+ * header: boolean, // default true
361
+ * skipPaths: Array<string|RegExp>,
362
+ * scope: "global"|"per-route",
363
+ * backend: "memory"|"cluster"|{ take, reset, gc },
364
+ * algorithm: "token-bucket"|"fixed-window",
365
+ * burst: number,
366
+ * refillPerSecond: number,
367
+ * max: number,
368
+ * limit: number,
369
+ * windowMs: number,
370
+ * pruneIntervalMs: number,
371
+ * trustProxy: boolean|number,
372
+ * }
373
+ *
374
+ * @example
375
+ * var b = require("@blamejs/core");
376
+ * var app = b.router.create();
377
+ * app.use(b.middleware.rateLimit({
378
+ * backend: "memory",
379
+ * algorithm: "token-bucket",
380
+ * burst: 60,
381
+ * refillPerSecond: 10,
382
+ * }));
383
+ */
244
384
  function create(opts) {
245
385
  opts = opts || {};
246
386
  validateOpts(opts, [
247
387
  "keyFn", "statusOnLimit", "bodyOnLimit", "header", "skipPaths", "scope",
248
- "backend", "trustProxy",
249
- // memory backend
388
+ "backend", "trustProxy", "algorithm",
389
+ // memory backend (token-bucket)
250
390
  "burst", "refillPerSecond",
251
- // cluster backend
252
- "limit", "windowMs", "pruneIntervalMs",
391
+ // memory backend (fixed-window) + cluster backend
392
+ "max", "limit", "windowMs", "pruneIntervalMs",
253
393
  ], "middleware.rateLimit");
254
394
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
255
395
  ? opts.trustProxy : false;
@@ -353,6 +493,8 @@ function create(opts) {
353
493
  module.exports = {
354
494
  create: create,
355
495
  // Backends exported for tests + advanced operator wiring.
356
- _memoryBackend: _memoryBackend,
357
- _clusterBackend: _clusterBackend,
496
+ _memoryBackend: _memoryBackend,
497
+ _memoryTokenBucketBackend: _memoryTokenBucketBackend,
498
+ _memoryFixedWindowBackend: _memoryFixedWindowBackend,
499
+ _clusterBackend: _clusterBackend,
358
500
  };
@@ -1,23 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * Request-ID middleware. Propagates an existing X-Request-Id header (or
4
- * trace ID from upstream) when present and well-formed; otherwise
5
- * generates a fresh 32-hex value. Sets req.requestId AND emits the same
6
- * value as a response header so downstream services + auditors can
7
- * correlate.
8
- *
9
- * Threading the request ID into audit.record() metadata is what makes
10
- * the cross-event correlation traceable; apps should pass this through
11
- * to every audit.record() they call within the request lifecycle.
12
- *
13
- * Options:
14
- * {
15
- * headerName: 'X-Request-Id'
16
- * trustUpstream: true // propagate upstream id if it matches
17
- * // the format check; false → always
18
- * // generate fresh
19
- * formatRegex: /^[A-Za-z0-9._-]{8,128}$/
20
- * }
3
+ * Request-ID middleware. Propagates an existing X-Request-Id header
4
+ * when present and well-formed; otherwise generates a fresh 32-hex
5
+ * value. Sets req.requestId AND emits the same value as a response
6
+ * header so downstream services + auditors can correlate.
21
7
  */
22
8
  var C = require("../constants");
23
9
  var { generateToken } = require("../crypto");
@@ -30,6 +16,38 @@ var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
30
16
  // can't drive ReDoS even against a careless operator pattern.
31
17
  var MAX_INBOUND_LEN = C.BYTES.bytes(256);
32
18
 
19
+ /**
20
+ * @primitive b.middleware.requestId
21
+ * @signature b.middleware.requestId(req, res, next)
22
+ * @since 0.1.0
23
+ * @related b.middleware.requestLog, b.middleware.traceLogCorrelation
24
+ *
25
+ * Sets a stable correlation id on every request. Constructed via
26
+ * the factory call `b.middleware.requestId(opts)`; the resulting
27
+ * middleware has the `(req, res, next)` shape shown above.
28
+ * Propagates a trusted inbound `X-Request-Id` (or operator-named
29
+ * header) when it matches the format regex; otherwise generates a
30
+ * fresh 16-byte hex token. The id lands on `req.requestId` and on
31
+ * the response header so downstream services + the framework's
32
+ * audit log can correlate the request across hops. Mount FIRST in
33
+ * the chain — every later primitive expects `req.requestId` to
34
+ * be present for log lines and audit-record metadata.
35
+ *
36
+ * @opts
37
+ * {
38
+ * headerName: string, // default "X-Request-Id"
39
+ * trustUpstream: boolean, // default true; false → always re-mint
40
+ * formatRegex: RegExp, // default /^[A-Za-z0-9._-]{8,128}$/
41
+ * }
42
+ *
43
+ * @example
44
+ * var b = require("@blamejs/core");
45
+ * var app = b.router.create();
46
+ * app.use(b.middleware.requestId({ trustUpstream: true }));
47
+ * app.get("/health", function (req, res) {
48
+ * res.end(req.requestId);
49
+ * });
50
+ */
33
51
  function create(opts) {
34
52
  opts = opts || {};
35
53
  validateOpts(opts, [
@@ -43,6 +43,43 @@ function _defaultLevel(status) {
43
43
  return "info";
44
44
  }
45
45
 
46
+ /**
47
+ * @primitive b.middleware.requestLog
48
+ * @signature b.middleware.requestLog(req, res, next)
49
+ * @since 0.1.0
50
+ * @related b.middleware.requestId, b.middleware.traceLogCorrelation
51
+ *
52
+ * HTTP access-log middleware. Constructed via
53
+ * `b.middleware.requestLog(opts)`; the resulting middleware has the
54
+ * `(req, res, next)` shape shown above. Emits one structured log entry per
55
+ * request via the operator-supplied `b.log` instance, capturing
56
+ * method / path / status / durationMs / bytes / actorIp / userAgent
57
+ * / requestId. Reads the final status via
58
+ * `b.requestHelpers.captureResponseStatus` so handlers using any
59
+ * shape (`writeHead` / `statusCode =` / fluent `status(...).send`)
60
+ * report correctly. `levelFn(status)` defaults to 5xx=error,
61
+ * 4xx=warn, else info; pass a string `level` or custom function for
62
+ * different policies. `trustProxy` gates `X-Forwarded-For`
63
+ * consumption.
64
+ *
65
+ * @opts
66
+ * {
67
+ * logger: object, // required b.log instance
68
+ * skipPaths: Array<string|RegExp>,
69
+ * trustProxy: boolean|number,
70
+ * level: string,
71
+ * levelFn: function(status): string,
72
+ * fields: string[],
73
+ * }
74
+ *
75
+ * @example
76
+ * var b = require("@blamejs/core");
77
+ * var app = b.router.create();
78
+ * app.use(b.middleware.requestLog({
79
+ * logger: b.log.boot("http"),
80
+ * skipPaths: ["/healthz"],
81
+ * }));
82
+ */
46
83
  function create(opts) {
47
84
  opts = opts || {};
48
85
  validateOpts(opts, [
@@ -44,6 +44,35 @@ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
44
44
  res.end(body);
45
45
  }
46
46
 
47
+ /**
48
+ * @primitive b.middleware.requireAal
49
+ * @signature b.middleware.requireAal(opts)
50
+ * @since 0.1.0
51
+ * @related b.middleware.requireStepUp, b.middleware.requireAuth
52
+ *
53
+ * Gates routes by NIST SP 800-63-4 Authenticator Assurance Level
54
+ * (AAL1 / AAL2 / AAL3). Reads the actual band from `req.user.aal`
55
+ * by default; operators with a different shape pass `getAal(req)`.
56
+ * Refuses below-minimum requests with HTTP 401 +
57
+ * `WWW-Authenticate: AAL-StepUp realm="<X>", required="<minimum>"`
58
+ * — the bespoke scheme name signals the frontend to trigger a
59
+ * step-up flow (re-prompt for TOTP / passkey). Throws at create()
60
+ * on an invalid `minimum` band. Emits `auth.aal.granted` /
61
+ * `auth.aal.denied` audit events.
62
+ *
63
+ * @opts
64
+ * {
65
+ * minimum: "AAL1"|"AAL2"|"AAL3", // required
66
+ * getAal: function(req): string,
67
+ * realm: string,
68
+ * audit: boolean, // default true
69
+ * }
70
+ *
71
+ * @example
72
+ * var b = require("@blamejs/core");
73
+ * var app = b.router.create();
74
+ * app.use("/admin", b.middleware.requireAal({ minimum: "AAL2" }));
75
+ */
47
76
  function create(opts) {
48
77
  opts = opts || {};
49
78
  validateOpts(opts, [
@@ -47,6 +47,38 @@ function _defaultPrefersJson(req) {
47
47
  return false;
48
48
  }
49
49
 
50
+ /**
51
+ * @primitive b.middleware.requireAuth
52
+ * @signature b.middleware.requireAuth(req, res, next)
53
+ * @since 0.1.0
54
+ * @related b.middleware.attachUser, b.middleware.bearerAuth, b.middleware.requireAal
55
+ *
56
+ * Gates routes that require an authenticated user. Constructed via
57
+ * `b.middleware.requireAuth(opts)`; the resulting middleware has
58
+ * the `(req, res, next)` shape shown above. Mount AFTER
59
+ * `attachUser`; this middleware reads `req.user` and either passes
60
+ * the request or rejects. JSON-preferring callers (Accept includes
61
+ * `application/json` or `X-Requested-With: XMLHttpRequest`) get 401
62
+ * `application/json`; browser-preferring with `redirectTo` get 302
63
+ * Location; otherwise 401 `text/plain`. The REQUEST Content-Type
64
+ * is intentionally NOT a signal — what the client SENT is not
65
+ * what they want BACK. Always emits `auth.required.denied` audit
66
+ * (method + path + client IP, no body content).
67
+ *
68
+ * @opts
69
+ * {
70
+ * redirectTo: string, // 302 location for browser
71
+ * prefersJson: function(req): boolean,
72
+ * errorMessage: string, // default "Authentication required."
73
+ * audit: boolean, // default true
74
+ * }
75
+ *
76
+ * @example
77
+ * var b = require("@blamejs/core");
78
+ * var app = b.router.create();
79
+ * app.use(b.middleware.attachUser({ userLoader: async function () { return { id: 1 }; } }));
80
+ * app.use(b.middleware.requireAuth({ redirectTo: "/login" }));
81
+ */
50
82
  function create(opts) {
51
83
  opts = opts || {};
52
84
  validateOpts(opts, [
@@ -70,6 +70,47 @@ function _timingSafeStringEqual(a, b) {
70
70
  return crypto().timingSafeEqual(Buffer.from(a), Buffer.from(b));
71
71
  }
72
72
 
73
+ /**
74
+ * @primitive b.middleware.requireBoundKey
75
+ * @signature b.middleware.requireBoundKey(opts)
76
+ * @since 0.1.0
77
+ * @related b.middleware.bearerAuth, b.middleware.requireMtls
78
+ *
79
+ * Bearer-API-key auth with scope + bound-fields + peer-cert
80
+ * fingerprint binding. Covers the service-to-service /
81
+ * partner-webhook / CI-runner case where a stable API key is
82
+ * registered with `{ scopes, boundFields, peerCertFingerprints }`.
83
+ * The middleware verifies the inbound `Bearer` token, checks
84
+ * scopes against `requiredScopes`, pulls each bound field via
85
+ * the operator-supplied `getBoundField[name](req)` and compares to
86
+ * the registered value, and (when registered) compares the
87
+ * peer-cert fingerprint to the allowlist. Fails closed on resolver
88
+ * error / undefined return. Refuses with HTTP 401/403 + structured
89
+ * JSON identifying which check failed; audits the api-key id (not
90
+ * the secret) on every decision.
91
+ *
92
+ * @opts
93
+ * {
94
+ * resolver: async function(apiKey): { id, scopes, boundFields, peerCertFingerprints } | null, // required
95
+ * requiredScopes: string[],
96
+ * getBoundField: Record<string, function(req): string|null>,
97
+ * tolerateMissingPeerCert: boolean,
98
+ * errorMessage: string,
99
+ * auditAction: string,
100
+ * audit: object,
101
+ * }
102
+ *
103
+ * @example
104
+ * var b = require("@blamejs/core");
105
+ * var app = b.router.create();
106
+ * app.post("/webhook", b.middleware.requireBoundKey({
107
+ * resolver: async function (apiKey) {
108
+ * if (apiKey === "valid-key") return { id: "k1", scopes: ["webhook.ingest"], boundFields: {} };
109
+ * return null;
110
+ * },
111
+ * requiredScopes: ["webhook.ingest"],
112
+ * }));
113
+ */
73
114
  function create(opts) {
74
115
  opts = opts || {};
75
116
  validateOpts(opts, [
@@ -39,6 +39,38 @@ function _normalizeAllowed(types) {
39
39
  return out;
40
40
  }
41
41
 
42
+ /**
43
+ * @primitive b.middleware.requireContentType
44
+ * @signature b.middleware.requireContentType(allowed, opts)
45
+ * @since 0.1.0
46
+ * @related b.middleware.requireMethods, b.middleware.bodyParser
47
+ *
48
+ * Refuses requests with a body (POST/PUT/PATCH by default) whose
49
+ * `Content-Type` header isn't in the operator-supplied allowlist.
50
+ * Defends against MIME-type confusion: a route that processes JSON
51
+ * shouldn't accept `application/x-www-form-urlencoded` even if the
52
+ * body parses, and vice versa. Refuses with HTTP 415 + `Accept:`
53
+ * listing the allowed types per RFC 9110 §15.5.16, BEFORE the
54
+ * body parser runs. Idempotent verbs (GET / HEAD / DELETE /
55
+ * OPTIONS) bypass by default; operators with rare DELETE-with-body
56
+ * shapes pass `methods` to override. Throws on empty / non-array
57
+ * allowlist.
58
+ *
59
+ * @opts
60
+ * {
61
+ * methods: string[], // override default ["POST", "PUT", "PATCH"]
62
+ * audit: boolean, // default true
63
+ * }
64
+ *
65
+ * @example
66
+ * var b = require("@blamejs/core");
67
+ * var app = b.router.create();
68
+ * app.post("/api/echo",
69
+ * b.middleware.requireContentType(["application/json"]),
70
+ * b.middleware.bodyParser({ json: { limit: 1024 } }),
71
+ * function (req, res) { res.end(JSON.stringify(req.body)); }
72
+ * );
73
+ */
42
74
  function create(allowed, opts) {
43
75
  var normalized = _normalizeAllowed(allowed);
44
76
  if (!normalized) {
@@ -22,6 +22,33 @@ var RequireMethodsError = defineClass("RequireMethodsError", { alwaysPermanent:
22
22
 
23
23
  var observability = lazyRequire(function () { return require("../observability"); });
24
24
 
25
+ /**
26
+ * @primitive b.middleware.requireMethods
27
+ * @signature b.middleware.requireMethods(allowed, opts)
28
+ * @since 0.1.0
29
+ * @related b.middleware.requireContentType
30
+ *
31
+ * Refuses HTTP methods outside the operator-supplied allowlist.
32
+ * Defends against unexpected verb routing — many CVE-class bugs
33
+ * trace to a route wired for GET that accidentally accepts arbitrary
34
+ * verbs (PROPFIND, OPTIONS, custom). Refuses with HTTP 405 +
35
+ * `Allow:` header listing the allowed methods per RFC 9110 §15.5.6.
36
+ * Throws at create-time on empty / non-array allowlist or methods
37
+ * containing whitespace/separator characters.
38
+ *
39
+ * @opts
40
+ * {
41
+ * audit: boolean, // default true
42
+ * }
43
+ *
44
+ * @example
45
+ * var b = require("@blamejs/core");
46
+ * var app = b.router.create();
47
+ * app.use("/api",
48
+ * b.middleware.requireMethods(["GET", "POST"]),
49
+ * function (req, res) { res.end("ok"); }
50
+ * );
51
+ */
25
52
  function create(allowed, opts) {
26
53
  if (!Array.isArray(allowed) || allowed.length === 0) {
27
54
  throw new RequireMethodsError("require-methods/no-allowlist",
@@ -63,6 +63,39 @@ function _normalizeFingerprintEntry(entry) {
63
63
  return entry;
64
64
  }
65
65
 
66
+ /**
67
+ * @primitive b.middleware.requireMtls
68
+ * @signature b.middleware.requireMtls(opts)
69
+ * @since 0.1.0
70
+ * @related b.middleware.requireBoundKey, b.middleware.bearerAuth
71
+ *
72
+ * Soft-enforcement gate for routes that require an authenticated
73
+ * client certificate. Refuses with HTTP 401 when no peer cert is
74
+ * presented, when the TLS layer marks `req.client.authorized ===
75
+ * false`, when the SHA3-512 fingerprint isn't on the operator
76
+ * allowlist, or when it appears on the denylist. Allowlist of
77
+ * null / empty means "any peer cert authorized at the TLS layer
78
+ * is fine"; non-empty allowlist additionally requires fingerprint
79
+ * match. Pair with `b.app({ tlsOptions: { requestCert: true, ca:
80
+ * [...] } })` so the TLS layer captures the client cert.
81
+ *
82
+ * @opts
83
+ * {
84
+ * fingerprintAllowList: string[],
85
+ * denyList: string[],
86
+ * onAuthenticated: function(req, res, next): void,
87
+ * auditAction: string,
88
+ * errorMessage: string,
89
+ * audit: object,
90
+ * }
91
+ *
92
+ * @example
93
+ * var b = require("@blamejs/core");
94
+ * var app = b.router.create();
95
+ * app.use("/internal", b.middleware.requireMtls({
96
+ * fingerprintAllowList: ["AB:CD:EF:01:23:45"],
97
+ * }));
98
+ */
66
99
  function create(opts) {
67
100
  opts = opts || {};
68
101
  validateOpts(opts, [
@@ -75,6 +75,43 @@ function _writeChallenge(res, challenge, body, statusCode) {
75
75
  res.end(json);
76
76
  }
77
77
 
78
+ /**
79
+ * @primitive b.middleware.requireStepUp
80
+ * @signature b.middleware.requireStepUp(opts)
81
+ * @since 0.1.0
82
+ * @related b.middleware.requireAal, b.middleware.bearerAuth
83
+ *
84
+ * Gates routes per RFC 9470 OAuth 2.0 Step-Up Authentication
85
+ * Challenge. Mount AFTER `attachUser` / `bearerAuth` so the
86
+ * request carries verified token claims. Refuses with HTTP 401 +
87
+ * `WWW-Authenticate: Bearer error="insufficient_user_authentication",
88
+ * acr_values="...", max_age="..."`. With `acceptGrant: true`
89
+ * (default) the middleware first checks for a fresh
90
+ * `b.auth.stepUp.grant` token in `X-Step-Up-Grant` so a
91
+ * multi-step flow doesn't re-prompt on every action. Never weakens
92
+ * defaults to accommodate missing IdP claims — operators configure
93
+ * the IdP to emit `acr` / `auth_time` / `amr` correctly.
94
+ *
95
+ * @opts
96
+ * {
97
+ * requirement: { acr, acrValues, maxAge, requiredAmr, phishingResistant }, // required
98
+ * getClaims: function(req): object,
99
+ * realm: string,
100
+ * acceptGrant: boolean, // default true
101
+ * grantHeader: string, // default "X-Step-Up-Grant"
102
+ * grantScope: string,
103
+ * errorDescription: string,
104
+ * audit: boolean, // default true
105
+ * }
106
+ *
107
+ * @example
108
+ * var b = require("@blamejs/core");
109
+ * var app = b.router.create();
110
+ * app.use("/billing/transfer", b.middleware.requireStepUp({
111
+ * requirement: { acr: "high", maxAge: 300 },
112
+ * realm: "billing-api",
113
+ * }));
114
+ */
78
115
  function create(opts) {
79
116
  opts = opts || {};
80
117
  validateOpts(opts, [