@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
@@ -114,6 +114,7 @@ var path = require("path");
114
114
  var nodeCrypto = require("node:crypto");
115
115
  var atomicFile = require("../atomic-file");
116
116
  var crypto = require("../crypto");
117
+ var lazyRequire = require("../lazy-require");
117
118
  var requestHelpers = require("../request-helpers");
118
119
  var safeBuffer = require("../safe-buffer");
119
120
  var safeJson = require("../safe-json");
@@ -121,6 +122,34 @@ var validateOpts = require("../validate-opts");
121
122
  var C = require("../constants");
122
123
  var { defineClass } = require("../framework-error");
123
124
 
125
+ var auditFwk = lazyRequire(function () { return require("../audit"); });
126
+
127
+ // Node's HTTP parser surfaces malformed chunked-transfer-encoding via a
128
+ // stable family of HPE_* codes. RFC 9112 §7.1 — when a server rejects a
129
+ // chunked decode the connection MUST close so a downstream proxy can't
130
+ // reuse the socket with the next request's body bytes still pending.
131
+ // HPE_INVALID_CHUNK_SIZE / HPE_CHUNK_EXTENSIONS_OVERFLOW (Node 24+) /
132
+ // HPE_INVALID_TRANSFER_ENCODING / HPE_INVALID_EOF_STATE (chunk truncated)
133
+ // all land here. The framework's Connection: close + audit emit closes
134
+ // the smuggling-adjacent socket-reuse path that bare 400-only handling
135
+ // leaves open.
136
+ var CHUNKED_MALFORMED_CODES = new Set([
137
+ "HPE_INVALID_CHUNK_SIZE",
138
+ "HPE_INVALID_TRANSFER_ENCODING",
139
+ "HPE_INVALID_EOF_STATE",
140
+ "HPE_INVALID_CONSTANT",
141
+ "HPE_CHUNK_EXTENSIONS_OVERFLOW",
142
+ "HPE_UNEXPECTED_CONTENT_LENGTH",
143
+ "ERR_HTTP_INVALID_CHUNK",
144
+ ]);
145
+ function _isChunkedMalformed(e) {
146
+ if (!e) return false;
147
+ if (typeof e.code === "string" && CHUNKED_MALFORMED_CODES.has(e.code)) return true;
148
+ if (typeof e.code === "string" && e.code.indexOf("HPE_") === 0 &&
149
+ typeof e.message === "string" && /chunk/i.test(e.message)) return true;
150
+ return false;
151
+ }
152
+
124
153
  var HTTP_STATUS = requestHelpers.HTTP_STATUS;
125
154
  var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
126
155
 
