@blamejs/core 0.8.43 → 0.8.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/log.js CHANGED
@@ -1,62 +1,57 @@
1
1
  "use strict";
2
2
  /**
3
- * log — structured JSON application logger with request-id correlation.
3
+ * @module b.log
4
+ * @featured true
5
+ * @nav Observability
6
+ * @title Log
4
7
  *
5
- * Distinct concern from lib/logger.js: logger.js is the framework's
6
- * own boot/operational chatter to console with `[blamejs:<name>] `
7
- * prefix (humans watching `npm start`). lib/log.js is the app-level
8
- * structured logger meant to be ingested by a log aggregator.
8
+ * @intro
9
+ * Structured JSON application logger meant to be ingested by a log
10
+ * aggregator. Each emitted line is a single JSON object terminated
11
+ * with `\n`; the log level is encoded as the string field `level`,
12
+ * not as console color. Distinct from `b.log.boot` — that path is
13
+ * framework-internal startup chatter to the TTY (humans watching
14
+ * `npm start`); `create()` is what apps wire into their request
15
+ * lifecycle.
9
16
  *
10
- * Each line is one JSON object on a single line, terminated with `\n`.
11
- * Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
12
- * Default routing: debug / info / warn → stdout; error / fatal → stderr.
13
- * Multi-sink config (`sinks: [...]`) takes full control of routing.
17
+ * Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
18
+ * Default routing: debug / info / warn stdout; error / fatal
19
+ * stderr. Multi-sink config (`sinks: [...]`) takes full control of
20
+ * routing — each sink gets every line at-or-above its own per-sink
21
+ * level, useful when the operator wants debug to a file but warn+
22
+ * to stderr.
14
23
  *
15
- * var log = b.log.create({
16
- * level: "info", // env LOG_LEVEL > opts.level > "info"
17
- * base: { service: "myapp", version: "1.2.3" },
18
- * redact: true, // run extras through lib/redact
19
- * });
20
- *
21
- * // Multi-sink: each sink gets every line at-or-above its own level.
22
- * // Default (no `sinks` opt) splits info-and-below to stdout and
23
- * // warn-and-up to stderr — same as before.
24
- * var log = b.log.create({
25
- * level: "debug",
26
- * sinks: [
27
- * { stream: process.stdout, level: "info" },
28
- * { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
29
- * { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
30
- * ],
31
- * });
32
- * // sinks: [...] is mutually exclusive with destination/errorDestination.
24
+ * Redact-aware: `extras` passed to `.info(msg, extras)` flow through
25
+ * `b.redact` by default, so password / token / cardNumber-shaped
26
+ * keys never reach the log line. Operators opt out with
27
+ * `redact: false` only when the logger sits behind a downstream
28
+ * redactor.
33
29
  *
34
- * log.info("user logged in", { userId: "u-1" });
35
- * log.error("payment failed", { orderId, err: e.message });
30
+ * Request correlation rides on Node's AsyncLocalStorage. The
31
+ * middleware allocates a `requestId` (or honors an inbound
32
+ * `X-Request-Id` header) and binds it for the entire async chain;
33
+ * every `log.info` inside the request automatically picks up the
34
+ * id without the caller threading it explicitly. OpenTelemetry
35
+ * trace correlation rides the same channel — `runWithContext`
36
+ * merges arbitrary fields (tenantId, traceId) into the bound store.
36
37
  *
37
- * // Child with bound context
38
- * var authLog = log.bind({ component: "auth" });
39
- * authLog.info("password verified", { userId: "u-1" });
38
+ * Child loggers via `log.bind({ component: "auth" })` carry the
39
+ * bound fields into every emitted line; chains compose, so an
40
+ * auth-handler logger can bind its own `userId` on top.
40
41
  *
41
- * // Request correlation via AsyncLocalStorage (Node async context)
42
- * await log.runWithRequestId("req-abc", async function () {
43
- * log.info("inside request"); // ..., "requestId": "req-abc"
44
- * });
42
+ * Field merge order (last wins): base context → bound chain → ALS
43
+ * store caller's extras → core fields (timestamp / level /
44
+ * message). Extras that try to clobber a core field are dropped
45
+ * and the line carries `_overwriteAttempt: true` so misconfig is
46
+ * visible.
45
47
  *
46
- * // Router middleware that allocates a requestId and binds it for
47
- * // the entire request async chain
48
- * r.use(log.middleware());
48
+ * Trojan-Source defense (CVE-2021-42574) is baked in: Unicode
49
+ * bidi / format controls in messages are escaped to `\uXXXX`
50
+ * literals before they reach the wire so a hostile message can't
51
+ * re-order the visible line in a TTY / syslog reader.
49
52
  *
50
- * Field merge order (last wins):
51
- * 1. base context from create()
52
- * 2. bound context from bind() (each ancestor up the chain)
53
- * 3. requestId from ALS (if set)
54
- * 4. extra arg from .info(msg, extra)
55
- * 5. core fields: timestamp, level, message
56
- *
57
- * Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
58
- * keeps level: "info" in the emitted line, with an _overwriteAttempt
59
- * flag if the operator tried to clobber.
53
+ * @card
54
+ * Structured JSON application logger meant to be ingested by a log aggregator.
60
55
  */
