@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
@@ -24,15 +24,9 @@
24
24
  * });
25
25
  * router.use(quota);
26
26
  *
27
- * The middleware fires twice per request:
28
- * - On entry: peek the running counter, refuse if already past quota
29
- * - On res.end / res.write: account both directions of byte transfer
30
- *
31
- * Single-node memory backend uses a Map<ip, { bins: Uint32Array(24),
32
- * windowStartHour: number }>. Each bin holds bytes for one rolling hour;
33
- * sweeping happens on every account() call so cold storage doesn't grow
34
- * unbounded. Cluster-aware operators wire opts.cache (b.cache instance)
35
- * and the same pattern runs in the shared backend.
27
+ * The middleware composes b.network.byteQuota handlers that already
28
+ * know the byte cost of an op call b.network.byteQuota.check / record
29
+ * directly without going through the middleware lifecycle.
36
30
  *
37
31
  * Failure modes:
38
32
  * - cache backend unreachable → fail-open (count drops, request
@@ -45,6 +39,7 @@
45
39
  var C = require("../constants");
46
40
  var defineClass = require("../framework-error").defineClass;
47
41
  var lazyRequire = require("../lazy-require");
42
+ var networkByteQuota = require("../network-byte-quota");
48
43
  var validateOpts = require("../validate-opts");
49
44
 
50
45
  var audit = lazyRequire(function () { return require("../audit"); });
@@ -53,9 +48,6 @@ var requestHelpers = lazyRequire(function () { return require("../request-helper
53
48
 
54
49
  var DailyByteQuotaError = defineClass("DailyByteQuotaError", { alwaysPermanent: true });
55
50
 
56
- var BINS_PER_DAY = 24; // allow:raw-byte-literal — 24 hours in a day
57
- var BIN_MS = C.TIME.hours(1);
58
-
59
51
  // Default getKey — req.ip OR the trusted-proxy-resolved peer address
60
52
  // when the operator wired b.middleware.requestId or similar earlier in
61
53
  // the chain. We don't try to be clever here: req.ip is the canonical
@@ -64,73 +56,40 @@ function _defaultGetKey(req) {
64
56
  return requestHelpers().clientIp(req, { trustProxy: false });
65
57
  }
66
58
 
67
- function _hourBin(nowMs) { return Math.floor(nowMs / BIN_MS); }
68
- function _newEntry() { return { bins: new Array(BINS_PER_DAY).fill(0), startHour: 0 }; }
69
-
70
- // Shared sliding-window helper — both backends call this so the
71
- // per-bin shift / zero / total math lives in one place. Returns the
72
- // (possibly mutated) entry; caller persists if the entry is shared
73
- // state (cache backend writes back).
74
- function _slideAndSum(entry, nowHour) {
75
- if (entry.startHour === 0) entry.startHour = nowHour - (BINS_PER_DAY - 1);
76
- var advance = nowHour - (entry.startHour + (BINS_PER_DAY - 1));
77
- var moved = false;
78
- if (advance > 0) {
79
- moved = true;
80
- if (advance >= BINS_PER_DAY) {
81
- for (var i = 0; i < BINS_PER_DAY; i++) entry.bins[i] = 0;
82
- } else {
83
- for (var j = 0; j < BINS_PER_DAY - advance; j++) entry.bins[j] = entry.bins[j + advance];
84
- for (var k = BINS_PER_DAY - advance; k < BINS_PER_DAY; k++) entry.bins[k] = 0;
85
- }
86
- entry.startHour = nowHour - (BINS_PER_DAY - 1);
87
- }
88
- var total = 0;
89
- for (var t = 0; t < BINS_PER_DAY; t++) total += entry.bins[t];
90
- return { entry: entry, total: total, moved: moved };
91
- }
92
-
93
- function _memoryBackend() {
94
- var store = new Map();
95
- function _get(key) {
96
- var entry = store.get(key);
97
- if (!entry) { entry = _newEntry(); store.set(key, entry); }
98
- return entry;
99
- }
100
- return {
101
- async total(key, nowMs) {
102
- return _slideAndSum(_get(key), _hourBin(nowMs)).total;
103
- },
104
- async account(key, bytes, nowMs) {
105
- var slid = _slideAndSum(_get(key), _hourBin(nowMs));
106
- slid.entry.bins[BINS_PER_DAY - 1] += bytes;
107
- },
108
- _resetForTest: function () { store.clear(); },
109
- };
110
- }
111
-
112
- function _cacheBackend(cache) {
113
- function _key(k) { return "dailyByteQuota:" + k; }
114
- async function _read(key) {
115
- var raw = await cache.get(_key(key));
116
- return raw && typeof raw === "object" && Array.isArray(raw.bins) ? raw : _newEntry();
117
- }
118
- return {
119
- async total(key, nowMs) {
120
- var entry = await _read(key);
121
- var slid = _slideAndSum(entry, _hourBin(nowMs));
122
- if (slid.moved) await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
123
- return slid.total;
124
- },
125
- async account(key, bytes, nowMs) {
126
- var entry = await _read(key);
127
- var slid = _slideAndSum(entry, _hourBin(nowMs));
128
- slid.entry.bins[BINS_PER_DAY - 1] += bytes;
129
- await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
130
- },
131
- };
132
- }
133
-
59
+ /**
60
+ * @primitive b.middleware.dailyByteQuota
61
+ * @signature b.middleware.dailyByteQuota(opts)
62
+ * @since 0.1.0
63
+ * @related b.middleware.rateLimit
64
+ *
65
+ * Per-IP rolling 24-hour byte budget. Tracks request + response
66
+ * bytes per peer key (default: client IP). When a peer exceeds the
67
+ * configured quota, further requests are refused with HTTP 429 +
68
+ * `Retry-After`. The window slides per-second a peer can't reset
69
+ * by waiting past midnight. Composes `b.network.byteQuota`; handlers
70
+ * that already know the byte cost of an op can call
71
+ * `b.network.byteQuota.check`/`record` directly. Fails open (request
72
+ * proceeds, audit emitted) when the backing cache is unreachable.
73
+ *
74
+ * @opts
75
+ * {
76
+ * bytesPerDay: number, // required, positive, finite
77
+ * getKey: function(req): string|null, // default: req client IP
78
+ * cache: object, // null = in-memory single-node
79
+ * onExceeded: function(req, res, info): void,
80
+ * skipPaths: string[],
81
+ * now: function(): number,
82
+ * audit: boolean, // default true
83
+ * }
84
+ *
85
+ * @example
86
+ * var b = require("@blamejs/core");
87
+ * var app = b.router.create();
88
+ * app.use(b.middleware.dailyByteQuota({
89
+ * bytesPerDay: b.constants.BYTES.gib(2),
90
+ * skipPaths: ["/healthz"],
91
+ * }));
92
+ */
134
93
  function create(opts) {
135
94
  opts = opts || {};
136
95
  validateOpts(opts, [
@@ -149,9 +108,17 @@ function create(opts) {
149
108
  var onExceeded = typeof opts.onExceeded === "function" ? opts.onExceeded : null;
150
109
  var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
151
110
  var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
152
- var backend = opts.cache && typeof opts.cache.get === "function"
153
- ? _cacheBackend(opts.cache)
154
- : _memoryBackend();
111
+
112
+ // Compose the standalone primitive — the middleware drives the same
113
+ // counter store the operator-facing b.network.byteQuota.check /
114
+ // record API exposes. No parallel byte-counter implementation.
115
+ var quota = networkByteQuota.create({
116
+ bytesPerDay: bytesPerDay,
117
+ cache: opts.cache || null,
118
+ audit: false, // middleware emits its own per-rejection audits
119
+ now: now,
120
+ });
121
+ var backend = quota._backend;
155
122
 
156
123
  function _shouldSkip(req) {
157
124
  if (skipPaths.length === 0) return false;
@@ -204,7 +171,7 @@ function create(opts) {
204
171
  var info = {
205
172
  quota: bytesPerDay,
206
173
  total: total,
207
- retryAfterSec: Math.max(C.TIME.seconds(1) / C.TIME.seconds(1) | 0, Math.ceil(BIN_MS / C.TIME.seconds(1))),
174
+ retryAfterSec: Math.ceil(C.TIME.hours(1) / C.TIME.seconds(1)),
208
175
  };
209
176
  if (onExceeded) {
210
177
  try { return onExceeded(req, res, info); }
@@ -270,6 +237,7 @@ function create(opts) {
270
237
  module.exports = {
271
238
  create: create,
272
239
  DailyByteQuotaError: DailyByteQuotaError,
273
- _memoryBackend: _memoryBackend, // exported for tests
274
- BINS_PER_DAY: BINS_PER_DAY,
240
+ BINS_PER_DAY: networkByteQuota.BINS_PER_DAY,
241
+ // Backward-compat re-export for tests that mocked the in-process backend.
242
+ _memoryBackend: networkByteQuota._memoryBackend,
275
243
  };
@@ -107,6 +107,46 @@ function _defaultResponder(req, res, status, info) {
107
107
  res.end(JSON.stringify(info));
108
108
  }
109
109
 
110
+ /**
111
+ * @primitive b.middleware.dbRoleFor
112
+ * @signature b.middleware.dbRoleFor(opts)
113
+ * @since 0.1.0
114
+ * @related b.middleware.attachUser, b.externalDb.query
115
+ *
116
+ * Binds a request-time database role into AsyncLocalStorage so
117
+ * `b.externalDb.query` / `read` / `write` / `transaction` auto-route
118
+ * to the matching backend (`app_user`, `analytics_user`, etc.) without
119
+ * any operator threading of the role through the handler signature.
120
+ * Resolution order: operator-supplied `resolve(req)` → permissions
121
+ * RBAC `dbRoleFor` → `defaultRole` → null. Throws at create-time on
122
+ * bad opts (resolver/responder shape, malformed default-role
123
+ * identifier, out-of-range missing-role status). Runtime returns
124
+ * are validated against `safeSql.validateIdentifier` — a garbage
125
+ * resolver return surfaces as next(err), not silent fallthrough.
126
+ * Emits `db.role.switched` audit + `db.role.bound` observability.
127
+ *
128
+ * @opts
129
+ * {
130
+ * resolve: function(req): string|null,
131
+ * permissions: object, // b.permissions instance
132
+ * defaultRole: string,
133
+ * requireRole: boolean,
134
+ * missingRoleStatus: number, // default 401
135
+ * responder: function(req, res, status, info): void,
136
+ * audit: object,
137
+ * auditFailures: boolean, // default true
138
+ * auditSuccess: boolean, // default true
139
+ * }
140
+ *
141
+ * @example
142
+ * var b = require("@blamejs/core");
143
+ * var app = b.router.create();
144
+ * app.use(b.middleware.attachUser({ userLoader: async function () { return { id: 1 }; } }));
145
+ * app.use(b.middleware.dbRoleFor({
146
+ * defaultRole: "app_user",
147
+ * resolve: function (req) { return req.user && req.user.dbRole; },
148
+ * }));
149
+ */
110
150
  function create(opts) {
111
151
  opts = opts || {};
112
152
  validateOpts(opts, ALLOWED_OPTS, "middleware.dbRoleFor");
@@ -117,6 +117,46 @@ function _reconstructHtu(req) {
117
117
  return proto + "://" + host + path;
118
118
  }
119
119
 
120
+ /**
121
+ * @primitive b.middleware.dpop
122
+ * @signature b.middleware.dpop(opts)
123
+ * @since 0.1.0
124
+ * @related b.middleware.bearerAuth, b.auth.jwt
125
+ *
126
+ * RFC 9449 Demonstrating Proof of Possession (DPoP). Verifies the
127
+ * `DPoP` header on inbound requests, attaches `req.dpop = { header,
128
+ * payload, jkt }` for downstream handlers to bind to the access
129
+ * token's `cnf.jkt` claim, and refuses with HTTP 401 +
130
+ * `WWW-Authenticate: DPoP error="invalid_dpop_proof"` on any
131
+ * failure. Replay store enforces single-use proofs within
132
+ * `iatWindowSec`. Optional server-issued nonce (RFC 9449 §8) with
133
+ * `requireNonce: true` rotates a current/previous pair lazily so
134
+ * in-flight clients aren't kicked off at rotation. Algorithm
135
+ * allowlist defaults to ES256 / EdDSA / ML-DSA-87 (PQC-first).
136
+ *
137
+ * @opts
138
+ * {
139
+ * replayStore: object, // required
140
+ * algorithms: string[], // default ES256/EdDSA/ML-DSA-87
141
+ * iatWindowSec: number, // default 60
142
+ * getAccessToken: function(req): string|null,
143
+ * getNonce: async function(req): string|null,
144
+ * getHtu: function(req): string,
145
+ * nonceStore: object,
146
+ * nonceWindowSec: number,
147
+ * nonceRotateSec: number,
148
+ * requireNonce: boolean,
149
+ * audit: boolean, // default true
150
+ * }
151
+ *
152
+ * @example
153
+ * var b = require("@blamejs/core");
154
+ * var app = b.router.create();
155
+ * app.use("/api", b.middleware.dpop({
156
+ * replayStore: b.nonceStore.create({ backend: "memory" }),
157
+ * iatWindowSec: 60,
158
+ * }));
159
+ */
120
160
  function create(opts) {
121
161
  opts = opts || {};
122
162
  validateOpts(opts, [
@@ -1,24 +1,47 @@
1
1
  "use strict";
2
2
  /**
3
- * Error-handler middleware — thin adapter over lib/errors-page.
3
+ * Error-handler middleware — thin adapter over lib/error-page.
4
4
  *
5
5
  * Use this for the standard wiring path (`router.onError(b.middleware.
6
- * errorHandler())`). Constructs an errors-page handler and forwards
7
- * the (err, req, res, next) router signature into it. All classification,
8
- * rendering, content negotiation, dev/prod gating, and audit emission
9
- * lives in lib/errors-page — this file only wires the middleware
10
- * convention plus the audit-action override that preserves the
11
- * 'system.http.error' action name the framework's audit log already
12
- * uses for HTTP-layer failures.
13
- *
14
- * Options forward to errors-page.create with one default override:
15
- * auditAction: "system.http.error" (vs errors-page default "request.error")
16
- *
17
- * Plus a back-compat alias:
18
- * exposeStackInDev: forwards to opts.showStack (true|false)
6
+ * errorHandler())`). Constructs an error-page handler and forwards
7
+ * the (err, req, res, next) router signature into it. Classification,
8
+ * rendering, content negotiation, dev/prod gating, and audit
9
+ * emission all live in `b.errorPage` — this file only wires the
10
+ * router middleware convention plus the audit-action override that
11
+ * preserves the `system.http.error` action name the framework's
12
+ * audit log already uses for HTTP-layer failures.
19
13
  */
20
14
  var errorPage = require("../error-page");
21
15
 
16
+ /**
17
+ * @primitive b.middleware.errorHandler
18
+ * @signature b.middleware.errorHandler(err, req, res, next)
19
+ * @since 0.1.0
20
+ * @related b.errorPage.create
21
+ *
22
+ * Thin adapter over `lib/error-page`. Constructed via the factory
23
+ * call `b.middleware.errorHandler(opts)`; the resulting middleware
24
+ * has the `(err, req, res, next)` shape shown above. Forwards the
25
+ * router signature into an errors-page handler.
26
+ * Classification, rendering, content negotiation, and audit emission
27
+ * live in `b.errorPage`; this middleware only sets the audit action
28
+ * to `system.http.error` and defaults to JSON output (page-style
29
+ * HTML negotiation is reachable via `b.errorPage.create` directly).
30
+ *
31
+ * @opts
32
+ * {
33
+ * auditAction: string, // default "system.http.error"
34
+ * defaultFormat: "json"|"html"|"auto",// default "json"
35
+ * showStack: boolean, // dev-stack exposure
36
+ * exposeStackInDev: boolean, // back-compat alias for showStack
37
+ * // ...all other b.errorPage.create opts forward unchanged
38
+ * }
39
+ *
40
+ * @example
41
+ * var b = require("@blamejs/core");
42
+ * var app = b.router.create();
43
+ * app.onError(b.middleware.errorHandler({ showStack: false }));
44
+ */
22
45
  function create(opts) {
23
46
  opts = opts || {};
24
47
  var pageOpts = Object.assign({}, opts);
@@ -52,6 +52,45 @@ function _writeReject(res, message) {
52
52
  res.end(body);
53
53
  }
54
54
 
55
+ /**
56
+ * @primitive b.middleware.fetchMetadata
57
+ * @signature b.middleware.fetchMetadata(req, res, next)
58
+ * @since 0.1.0
59
+ * @related b.middleware.cors, b.middleware.csrfProtect
60
+ *
61
+ * Resource Isolation Policy enforced via Sec-Fetch-Site / -Mode /
62
+ * -Dest. Constructed via `b.middleware.fetchMetadata(opts)`; the
63
+ * resulting middleware has the `(req, res, next)` shape shown above. Refuses cross-site requests on state-changing methods
64
+ * unless the operator allowlists them — second-line defense
65
+ * alongside CSRF tokens. Same-origin / same-site requests pass
66
+ * through. Cross-site is refused with HTTP 403 unless `allowedDest`
67
+ * contains the request's `Sec-Fetch-Dest`. Legacy browsers without
68
+ * Sec-Fetch-* default to `allowMissing: true` so the gate doesn't
69
+ * break older clients — the browser-fetch-metadata isolation IS
70
+ * the value-add; non-browser callers carry their own auth threat
71
+ * model.
72
+ *
73
+ * @opts
74
+ * {
75
+ * allowSameSite: boolean, // default true
76
+ * allowCrossSite: boolean, // default false
77
+ * allowMissing: boolean, // default true
78
+ * allowedDest: string[],
79
+ * allowedNavigate: boolean, // default true
80
+ * methods: string[], // default POST/PUT/DELETE/PATCH
81
+ * audit: boolean, // default true
82
+ * }
83
+ *
84
+ * @example
85
+ * var b = require("@blamejs/core");
86
+ * var app = b.router.create();
87
+ * app.use("/api", b.middleware.fetchMetadata({
88
+ * allowSameSite: true,
89
+ * allowCrossSite: false,
90
+ * allowedDest: ["empty", "document"],
91
+ * allowedNavigate: true,
92
+ * }));
93
+ */
55
94
  function create(opts) {
56
95
  opts = opts || {};
57
96
  validateOpts(opts, [
@@ -30,6 +30,40 @@ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
30
30
 
31
31
  var contextMod = lazyRequire(function () { return require("../flag-evaluation-context"); });
32
32
 
33
+ /**
34
+ * @primitive b.middleware.flagContext
35
+ * @signature b.middleware.flagContext(opts)
36
+ * @since 0.1.0
37
+ * @related b.flagClient.getBoolean
38
+ *
39
+ * Extracts an OpenFeature evaluation context onto `req.flagCtx` so
40
+ * downstream handlers and multiple flag clients read a consistent
41
+ * context without re-deriving it per call. The middleware itself
42
+ * does NOT evaluate flags — pair with `flag.middleware()` for the
43
+ * request-attached accessor, or pass `req.flagCtx` directly to a
44
+ * flag client method when several clients with different providers
45
+ * share the same context. `userKey` (literal) takes precedence over
46
+ * `userKeyHeader`; `tenantKeyHeader` augments with tenantId; the
47
+ * operator-supplied `extractAttributes(req)` adds arbitrary fields.
48
+ *
49
+ * @opts
50
+ * {
51
+ * userKey: string,
52
+ * userKeyHeader: string,
53
+ * tenantKeyHeader: string,
54
+ * extractAttributes: function(req): object,
55
+ * }
56
+ *
57
+ * @example
58
+ * var b = require("@blamejs/core");
59
+ * var app = b.router.create();
60
+ * app.use(b.middleware.flagContext({
61
+ * userKeyHeader: "x-user-id",
62
+ * extractAttributes: function (req) {
63
+ * return { environment: "prod" };
64
+ * },
65
+ * }));
66
+ */
33
67
  function create(opts) {
34
68
  opts = opts || {};
35
69
  validateOpts(opts, [
@@ -68,6 +68,39 @@ function _emitAudit(audit, action, outcome, metadata) {
68
68
  } catch (_e) { /* drop-silent */ }
69
69
  }
70
70
 
71
+ /**
72
+ * @primitive b.middleware.gpc
73
+ * @signature b.middleware.gpc(opts)
74
+ * @since 0.1.0
75
+ * @compliance ccpa, modpa
76
+ * @related b.middleware.cookies
77
+ *
78
+ * Sec-GPC (Global Privacy Control) handler. Reads the `Sec-GPC: 1`
79
+ * inbound header and sets `req.gpcOptOut = true` for downstream
80
+ * consumers. Echoes `Sec-GPC-Status: honored` so the UA + auditors
81
+ * see the acknowledgement. Honoring Sec-GPC is legally required in
82
+ * California (CCPA/CPRA) and a growing list of US states; operators
83
+ * who don't process the listed purposes (`sale`, `share`,
84
+ * `targeted-ads`, `cross-context-behavioral-advertising`,
85
+ * `profiling`) still emit the acknowledgement so audits trace.
86
+ * Optional `consent` integration auto-records purpose withdrawal.
87
+ *
88
+ * @opts
89
+ * {
90
+ * mode: "enforce"|"audit-only", // default "enforce"
91
+ * consent: object, // b.consent instance
92
+ * statusHeader: boolean, // default true
93
+ * audit: object,
94
+ * }
95
+ *
96
+ * @example
97
+ * var b = require("@blamejs/core");
98
+ * var app = b.router.create();
99
+ * app.use(b.middleware.gpc({ mode: "enforce" }));
100
+ * app.get("/api/data", function (req, res) {
101
+ * res.end(req.gpcOptOut ? "minimal" : "full");
102
+ * });
103
+ */
71
104
  function create(opts) {
72
105
  opts = opts || {};
73
106
  var mode = opts.mode || "enforce";
@@ -159,6 +159,41 @@ function _detectIssues(headers, opts) {
159
159
  return issues;
160
160
  }
161
161
 
162
+ /**
163
+ * @primitive b.middleware.headers
164
+ * @signature b.middleware.headers(opts)
165
+ * @since 0.1.0
166
+ * @related b.middleware.cookies, b.middleware.bodyParser
167
+ *
168
+ * Inbound HTTP header threat detection. Validates header names
169
+ * against the RFC 9110 §5.1 token grammar and surfaces CRLF
170
+ * injection, RFC 9112 §6.1 CL+TE request-smuggling shapes, multiple
171
+ * `Content-Length` / `Transfer-Encoding` values, oversize header
172
+ * count / value, and deprecated `X-Forwarded-*` patterns when the
173
+ * operator hasn't opted into `trustProxy`. In `mode: "enforce"`
174
+ * (default) high-severity issues refuse with HTTP 400 + `Connection:
175
+ * close`; `audit-only` and `log-only` pass through but still emit
176
+ * audits.
177
+ *
178
+ * @opts
179
+ * {
180
+ * mode: "enforce"|"audit-only"|"log-only", // default "enforce"
181
+ * refuseOnHigh: boolean, // default true (enforce only)
182
+ * maxHeaderCount: number, // default 100
183
+ * maxValueBytes: number, // default 8 KiB
184
+ * trustProxy: boolean,
185
+ * audit: object,
186
+ * }
187
+ *
188
+ * @example
189
+ * var b = require("@blamejs/core");
190
+ * var app = b.router.create();
191
+ * app.use(b.middleware.headers({
192
+ * mode: "enforce",
193
+ * maxHeaderCount: 100,
194
+ * maxValueBytes: b.constants.BYTES.kib(8),
195
+ * }));
196
+ */
162
197
  function create(opts) {
163
198
  opts = opts || {};
164
199
  var mode = opts.mode || "enforce";
@@ -115,6 +115,52 @@ var TIER_SET = new Set(TIERS);
115
115
 
116
116
  var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
117
117
 
118
+ /**
119
+ * @primitive b.middleware.health
120
+ * @signature b.middleware.health(req, res, next)
121
+ * @since 0.1.0
122
+ * @related b.middleware.requestId
123
+ *
124
+ * Liveness / readiness / startup probe primitive. Constructed via
125
+ * `b.middleware.health(opts)` returning a controller exposing
126
+ * `registerCheck`, `markShuttingDown`, and `middleware()`; the
127
+ * `middleware()` value has the `(req, res, next)` shape shown above. Three tiers, each
128
+ * its own URL path: `/healthz` (process alive — orchestrator kill
129
+ * decision), `/readyz` (can serve traffic — LB route decision),
130
+ * `/startupz` (slow-init complete — Kubernetes startupProbe).
131
+ * `markShuttingDown()` flips ONLY readiness to 503 so LBs drain
132
+ * while `/healthz` keeps responding 200 through clean exit.
133
+ * Detail level defaults to "minimal" (`{ status }` only — no
134
+ * internal-state leakage); `detailLevel: "detailed"` and
135
+ * `detailPredicate(req)` gate the full per-check breakdown to
136
+ * authed endpoints. Per-check `Promise.all` + `withTimeout` so one
137
+ * stuck check can't block the others. Returns a controller exposing
138
+ * `registerCheck`, `markShuttingDown`, and `middleware()`.
139
+ *
140
+ * @opts
141
+ * {
142
+ * livenessPath: string, // default "/healthz"
143
+ * readinessPath: string, // default "/readyz"
144
+ * startupPath: string, // default "/startupz"
145
+ * detailLevel: "minimal"|"detailed", // default "minimal"
146
+ * detailPredicate: function(req): boolean,
147
+ * defaultTimeoutMs: number, // default 5000
148
+ * cacheMs: number, // default 0
149
+ * includeMeta: boolean,
150
+ * version: string,
151
+ * }
152
+ *
153
+ * @example
154
+ * var b = require("@blamejs/core");
155
+ * var app = b.router.create();
156
+ * var hc = b.middleware.health({
157
+ * livenessPath: "/healthz",
158
+ * readinessPath: "/readyz",
159
+ * defaultTimeoutMs: 5000,
160
+ * });
161
+ * hc.registerCheck("db", async function () { return { ok: true }; }, { tier: "readiness" });
162
+ * app.use(hc.middleware());
163
+ */
118
164
  function create(opts) {
119
165
  opts = opts || {};
120
166
  validateOpts(opts, [
@@ -81,6 +81,36 @@ function _matches(entry, actual) {
81
81
  return false;
82
82
  }
83
83
 
84
+ /**
85
+ * @primitive b.middleware.hostAllowlist
86
+ * @signature b.middleware.hostAllowlist(opts)
87
+ * @since 0.1.0
88
+ * @related b.middleware.networkAllowlist, b.middleware.cors
89
+ *
90
+ * DNS-rebinding defense. Refuses requests whose `Host` header
91
+ * doesn't match the operator-supplied allowlist. Wildcard-leading
92
+ * entries (`*.example.com`) match a single label. Entries without
93
+ * a port match any port. Refuses with HTTP 421 (Misdirected
94
+ * Request) by default. Operators behind a CDN that rewrites the
95
+ * Host set `hosts` to the post-rewrite values. Skip entirely for
96
+ * services that serve arbitrary subdomains by design (anyone-can-
97
+ * host-the-domain shapes).
98
+ *
99
+ * @opts
100
+ * {
101
+ * hosts: string[], // required, non-empty
102
+ * denyStatus: number, // default 421
103
+ * denyBody: 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(b.middleware.hostAllowlist({
111
+ * hosts: ["app.example.com", "*.example.com"],
112
+ * }));
113
+ */
84
114
  function create(opts) {
85
115
  validateOpts.requireObject(opts, "middleware.hostAllowlist", HostAllowlistError);
86
116
  validateOpts(opts, [
@@ -71,6 +71,44 @@ function _validateCidr(cidr) {
71
71
  catch (_e) { return false; }
72
72
  }
73
73
 
74
+ /**
75
+ * @primitive b.middleware.networkAllowlist
76
+ * @signature b.middleware.networkAllowlist(req, res, next)
77
+ * @since 0.1.0
78
+ * @related b.middleware.hostAllowlist, b.middleware.requireAuth
79
+ *
80
+ * In-process CIDR fence for path-scoped admin gates. Constructed
81
+ * via `b.middleware.networkAllowlist(opts)`; the resulting
82
+ * middleware has the `(req, res, next)` shape shown above. Path-based
83
+ * authorization prevents unauthorized USERS from reaching sensitive
84
+ * routes; this middleware adds a NETWORK-layer fence so a credential
85
+ * leak doesn't compromise the gate. Path-scoped — requests outside
86
+ * the configured prefixes pass through hot-path-cheap. Resolves
87
+ * client IP via `b.requestHelpers.clientIp` (with `trustProxy`),
88
+ * checks against the CIDR allowlist via `b.ssrfGuard.cidrContains`,
89
+ * and refuses misses with HTTP 404 by default (hides the gate from
90
+ * probes). Throws at create-time on malformed opts.
91
+ *
92
+ * @opts
93
+ * {
94
+ * paths: string[], // pathname prefixes, required
95
+ * allowedCidrs: string[], // required
96
+ * deniedCidrs: string[],
97
+ * trustProxy: boolean, // default false
98
+ * denyStatus: number, // default 404
99
+ * denyBody: string, // default "Not Found"
100
+ * audit: object,
101
+ * }
102
+ *
103
+ * @example
104
+ * var b = require("@blamejs/core");
105
+ * var app = b.router.create();
106
+ * app.use(b.middleware.networkAllowlist({
107
+ * paths: ["/admin"],
108
+ * allowedCidrs: ["10.0.0.0/8", "::1/128"],
109
+ * trustProxy: true,
110
+ * }));
111
+ */
74
112
  function create(opts) {
75
113
  opts = opts || {};
76
114
  validateOpts(opts, [