@@ -1098,6 +1127,46 @@ async function _parseMultipart(req, opts, ctParams) {
1098
1127
 
1099
1128
  // ---- main middleware factory ----
1100
1129
 
1130
+ /**
1131
+ * @primitive b.middleware.bodyParser
1132
+ * @signature b.middleware.bodyParser(req, res, next)
1133
+ * @since 0.1.0
1134
+ * @related b.middleware.bodyParser.raw, b.parsers.json, b.parsers.multipart
1135
+ *
1136
+ * Buffers and parses request bodies based on Content-Type.
1137
+ * Constructed via `b.middleware.bodyParser(opts)`; the resulting
1138
+ * middleware has the `(req, res, next)` shape shown above. Five
1139
+ * sub-parsers ship: JSON (via `safe-json` — POISONED_KEYS stripped,
1140
+ * depth + size caps), urlencoded, text, raw octet-stream, and
1141
+ * multipart/form-data. Multipart streams file parts to a tmp dir
1142
+ * with per-file + total-request size caps, filename sanitization,
1143
+ * SHA3-512 hashing during streaming, and tmp-file cleanup on
1144
+ * response end. Defends against RFC 9112 §6.1 request smuggling
1145
+ * before any body bytes are read. Each sub-parser can be disabled
1146
+ * by passing `false` in its slot.
1147
+ *
1148
+ * @opts
1149
+ * {
1150
+ * json: false | { limit, strict, charset, parseHook, contentTypes },
1151
+ * urlencoded: false | { limit, arrayLimit, contentTypes },
1152
+ * text: false | { limit, charset, contentTypes },
1153
+ * raw: false | { limit, contentTypes },
1154
+ * multipart: false | {
1155
+ * tmpDir, fileSize, totalSize, fileCount, fieldCount, fieldSize,
1156
+ * mimeAllowlist, fileFilter, fields, audit, contentTypes,
1157
+ * },
1158
+ * keepRawBody: boolean, // expose req.bodyRaw for webhook signing
1159
+ * }
1160
+ *
1161
+ * @example
1162
+ * var b = require("@blamejs/core");
1163
+ * var app = b.router.create();
1164
+ * app.use(b.middleware.bodyParser({
1165
+ * json: { limit: b.constants.BYTES.mib(1) },
1166
+ * urlencoded: { limit: b.constants.BYTES.mib(1) },
1167
+ * multipart: false,
1168
+ * }));
1169
+ */
1101
1170
  function create(opts) {
1102
1171
  opts = opts || {};
1103
1172
  validateOpts(opts, [
@@ -1191,6 +1260,52 @@ function create(opts) {
1191
1260
  "body-parser/unsupported-content-type"
1192
1261
  );
1193
1262
  } catch (e) {
1263
+ // RFC 9112 §7.1 — a server that rejects a chunked-decoded body
1264
+ // MUST close the connection so the upstream proxy cannot reuse
1265
+ // the socket with the next request's bytes still in flight. The
1266
+ // smuggling-shape pre-flight at top of the request already
1267
+ // catches the static TE/CL conflict cases; this catch handles
1268
+ // mid-stream parser failure (HPE_INVALID_CHUNK_SIZE etc. surfaced
1269
+ // by Node's HTTP parser as the body bytes arrive). Set
1270
+ // Connection: close + audit + 400.
1271
+ if (_isChunkedMalformed(e)) {
1272
+ // CVE-2026-33870 — chunked-encoding extension smuggling. When
1273
+ // Node's parser surfaces HPE_CHUNK_EXTENSIONS_OVERFLOW the
1274
+ // chunk-extension parameters exceeded llhttp's cap; the
1275
+ // framework emits a distinct audit action so operators can
1276
+ // alert on extension-smuggling specifically. RFC 9112 §7.1.1
1277
+ // chunk-ext is `; chunk-ext-name [= chunk-ext-val]` per chunk;
1278
+ // multi-`;` and `;param=value` shapes reach this code path
1279
+ // when the operator sets a tighter
1280
+ // `--max-http-header-size` / per-chunk extension cap.
1281
+ var chunkAction = (e && e.code === "HPE_CHUNK_EXTENSIONS_OVERFLOW")
1282
+ ? "http.chunked.extension.refused"
1283
+ : "http.chunked.malformed.refused";
1284
+ try {
1285
+ auditFwk().safeEmit({
1286
+ action: chunkAction,
1287
+ outcome: "denied",
1288
+ metadata: {
1289
+ code: e.code || null,
1290
+ message: (e && e.message) ? String(e.message).slice(0, 256) : "", // allow:raw-byte-literal — diagnostic-message clamp characters, not bytes
1291
+ },
1292
+ });
1293
+ } catch (_e) { /* audit best-effort */ }
1294
+ if (!res.headersSent) {
1295
+ var malformedBody = JSON.stringify({
1296
+ error: "malformed chunked transfer-encoding (RFC 9112 §7.1 — connection closed)",
1297
+ code: "http/chunked-malformed",
1298
+ });
1299
+ res.writeHead(HTTP_STATUS.BAD_REQUEST, {
1300
+ "Content-Type": "application/json; charset=utf-8",
1301
+ "Content-Length": Buffer.byteLength(malformedBody),
1302
+ "Connection": "close",
1303
+ });
1304
+ res.end(malformedBody);
1305
+ }
1306
+ try { req.destroy(); } catch (_e) { /* socket already closed */ }
1307
+ return;
1308
+ }
1194
1309
  var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
1195
1310
  var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
1196
1311
  var message = (e && e.message) ? e.message : String(e);
@@ -1238,6 +1353,36 @@ async function _parseJsonFromBuf(buf, opts) {
1238
1353
  // Accepts the same `raw`-section opts as create() (limit, contentTypes).
1239
1354
  // contentTypes default expands to `["*/*"]` so any Content-Type lands
1240
1355
  // as raw bytes.
1356
+
1357
+ /**
1358
+ * @primitive b.middleware.bodyParser.raw
1359
+ * @signature b.middleware.bodyParser.raw(opts)
1360
+ * @since 0.1.0
1361
+ * @related b.middleware.bodyParser
1362
+ *
1363
+ * Convenience factory that mounts only the raw-bytes sub-parser of
1364
+ * `bodyParser`. Sets `req.body` to a Buffer regardless of
1365
+ * `Content-Type`. Use on webhook-signature routes where the HMAC is
1366
+ * computed over the literal body bytes — JSON-parsing first would
1367
+ * change them. The `contentTypes` default expands to `["*\/*"]` so
1368
+ * any inbound type is captured.
1369
+ *
1370
+ * @opts
1371
+ * {
1372
+ * limit: number, // default ~10 MiB
1373
+ * contentTypes: string[], // default ["*\/*"]
1374
+ * }
1375
+ *
1376
+ * @example
1377
+ * var b = require("@blamejs/core");
1378
+ * var app = b.router.create();
1379
+ * app.post("/hooks/in", b.middleware.bodyParser.raw({
1380
+ * limit: b.constants.BYTES.mib(1),
1381
+ * }), function (req, res) {
1382
+ * // req.body is a Buffer of the raw request bytes
1383
+ * res.end(String(req.body.length));
1384
+ * });
1385
+ */
1241
1386
  function raw(opts) {
1242
1387
  opts = opts || {};
1243
1388
  return create({
@@ -1257,10 +1402,95 @@ function raw(opts) {
1257
1402
  // itself, so static helpers hang off it).
1258
1403
  create.raw = raw;
1259
1404
 
1405
+ // ---- Standalone async parsers ----
1406
+ //
1407
+ // `parseJsonStandalone(req, opts)` and `parseMultipartStandalone(req, opts)`
1408
+ // are the same parsing pipelines the middleware uses, exposed for handlers
1409
+ // that lazy-parse — code that decides parser shape from a route flag, or
1410
+ // bypasses the middleware for streaming endpoints. The middleware composes
1411
+ // these so there's no parallel pipeline to drift.
1412
+ //
1413
+ // Throws BodyParserError on caps / malformed shapes — operator handles in
1414
+ // a try/catch around `await b.parsers.json(req, ...)` /
1415
+ // `await b.parsers.multipart(req, ...)`. Validation tier is config-time
1416
+ // (throw at create on bad opts) + observable (throw on bad input — the
1417
+ // handler is awaiting the call, not a request lifecycle hook).
1418
+
1419
+ function _resolveStandaloneJsonOpts(opts) {
1420
+ opts = opts || {};
1421
+ var maxBytes = (opts.maxBytes !== undefined) ? opts.maxBytes : DEFAULTS.json.limit;
1422
+ validateOpts.optionalPositiveFinite(maxBytes, "parsers.json: opts.maxBytes",
1423
+ BodyParserError, "body-parser/bad-max-bytes");
1424
+ var strict = (opts.strict !== undefined) ? !!opts.strict : DEFAULTS.json.strict;
1425
+ var charset = (typeof opts.charset === "string") ? opts.charset : DEFAULTS.json.charset;
1426
+ return {
1427
+ limit: maxBytes,
1428
+ strict: strict,
1429
+ charset: charset,
1430
+ parseHook: (typeof opts.parseHook === "function") ? opts.parseHook : undefined,
1431
+ };
1432
+ }
1433
+
1434
+ function _resolveStandaloneMultipartOpts(opts, ct) {
1435
+ opts = opts || {};
1436
+ var resolved = Object.assign({}, DEFAULTS.multipart);
1437
+ validateOpts.optionalPositiveFinite(opts.maxBytes, "parsers.multipart: opts.maxBytes",
1438
+ BodyParserError, "body-parser/bad-max-bytes");
1439
+ if (opts.maxBytes !== undefined) {
1440
+ resolved.totalSize = opts.maxBytes;
1441
+ // Per-file cap clamps to maxBytes so a single field can't exceed the
1442
+ // request total — operator opts in to a smaller fileSize via opts.fileSize.
1443
+ if (resolved.fileSize > opts.maxBytes) resolved.fileSize = opts.maxBytes;
1444
+ }
1445
+ if (opts.maxFiles !== undefined) {
1446
+ var mf = opts.maxFiles;
1447
+ var mfBad = typeof mf !== "number" || !isFinite(mf) || mf <= 0 || Math.floor(mf) !== mf;
1448
+ if (mfBad) {
1449
+ throw new BodyParserError("body-parser/bad-max-files",
1450
+ "parsers.multipart: opts.maxFiles must be a positive integer",
1451
+ true, HTTP_STATUS.BAD_REQUEST);
1452
+ }
1453
+ resolved.fileCount = mf;
1454
+ }
1455
+ // Pass-through overrides for the multipart-specific knobs the middleware
1456
+ // accepts. parsers.multipart is a thin wrapper, not a feature subset.
1457
+ ["tmpDir", "fileSize", "fieldCount", "fieldSize", "mimeAllowlist",
1458
+ "fileFilter", "fields", "audit"].forEach(function (k) {
1459
+ if (opts[k] !== undefined) resolved[k] = opts[k];
1460
+ });
1461
+ // ct is the parsed Content-Type; required for the boundary parameter.
1462
+ if (!ct || typeof ct.type !== "string" || ct.type !== "multipart/form-data") {
1463
+ throw new BodyParserError("body-parser/standalone-not-multipart",
1464
+ "parsers.multipart: request Content-Type must be multipart/form-data, got " +
1465
+ JSON.stringify(ct ? ct.type : null),
1466
+ true, HTTP_STATUS.BAD_REQUEST);
1467
+ }
1468
+ return resolved;
1469
+ }
1470
+
1471
+ async function parseJsonStandalone(req, opts) {
1472
+ var resolved = _resolveStandaloneJsonOpts(opts);
1473
+ return _parseJson(req, resolved);
1474
+ }
1475
+
1476
+ async function parseMultipartStandalone(req, opts) {
1477
+ var ct = _contentType(req);
1478
+ var resolved = _resolveStandaloneMultipartOpts(opts, ct);
1479
+ // Returns { fields, files, filesRejected } — same shape the middleware
1480
+ // attaches to req. Handlers that already-accepted the upload wire
1481
+ // cleanup themselves (move file off tmp / unlink).
1482
+ return _parseMultipart(req, resolved, ct.params);
1483
+ }
1484
+
1260
1485
  module.exports = {
1261
1486
  create: create,
1262
1487
  raw: raw,
1263
1488
  BodyParserError: BodyParserError,
1489
+ // Standalone async helpers — surfaced via b.parsers.{json,multipart}.
1490
+ // The middleware composes these so the request-handling pipeline and
1491
+ // the operator-callable surface share one parsing path.
1492
+ parseJson: parseJsonStandalone,
1493
+ parseMultipart: parseMultipartStandalone,
1264
1494
  // Internal helpers exposed for tests + the csrf-protect refactor.
1265
1495
  _contentType: _contentType,
1266
1496
  _hasBody: _hasBody,
@@ -41,6 +41,40 @@ var DEFAULT_BANNER_HTML = '<div role="status" data-bot-disclosure="true" ' +
41
41
  'For California users: this disclosure is provided per Cal. Bus. &amp; Prof. Code §17941.' +
42
42
  '</div>';
43
43
 
44
+ /**
45
+ * @primitive b.middleware.botDisclose
46
+ * @signature b.middleware.botDisclose(opts)
47
+ * @since 0.1.0
48
+ * @related b.middleware.aiActDisclosure, b.middleware.botGuard
49
+ *
50
+ * California SB 1001 bot-disclosure (Cal. Bus. & Prof. Code §17941):
51
+ * automated conversation surfaces (LLM chat / IVR / SMS) used to
52
+ * incentivize sales or influence elections must disclose their
53
+ * non-human nature. Injects a disclosure banner into HTML responses,
54
+ * sets `X-Bot-Disclosure` for API consumers, and emits an audit
55
+ * event for every conversation-initiating request. Operator-supplied
56
+ * `bannerHtml` / `bannerJson` carry custom branding while the
57
+ * default copy meets the §17941(a) "clear, conspicuous, reasonably
58
+ * designed" bar.
59
+ *
60
+ * @opts
61
+ * {
62
+ * mountPaths: string[], // null = apply to all routes
63
+ * bannerHtml: string,
64
+ * bannerJson: object,
65
+ * headerName: string, // default "X-Bot-Disclosure"
66
+ * auditAction: string, // default "middleware.bot_disclose"
67
+ * audit: boolean, // default true
68
+ * }
69
+ *
70
+ * @example
71
+ * var b = require("@blamejs/core");
72
+ * var app = b.router.create();
73
+ * app.use(b.middleware.botDisclose({
74
+ * mountPaths: ["/chat", "/api/chat"],
75
+ * bannerJson: { _bot: true, disclosure: "automated-assistant" },
76
+ * }));
77
+ */
44
78
  function create(opts) {
45
79
  opts = opts || {};
46
80
  validateOpts(opts, [
@@ -77,6 +77,45 @@ function _xffIpFor(trustProxy) {
77
77
  };
78
78
  }
79
79
 
80
+ /**
81
+ * @primitive b.middleware.botGuard
82
+ * @signature b.middleware.botGuard(req, res, next)
83
+ * @since 0.1.0
84
+ * @related b.middleware.fetchMetadata, b.middleware.botDisclose
85
+ *
86
+ * Cheap fingerprint-based detection of obviously-non-browser requests.
87
+ * Constructed via `b.middleware.botGuard(opts)`; the resulting
88
+ * middleware has the `(req, res, next)` shape shown above.
89
+ * Combines three heuristics: missing `Accept-Language`, missing
90
+ * `Sec-Fetch-Mode` (HTML routes), and User-Agent regex match against
91
+ * a default list (curl / wget / python-requests / axios / etc.). Not
92
+ * a substitute for proper authentication — catches drive-by scrapers
93
+ * and low-effort bots. In `mode: "block"` (default) the request is
94
+ * refused; in `mode: "tag"` `req.suspectedBot = true` is set and the
95
+ * request continues so the application can rate-limit suspected bots
96
+ * separately. Every decision is audited.
97
+ *
98
+ * @opts
99
+ * {
100
+ * mode: "block"|"tag", // default "block"
101
+ * onlyForHtml: boolean, // default true
102
+ * allowedAgents: RegExp[], // override matches
103
+ * blockedAgents: RegExp[], // append to defaults
104
+ * skipPaths: string[],
105
+ * statusOnBlock: number, // default 403
106
+ * bodyOnBlock: string,
107
+ * trustProxy: boolean|number,
108
+ * }
109
+ *
110
+ * @example
111
+ * var b = require("@blamejs/core");
112
+ * var app = b.router.create();
113
+ * app.use(b.middleware.botGuard({
114
+ * mode: "tag",
115
+ * skipPaths: ["/healthz"],
116
+ * onlyForHtml: true,
117
+ * }));
118
+ */
80
119
  function create(opts) {
81
120
  opts = opts || {};
82
121
  validateOpts(opts, [
@@ -225,6 +225,43 @@ function _appendVary(existing, token) {
225
225
  return parts.join(", ");
226
226
  }
227
227
 
228
+ /**
229
+ * @primitive b.middleware.compression
230
+ * @signature b.middleware.compression(req, res, next)
231
+ * @since 0.1.0
232
+ * @related b.middleware.sse
233
+ *
234
+ * Brotli + gzip response compression. Constructed via
235
+ * `b.middleware.compression(opts)`; the resulting middleware has
236
+ * the `(req, res, next)` shape shown above. Intercepts the response stream
237
+ * and pipes it through `node:zlib`'s transform when the client
238
+ * supports it. Brotli is preferred (better ratio for text), gzip is
239
+ * the fallback. Skips small responses (below `threshold`),
240
+ * already-encoded responses, 204/304 status codes, server-sent
241
+ * events streams (chunked compression breaks SSE framing), and
242
+ * Content-Types outside the allowlist (image/* / video/* / archives
243
+ * are already entropy-dense). Operators with custom skip logic wire
244
+ * a `filter(req, res)` predicate.
245
+ *
246
+ * @opts
247
+ * {
248
+ * threshold: number, // default 1024 bytes
249
+ * encodings: string[], // default ["br", "gzip"]
250
+ * contentTypes: string[], // allowlist of MIME types
251
+ * gzipLevel: number, // 1..9, default 6
252
+ * brotliQuality: number, // 0..11, default 4
253
+ * filter: function(req, res): boolean,
254
+ * }
255
+ *
256
+ * @example
257
+ * var b = require("@blamejs/core");
258
+ * var app = b.router.create();
259
+ * app.use(b.middleware.compression({
260
+ * threshold: 1024,
261
+ * encodings: ["br", "gzip"],
262
+ * contentTypes: ["text/*", "application/json"],
263
+ * }));
264
+ */
228
265
  function create(opts) {
229
266
  opts = opts || {};
230
267
  validateOpts(opts, [
@@ -47,6 +47,38 @@ function _emitAudit(audit, action, outcome, metadata) {
47
47
  } catch (_e) { /* drop-silent — observability sink */ }
48
48
  }
49
49
 
50
+ /**
51
+ * @primitive b.middleware.cookies
52
+ * @signature b.middleware.cookies(opts)
53
+ * @since 0.1.0
54
+ * @related b.cookies.parseSafe, b.middleware.csrfProtect
55
+ *
56
+ * Inbound `Cookie` header threat detection. Runs every request through
57
+ * `b.cookies.parseSafe` and surfaces header-cap / control-byte /
58
+ * malformed-pair / empty-name / name-cap / value-cap / duplicate-name
59
+ * (cookie-tossing) issues. Sets `req.cookieJar` to the parsed jar.
60
+ * In `mode: "enforce"` (default) high-severity issues refuse the
61
+ * request with HTTP 400 + JSON body; `audit-only` and `log-only`
62
+ * modes pass through but still emit audits.
63
+ *
64
+ * @opts
65
+ * {
66
+ * mode: "enforce"|"audit-only"|"log-only", // default "enforce"
67
+ * refuseOnHigh: boolean, // default true (only meaningful in enforce)
68
+ * maxHeaderBytes: number,
69
+ * maxNameBytes: number,
70
+ * maxValueBytes: number,
71
+ * audit: object,
72
+ * }
73
+ *
74
+ * @example
75
+ * var b = require("@blamejs/core");
76
+ * var app = b.router.create();
77
+ * app.use(b.middleware.cookies({
78
+ * mode: "enforce",
79
+ * maxHeaderBytes: b.constants.BYTES.kib(8),
80
+ * }));
81
+ */
50
82
  function create(opts) {
51
83
  opts = opts || {};
52
84
  var mode = opts.mode || "enforce";
@@ -156,6 +156,46 @@ function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy, str
156
156
  return reqOrigin !== null && reqOrigin === canonOrigin;
157
157
  }
158
158
 
159
+ /**
160
+ * @primitive b.middleware.cors
161
+ * @signature b.middleware.cors(req, res, next)
162
+ * @since 0.1.0
163
+ * @related b.middleware.csrfProtect, b.middleware.fetchMetadata
164
+ *
165
+ * Cross-Origin Resource Sharing handler. Constructed via
166
+ * `b.middleware.cors(opts)`; the resulting middleware has the
167
+ * `(req, res, next)` shape shown above. Allowlist matches strings
168
+ * (canonicalized) and RegExp entries. Handles preflights,
169
+ * `Access-Control-Allow-*` response headers, and
170
+ * `strictNullOrigin: true` (default) refuses `Origin: null` even
171
+ * with `Sec-Fetch-Site: same-origin` since non-browser callers can
172
+ * forge that header. `siteOrigin` declares the framework's own
173
+ * origin(s) for same-origin shortcuts. Throws at create() on
174
+ * unparseable origin entries — operators catch typos at boot.
175
+ *
176
+ * @opts
177
+ * {
178
+ * origins: Array<string|RegExp>,
179
+ * siteOrigin: string|string[],
180
+ * methods: string[], // default GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
181
+ * headers: string[], // default Content-Type,Authorization,X-Request-Id
182
+ * exposeHeaders: string[], // default X-Request-Id
183
+ * credentials: boolean,
184
+ * maxAgeSeconds: number, // default 600
185
+ * refuseUnknown: boolean, // default true
186
+ * strictNullOrigin: boolean, // default true
187
+ * trustProxy: boolean|number,
188
+ * }
189
+ *
190
+ * @example
191
+ * var b = require("@blamejs/core");
192
+ * var app = b.router.create();
193
+ * app.use(b.middleware.cors({
194
+ * origins: ["https://app.example.com", /\.example\.com$/],
195
+ * siteOrigin: "https://example.com",
196
+ * methods: ["GET", "POST"],
197
+ * }));
198
+ */
159
199
  function create(opts) {
160
200
  opts = opts || {};
161
201
 
@@ -224,6 +224,46 @@ function _injectNonce(cspHeader, nonce, directives, strictDynamic) {
224
224
  return _serializeCsp(parts);
225
225
  }
226
226
 
227
+ /**
228
+ * @primitive b.middleware.cspNonce
229
+ * @signature b.middleware.cspNonce(req, res, next)
230
+ * @since 0.1.0
231
+ * @related b.middleware.securityHeaders, b.middleware.cspReport
232
+ *
233
+ * Per-request CSP nonce + render integration. Constructed via
234
+ * `b.middleware.cspNonce(opts)`; the resulting middleware has the
235
+ * `(req, res, next)` shape shown above. Generates a fresh
236
+ * random nonce (16 bytes / 22 chars base64 by default), attaches it
237
+ * to `req.cspNonce` and `res.locals.cspNonce` (auto-merged into
238
+ * template data), and patches the existing Content-Security-Policy
239
+ * header to append `'nonce-XYZ'` to the configured directives
240
+ * (default: script-src + style-src). With `strictDynamic: true`,
241
+ * appends `'strict-dynamic'` so nonced scripts can load dependencies
242
+ * without origin allowlisting (recommended for SPA hydration). Mount
243
+ * after `securityHeaders`. Below-16-byte nonces are refused at
244
+ * config time.
245
+ *
246
+ * @opts
247
+ * {
248
+ * directives: string[], // default ["script-src", "style-src"]
249
+ * nonceBytes: number, // default 16; minimum 16
250
+ * strictDynamic: boolean,
251
+ * headerName: string, // default "Content-Security-Policy"
252
+ * property: string, // default "cspNonce"
253
+ * always: boolean,
254
+ * placeholder: string,
255
+ * }
256
+ *
257
+ * @example
258
+ * var b = require("@blamejs/core");
259
+ * var app = b.router.create();
260
+ * app.use(b.middleware.securityHeaders());
261
+ * app.use(b.middleware.cspNonce({
262
+ * directives: ["script-src", "style-src"],
263
+ * nonceBytes: 16,
264
+ * strictDynamic: true,
265
+ * }));
266
+ */
227
267
  function create(opts) {
228
268
  opts = opts || {};
229
269
  validateOpts(opts, [
@@ -82,6 +82,40 @@ function _normalizeOne(reportLike) {
82
82
  return null;
83
83
  }
84
84
 
85
+ /**
86
+ * @primitive b.middleware.cspReport
87
+ * @signature b.middleware.cspReport(req, res, next)
88
+ * @since 0.1.0
89
+ * @related b.middleware.cspNonce, b.middleware.securityHeaders
90
+ *
91
+ * Reporting-API endpoint for CSP / COEP / COOP / Permissions-Policy
92
+ * violations. Constructed via `b.middleware.cspReport(opts)`; the
93
+ * resulting middleware has the `(req, res, next)` shape shown above. Accepts `application/reports+json` (modern) and the
94
+ * legacy `application/csp-report` body shapes. Refuses non-POST
95
+ * (HTTP 405), oversized bodies (HTTP 413, default 64 KiB cap), and
96
+ * non-JSON (HTTP 400). Each report is normalized to a uniform shape
97
+ * (`type`, `url`, `body.{documentURL, blockedURL, effectiveDirective,
98
+ * sample, sourceFile, lineNumber}`), audited with action
99
+ * `csp.violation`, and forwarded to the operator's `onReport`
100
+ * callback for metrics or alerting.
101
+ *
102
+ * @opts
103
+ * {
104
+ * onReport: function(report): void,
105
+ * maxBytes: number, // default 64 KiB
106
+ * audit: boolean, // default true
107
+ * }
108
+ *
109
+ * @example
110
+ * var b = require("@blamejs/core");
111
+ * var app = b.router.create();
112
+ * app.post("/csp-report", b.middleware.cspReport({
113
+ * maxBytes: b.constants.BYTES.kib(64),
114
+ * onReport: function (report) {
115
+ * console.log("csp violation", report.body.effectiveDirective);
116
+ * },
117
+ * }));
118
+ */
85
119
  function create(opts) {
86
120
  opts = opts || {};
87
121
  validateOpts(opts, ["audit", "onReport", "maxBytes"], "middleware.cspReport");
@@ -226,6 +226,49 @@ function _writeReject(res, message) {
226
226
  }
227
227
  }
228
228
 
229
+ /**
230
+ * @primitive b.middleware.csrfProtect
231
+ * @signature b.middleware.csrfProtect(req, res, next)
232
+ * @since 0.1.0
233
+ * @related b.middleware.cors, b.middleware.fetchMetadata
234
+ *
235
+ * Issues CSRF tokens to safe-method requests and rejects state-
236
+ * changing requests whose submitted token doesn't match. Constructed
237
+ * via `b.middleware.csrfProtect(opts)`; the resulting middleware
238
+ * has the `(req, res, next)` shape shown above. Two
239
+ * storage modes (mutually exclusive, exactly one required):
240
+ * (a) cookie-stored double-submit (default — `__Host-csrf` over
241
+ * HTTPS, SameSite=Lax) where the framework issues + reads the
242
+ * cookie; (b) operator-supplied `tokenLookup(req)` for session-
243
+ * stored tokens. Submitted-token sources: header (default
244
+ * `X-CSRF-Token`) then body field (default `_csrf`). Refuses with
245
+ * HTTP 403 + audits `auth.csrf.denied` on mismatch. Mount AFTER
246
+ * `attachUser` (session lookup) and `bodyParser` (form-field read).
247
+ *
248
+ * @opts
249
+ * {
250
+ * cookie: boolean | { name, sameSite, secure, path, httpOnly },
251
+ * tokenLookup: function(req): string|null,
252
+ * fieldName: string, // default "_csrf"
253
+ * headerName: string, // default "X-CSRF-Token"
254
+ * methods: string[], // default POST/PUT/DELETE/PATCH
255
+ * checkOrigin: boolean,
256
+ * allowedOrigins: string[],
257
+ * requireOrigin: boolean,
258
+ * requireJsonContentType: boolean,
259
+ * trustProxy: boolean|number,
260
+ * audit: boolean,
261
+ * }
262
+ *
263
+ * @example
264
+ * var b = require("@blamejs/core");
265
+ * var app = b.router.create();
266
+ * app.use(b.middleware.csrfProtect({
267
+ * cookie: true,
268
+ * checkOrigin: true,
269
+ * requireOrigin: true,
270
+ * }));
271
+ */
229
272
  function create(opts) {
230
273
  opts = opts || {};
231
274