61
56
 
62
57
  var { AsyncLocalStorage } = require("node:async_hooks");
@@ -197,6 +192,46 @@ function _resolveSinks(opts) {
197
192
  ];
198
193
  }
199
194
 
195
+ /**
196
+ * @primitive b.log.create
197
+ * @signature b.log.create(opts)
198
+ * @since 0.1.70
199
+ * @status stable
200
+ * @related b.log.boot, b.log.makeViaOrFallback, b.redact.redact
201
+ *
202
+ * Build a structured JSON logger instance. Returns an object with
203
+ * `.debug` / `.info` / `.warn` / `.error` / `.fatal` emitters, plus
204
+ * `.bind(extra)` for child loggers, `.middleware()` for router-side
205
+ * request-id binding, `.runWithRequestId(id, fn)` /
206
+ * `.runWithContext(ctx, fn)` for ad-hoc AsyncLocalStorage scopes,
207
+ * and `.setLevel` / `.getLevel` / `.isLevelEnabled` for runtime
208
+ * level control. Level resolution is `LOG_LEVEL` env > `opts.level`
209
+ * > `"info"`.
210
+ *
211
+ * @opts
212
+ * level: "info", // string or 0-4
213
+ * base: { service: "myapp" }, // merged into every line
214
+ * redact: true, // run extras through b.redact
215
+ * sinks: [
216
+ * { stream: process.stdout, level: "info" },
217
+ * { stream: fs.createWriteStream("./errors.log"), level: "error" },
218
+ * ],
219
+ * destination: process.stdout, // legacy single-sink
220
+ * errorDestination: process.stderr, // legacy two-sink split
221
+ * format: "json",
222
+ * clock: function () { return new Date(); }, // test seam
223
+ *
224
+ * @example
225
+ * var log = b.log.create({
226
+ * level: "info",
227
+ * base: { service: "myapp", version: "1.2.3" },
228
+ * });
229
+ * log.info("user logged in", { userId: "u-1" });
230
+ * var authLog = log.bind({ component: "auth" });
231
+ * authLog.warn("rate-limited", { ip: "203.0.113.7" });
232
+ * // → {"timestamp":"...","level":"info","message":"user logged in",
233
+ * // "service":"myapp","version":"1.2.3","userId":"u-1"}
234
+ */
200
235
  function create(opts) {
201
236
  opts = opts || {};
202
237
  validateOpts(opts, [
@@ -386,23 +421,30 @@ function create(opts) {
386
421
  return _makeInstance([]);
387
422
  }
388
423
 
389
- // ---- Boot logger ----
390
- //
391
- // Framework-internal modules emit human-readable startup chatter
392
- // during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
393
- // distinct from the structured app-level logger above. The boot
394
- // channel is TTY-aware:
395
- //
396
- // - stdout is a TTY → "[blamejs:<name>] <message>" line
397
- // - stdout is piped → JSON line { timestamp, level, message,
398
- // component: <name>, boot: true }
399
- //
400
- // This keeps `npm start` readable for humans while letting log
401
- // aggregators ingest boot chatter as structured records.
402
- //
403
- // Returned object is a callable (info path) plus .info / .warn /
404
- // .error / .prefix members so calls like `log("ready")` and
405
- // `log.warn("…")` both work.
424
+ /**
425
+ * @primitive b.log.boot
426
+ * @signature b.log.boot(name)
427
+ * @since 0.7.0
428
+ * @status stable
429
+ * @related b.log.create, b.log.makeViaOrFallback
430
+ *
431
+ * Framework-internal boot logger for human-readable startup chatter
432
+ * (`[blamejs:db] ready`, `[blamejs:vault] WARNING: ...`). TTY-aware:
433
+ * when stdout is a terminal it emits a prefixed line; when stdout is
434
+ * piped it emits a one-line JSON object so log aggregators can ingest
435
+ * boot chatter as structured records. The returned value is a
436
+ * callable (info path) plus `.debug` / `.info` / `.warn` / `.error` /
437
+ * `.prefix` members so `log("ready")` and `log.warn("...")` both
438
+ * work.
439
+ *
440
+ * @example
441
+ * var log = b.log.boot("db");
442
+ * log("ready");
443
+ * log.warn("connection slow");
444
+ * // → "[blamejs:db] ready" (TTY)
445
+ * // → {"timestamp":"...","level":"info","message":"ready",
446
+ * // "component":"db","boot":true} (piped)
447
+ */
406
448
  function boot(name) {
407
449
  if (typeof name !== "string" || name.length === 0) {
408
450
  throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
@@ -456,20 +498,27 @@ function boot(name) {
456
498
  return info;
457
499
  }
458
500
 
459
- // makeViaOrFallback — closure factory for operator-log routing. Used by
460
- // bundler / dev / error-page / pqc-gate (and similar primitives) that
461
- // accept opts.log but must keep emitting through a per-module fallback
462
- // when the operator didn't pass one. Replaces the per-file
463
- // `function _logVia(log, level, message, fields) { if (log && typeof
464
- // log[level] === "function") { try { log[level](message, fields); }
465
- // catch ... } return; } ... fallback;` boilerplate.
466
- //
467
- // var _logVia = log.makeViaOrFallback(opts.log, log.boot("bundler"));
468
- // _logVia("error", "build-failed", { reason: "..." });
469
- //
470
- // The operator log call is best-effort: a misbehaving log[level] swallows
471
- // internally rather than crash the caller. Fallback is invoked only when
472
- // the operator log is absent or doesn't expose the requested level.
501
+ /**
502
+ * @primitive b.log.makeViaOrFallback
503
+ * @signature b.log.makeViaOrFallback(operatorLog, fallbackLog)
504
+ * @since 0.7.30
505
+ * @status stable
506
+ * @related b.log.create, b.log.boot
507
+ *
508
+ * Closure factory for operator-log routing. Used by primitives
509
+ * (bundler, dev server, error-page renderer, pqc-gate, ...) that
510
+ * accept `opts.log` but must keep emitting through a per-module
511
+ * fallback when the operator didn't pass one. The operator log call
512
+ * is best-effort a misbehaving `log[level]` is swallowed rather
513
+ * than crashing the caller. Fallback fires only when the operator
514
+ * log is absent or doesn't expose the requested level.
515
+ *
516
+ * @example
517
+ * var fallback = b.log.boot("bundler");
518
+ * var via = b.log.makeViaOrFallback(null, fallback);
519
+ * via("error", "build-failed", { reason: "missing entrypoint" });
520
+ * // → "[blamejs:bundler] build-failed {\"reason\":\"missing entrypoint\"}"
521
+ */
473
522
  function makeViaOrFallback(operatorLog, fallbackLog) {
474
523
  return function (level, message, fields) {
475
524
  if (operatorLog && typeof operatorLog[level] === "function") {
@@ -514,14 +563,63 @@ function _bootMinLevel() {
514
563
  return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
515
564
  }
516
565
 
566
+ /**
567
+ * @primitive b.log.getRequestId
568
+ * @signature b.log.getRequestId()
569
+ * @since 0.1.70
570
+ * @status stable
571
+ * @related b.log.runWithRequestId, b.log.create
572
+ *
573
+ * Read the current AsyncLocalStorage-bound request id, or `null`
574
+ * when called outside a `runWithRequestId` / middleware-wrapped
575
+ * scope. The module-level helper exists for code paths that don't
576
+ * have a logger instance handy but still need to read the
577
+ * request-correlation token (e.g. an external SDK callback that
578
+ * must include the id in a remote span).
579
+ *
580
+ * @example
581
+ * await b.log.runWithRequestId("req-abc", async function () {
582
+ * var id = b.log.getRequestId();
583
+ * // → "req-abc"
584
+ * });
585
+ */
586
+ function getRequestId() {
587
+ var s = _getStore();
588
+ return s ? s.requestId : null;
589
+ }
590
+
591
+ /**
592
+ * @primitive b.log.runWithRequestId
593
+ * @signature b.log.runWithRequestId(id, fn)
594
+ * @since 0.1.70
595
+ * @status stable
596
+ * @related b.log.getRequestId, b.log.create
597
+ *
598
+ * Run `fn` inside an AsyncLocalStorage scope where
599
+ * `b.log.getRequestId()` returns `id`. Every `b.log.create`-built
600
+ * logger inside the scope automatically picks up the id on each
601
+ * emitted line. Returns whatever `fn` returns (including a Promise);
602
+ * the binding propagates through `await` boundaries via Node's
603
+ * async-context plumbing.
604
+ *
605
+ * @example
606
+ * var result = await b.log.runWithRequestId("req-abc", async function () {
607
+ * return b.log.getRequestId();
608
+ * });
609
+ * // → "req-abc"
610
+ */
611
+ function runWithRequestId(id, fn) {
612
+ return _als.run({ requestId: id || null, _extra: {} }, fn);
613
+ }
614
+
517
615
  module.exports = {
518
- create: create,
519
- boot: boot,
616
+ create: create,
617
+ boot: boot,
520
618
  makeViaOrFallback: makeViaOrFallback,
521
- LEVELS: LEVELS,
522
- LogError: LogError,
619
+ LEVELS: LEVELS,
620
+ LogError: LogError,
523
621
  // Module-level helpers for code paths that don't have a logger
524
622
  // instance handy but still need to read ALS state.
525
- getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
526
- runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
623
+ getRequestId: getRequestId,
624
+ runWithRequestId: runWithRequestId,
527
625
  };
@@ -1,66 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * mail-bounce — vendor-shaped intake for outbound-mail bounces /
4
- * complaints / delivery callbacks.
3
+ * @module b.mailBounce
4
+ * @nav Communication
5
+ * @title Mail Bounce
5
6
  *
6
- * Outbound mail can come back as:
7
- * - a hard or soft bounce (delivery failure)
8
- * - a spam / abuse complaint
9
- * - a delivered confirmation
10
- * - a list-unsubscribe complaint
7
+ * @intro
8
+ * Inbound mail bounce-handler parse the vendor's webhook DSN /
9
+ * complaint / delivery payload, normalize it into one event shape,
10
+ * classify hard vs soft bounces, and feed an operator-supplied
11
+ * suppression-list hook.
11
12
  *
12
- * Vendors (Postmark, SES via SNS, Resend) ship these as HTTP webhooks
13
- * with payload shapes that all carry the same information dressed up
14
- * differently. This module owns the translation: each vendor parser
15
- * normalizes its payload into one shape so operators don't write
16
- * vendor-specific reconciliation code per environment.
13
+ * Outbound mail comes back as a hard bounce (permanent invalid
14
+ * address), a soft bounce (transient mailbox full, greylisted), a
15
+ * spam / abuse complaint, a delivery confirmation, or a list-
16
+ * unsubscribe. Vendors (Postmark, AWS SES via SNS, Resend) ship the
17
+ * same information dressed up in three different JSON shapes. This
18
+ * module owns the translation so operators write a single
19
+ * reconciliation path regardless of vendor.
17
20
  *
18
- * Public API:
21
+ * `b.mailBounce.parse` is the pure synchronous parser; it returns the
22
+ * normalized event `{ vendor, type, subType, recipient, messageId,
23
+ * reason, timestamp, raw }`. `b.mailBounce.handler` wires that
24
+ * parser into an Express-style middleware that buffers the body,
25
+ * runs an operator `verify` hook (HMAC, Basic Auth, SNS-Signature),
26
+ * emits a `system.mail.bounce` audit row, and calls the operator's
27
+ * `onBounce(event)` so the suppression list can be updated before
28
+ * the 200 response goes back.
19
29
  *
20
- * mailBounce.parse(payload, { vendor })
21
- * Pure synchronous parser. Returns the normalized event:
22
- * {
23
- * vendor: "postmark" | "ses" | "resend",
24
- * type: "bounce" | "complaint" | "delivery",
25
- * subType: "hard" | "soft" | "abuse" | ... | null,
26
- * recipient: "user@example.com",
27
- * messageId: "<framework-emitted Message-ID>" | null,
28
- * reason: "smtp 550 ..." | null,
29
- * timestamp: "2026-04-28T..." (ISO 8601),
30
- * raw: <input payload, untouched>,
31
- * }
30
+ * Generic RFC 3464 DSN is intentionally NOT a built-in vendor
31
+ * parsing arbitrary MTA reports needs a full email parser the
32
+ * framework does not vendor for this surface. Operators with raw
33
+ * DSN inflow supply `{ parser }` to plug a custom normalizer.
32
34
  *
33
- * mailBounce.handler({ vendor, verify?, onBounce?, audit?, ... })
34
- * Returns an Express-style middleware (req, res). Buffers + parses
35
- * the request body, calls verify(req, body) if supplied, runs the
36
- * parser, emits system.mail.bounce, calls onBounce(event), and
37
- * responds 200. Validation errors land as 400 with a JSON error.
38
- * Verification failures land as 401.
39
- *
40
- * Operator example (Postmark):
41
- *
42
- * var bounce = b.mailBounce.handler({
43
- * vendor: "postmark",
44
- * verify: function (req) {
45
- * // Postmark recommends Basic Auth on the webhook URL.
46
- * return req.headers.authorization === expectedBasic;
47
- * },
48
- * onBounce: function (event) {
49
- * // Mark the recipient as bounced in your data store.
50
- * return repo.users.markBounced(event.recipient, event.subType);
51
- * },
52
- * });
53
- * r.post("/webhooks/postmark", bounce);
54
- *
55
- * Audit: every parsed bounce/complaint/delivery emits one
56
- * `system.mail.bounce` row carrying the normalized fields. Audit is on
57
- * by default and disabled per-instance via { audit: false }.
58
- *
59
- * Generic DSN (RFC 3464 multipart/report) is intentionally NOT included
60
- * as a built-in vendor: parsing arbitrary email-back-from-MTA reports
61
- * needs a full email parser, which the framework does not vendor for
62
- * this surface. Operators with raw DSN inflow plug a custom parser via
63
- * the { vendor: 'custom', parser } shape (see _customParser below).
35
+ * @card
36
+ * Inbound mail bounce-handler parse the vendor's webhook DSN / complaint / delivery payload, normalize it into one event shape, classify hard vs soft bounces, and feed an operator-supplied suppression-list hook.
64
37
  */
65
38
 
66
39
  var lazyRequire = require("./lazy-require");
@@ -368,6 +341,43 @@ var VENDORS = {
368
341
  resend: _parseResend,
369
342
  };
370
343
 
344
+ /**
345
+ * @primitive b.mailBounce.parse
346
+ * @signature b.mailBounce.parse(payload, opts)
347
+ * @since 0.5.0
348
+ * @status stable
349
+ * @related b.mailBounce.handler
350
+ *
351
+ * Pure synchronous parser. Routes `payload` through the chosen vendor
352
+ * parser (built-ins: `postmark`, `ses`, `resend`) and returns the
353
+ * normalized event. Operators with bespoke vendors supply
354
+ * `opts.parser` — a function `(payload) -> normalizedEvent` that the
355
+ * framework runs and then validates so a misbehaving custom parser
356
+ * cannot emit malformed audit rows.
357
+ *
358
+ * Throws `MailBounceError` (HTTP 400) on missing / unknown vendor,
359
+ * empty payload, or payload missing the required vendor-specific
360
+ * fields. Never mutates `payload` — the original is preserved on
361
+ * `event.raw` for downstream re-parsing.
362
+ *
363
+ * @opts
364
+ * vendor: "postmark" | "ses" | "resend", // required when `parser` is absent
365
+ * parser: function (payload): normalizedEvent, // alternative to `vendor`
366
+ *
367
+ * @example
368
+ * var event = b.mailBounce.parse({
369
+ * RecordType: "Bounce",
370
+ * Type: "HardBounce",
371
+ * Email: "user@example.com",
372
+ * MessageID: "abc-123",
373
+ * Description: "550 No such mailbox",
374
+ * BouncedAt: "2026-04-28T10:00:00Z",
375
+ * }, { vendor: "postmark" });
376
+ * event.type; // → "bounce"
377
+ * event.subType; // → "hard"
378
+ * event.recipient; // → "user@example.com"
379
+ * event.timestamp; // → "2026-04-28T10:00:00Z"
380
+ */
371
381
  function parse(payload, opts) {
372
382
  opts = opts || {};
373
383
  if (typeof opts.parser === "function") {
@@ -387,12 +397,47 @@ function parse(payload, opts) {
387
397
  return fn(payload);
388
398
  }
389
399
 
390
- // ---- Webhook handler middleware ----
391
- //
392
- // Buffers the request body (JSON), runs verify() if supplied, parses
393
- // via the configured vendor / custom parser, emits the audit event,
394
- // invokes onBounce, and responds 200. Errors map to 400 (bad payload),
395
- // 401 (verify rejected), 413 (body too large), 500 (onBounce threw).
400
+ /**
401
+ * @primitive b.mailBounce.handler
402
+ * @signature b.mailBounce.handler(opts)
403
+ * @since 0.5.0
404
+ * @status stable
405
+ * @related b.mailBounce.parse, b.audit.safeEmit
406
+ *
407
+ * Returns an Express-style `(req, res)` middleware that buffers the
408
+ * inbound webhook body (capped at `maxBytes`), runs `verify(req, body,
409
+ * raw)` if supplied, parses via the configured vendor (or custom
410
+ * `parser`), emits one `system.mail.bounce` audit row, calls
411
+ * `onBounce(event)`, and responds 200. Failures map to 400 (bad
412
+ * payload), 401 (verify rejected), 413 (body too large), 500
413
+ * (`onBounce` threw).
414
+ *
415
+ * `audit` defaults to ON; pass `audit: false` to suppress the
416
+ * `system.mail.bounce` row when the operator already records the
417
+ * normalized event in their own data store.
418
+ *
419
+ * @opts
420
+ * vendor: "postmark" | "ses" | "resend",
421
+ * parser: function (payload): normalizedEvent, // alternative to `vendor`
422
+ * verify: function (req, body, raw): boolean, // optional authenticity gate
423
+ * onBounce: function (event): Promise|void, // operator suppression hook
424
+ * audit: boolean, // default: true
425
+ * maxBytes: number, // body cap; default 256 KiB
426
+ *
427
+ * @example
428
+ * var bounce = b.mailBounce.handler({
429
+ * vendor: "postmark",
430
+ * verify: function (req) {
431
+ * return req.headers.authorization === "Basic c2VjcmV0";
432
+ * },
433
+ * onBounce: function (event) {
434
+ * suppressionList[event.recipient] = event.subType;
435
+ * },
436
+ * maxBytes: b.constants.BYTES.kib(64),
437
+ * });
438
+ * typeof bounce; // → "function"
439
+ * bounce.length; // → 2
440
+ */
396
441
  function handler(opts) {
397
442
  opts = opts || {};
398
443
  var vendor = opts.vendor;