@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
package/lib/network.js CHANGED
@@ -1,5 +1,36 @@
1
1
  "use strict";
2
+ /**
3
+ * @module b.network
4
+ * @featured true
5
+ * @nav Network
6
+ * @title Network
7
+ *
8
+ * @intro
9
+ * Framework network helpers — DNS-over-HTTPS dispatch, TLS
10
+ * configuration, OCSP/CT validation, NTP/NTS-KE bootstrap.
11
+ *
12
+ * `b.network` is the umbrella facade over the framework's outbound-
13
+ * network surface: DNS (default DoH on, optional DoT, lookup cache
14
+ * with TTL bound), TLS trust store (CA bundle / system trust /
15
+ * ignored-cert opt-in), proxy resolution from `HTTP_PROXY` /
16
+ * `HTTPS_PROXY` / `NO_PROXY`, NTP / NTS-KE drift checks, SMTP
17
+ * policy (MTA-STS / DANE / TLS-RPT), heartbeat watchdog, byte
18
+ * quota, SSRF allowlist, and socket-level defaults
19
+ * (TCP_NODELAY / SO_KEEPALIVE).
20
+ *
21
+ * `bootFromEnv` reads BLAMEJS_* environment variables once at
22
+ * startup and applies the union to the live facade — operators
23
+ * wire it from a process-supervisor's env without touching code.
24
+ * `snapshot` returns a redacted view of the current configuration
25
+ * for the operations dashboard. `applyToSocket` is the per-socket
26
+ * tuning hook for primitives building their own server (`tls`,
27
+ * `wsServer`, etc.).
28
+ *
29
+ * @card
30
+ * Framework network helpers — DNS-over-HTTPS dispatch, TLS configuration, OCSP/CT validation, NTP/NTS-KE bootstrap.
31
+ */
2
32
 
33
+ var byteQuota = require("./network-byte-quota");
3
34
  var ntpCheck = require("./ntp-check");
4
35
  var nts = require("./network-nts");
5
36
  var dns = require("./network-dns");
@@ -72,6 +103,28 @@ function _socketDefaults() {
72
103
  };
73
104
  }
74
105
 
106
+ /**
107
+ * @primitive b.network.applyToSocket
108
+ * @signature b.network.applyToSocket(socket)
109
+ * @since 0.7.68
110
+ * @related b.network.bootFromEnv, b.network.snapshot
111
+ *
112
+ * Apply the framework's socket defaults (`TCP_NODELAY`,
113
+ * `SO_KEEPALIVE` + initial-delay) to a freshly-created
114
+ * `net.Socket` / `tls.TLSSocket`. Best-effort: a socket that has
115
+ * already errored, lacks the setter methods, or rejects the call
116
+ * is left as-is. Returns the same socket. Used by primitives that
117
+ * build their own server (`b.tls`, `b.wsServer`, `b.smtp`) so
118
+ * every socket on the wire follows the same tuning.
119
+ *
120
+ * @example
121
+ * var net = require("net");
122
+ * var s = new net.Socket();
123
+ * var ret = b.network.applyToSocket(s);
124
+ * ret === s;
125
+ * // → true
126
+ * s.destroy();
127
+ */
75
128
  function applyToSocket(socket) {
76
129
  if (!socket) return socket;
77
130
  try {
@@ -103,6 +156,34 @@ var ntpFacade = {
103
156
  nts: nts,
104
157
  };
105
158
 
159
+ /**
160
+ * @primitive b.network.bootFromEnv
161
+ * @signature b.network.bootFromEnv(opts)
162
+ * @since 0.7.68
163
+ * @related b.network.snapshot, b.network.applyToSocket
164
+ *
165
+ * Read `BLAMEJS_*` environment variables once and apply the union to
166
+ * the live network facade. Recognised keys cover NTP servers /
167
+ * timeout / drift thresholds, DNS servers / result-order / family /
168
+ * lookup-timeout / cache-TTL / DoH URL or provider / DoT host+port,
169
+ * `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`, extra-CA file or
170
+ * directory, `BLAMEJS_USE_SYSTEM_TRUST`, and the socket
171
+ * `TCP_NODELAY` / `SO_KEEPALIVE` defaults. Returns an `applied`
172
+ * report — exactly which keys took effect. Audits
173
+ * `network.boot.from_env` unless `opts.audit:false`.
174
+ *
175
+ * @opts
176
+ * env: object, // default process.env — pass a fixture object in tests
177
+ * audit: boolean, // default true — emit `network.boot.from_env`
178
+ *
179
+ * @example
180
+ * var applied = b.network.bootFromEnv({
181
+ * env: { BLAMEJS_NTP_SERVERS: "time.cloudflare.com,time.google.com" },
182
+ * audit: false,
183
+ * });
184
+ * applied.ntp.servers;
185
+ * // → 2
186
+ */
106
187
  function bootFromEnv(opts) {
107
188
  opts = opts || {};
108
189
  validateOpts(opts, ["env", "audit"], "network.bootFromEnv");
@@ -181,6 +262,24 @@ function bootFromEnv(opts) {
181
262
  return applied;
182
263
  }
183
264
 
265
+ /**
266
+ * @primitive b.network.snapshot
267
+ * @signature b.network.snapshot()
268
+ * @since 0.7.68
269
+ * @related b.network.bootFromEnv
270
+ *
271
+ * Return a redacted snapshot of the network facade's current
272
+ * configuration: NTP servers + drift thresholds, DNS state (servers,
273
+ * result-order, family, DoH/DoT, cache TTL), proxy resolution, TLS
274
+ * trust-store size + system-trust flag, heartbeat statuses, and
275
+ * socket defaults. Cheap; safe to call from a `/healthz` or
276
+ * operations endpoint. No secrets are returned.
277
+ *
278
+ * @example
279
+ * var snap = b.network.snapshot();
280
+ * typeof snap.tls.caCount;
281
+ * // → "number"
282
+ */
184
283
  function snapshot() {
185
284
  return {
186
285
  ntp: {
@@ -228,6 +327,10 @@ module.exports = {
228
327
  tlsRpt: smtpPolicy.tlsRpt,
229
328
  },
230
329
  allowlist: { create: ssrfGuard.createAllowlist },
330
+ byteQuota: {
331
+ create: byteQuota.create,
332
+ ByteQuotaError: byteQuota.ByteQuotaError,
333
+ },
231
334
  socket: {
232
335
  setDefaultNoDelay: _setSocketNoDelay,
233
336
  setDefaultKeepAlive: _setSocketKeepAlive,
package/lib/notify.js CHANGED
@@ -1,50 +1,43 @@
1
1
  "use strict";
2
2
  /**
3
- * b.notify — generic notification dispatcher.
3
+ * @module b.notify
4
+ * @nav Communication
5
+ * @title Notify
4
6
  *
5
- * Composes existing primitives — retry/backoff (b.retry), per-call
6
- * timeout (b.safeAsync.withTimeout), per-channel circuit breaker
7
- * (b.retry.CircuitBreaker), span+counter wrapping
8
- * (b.observability.tap), audit emission (b.audit.safeEmit),
9
- * 5 W's actor context (b.requestHelpers.extractActorContext), URL
10
- * validation (b.safeUrl.parse), HTTP I/O (b.httpClient.request), PII
11
- * redaction (b.redact.redact). Notify never re-implements any of these
12
- * — its job is to coordinate them around a transport abstraction.
7
+ * @intro
8
+ * Pluggable notification dispatcher. One contract — `{ name, send }`
9
+ * adapts any transport (Slack incoming-webhook, Discord, Microsoft
10
+ * Teams, PagerDuty, Twilio, FCM / APNs operator shim, plain
11
+ * developer log) and the dispatcher coordinates retry / timeout /
12
+ * circuit-breaker / observability / audit / PII redaction around it.
13
13
  *
14
- * var notify = b.notify.create({
15
- * channels: {
16
- * slack: b.notify.transports.httpJson({ url: process.env.SLACK_HOOK }),
17
- * sms: operatorTwilioShim,
18
- * log: b.notify.transports.log(),
19
- * },
20
- * audit: b.audit,
21
- * });
22
- *
23
- * await notify.send({ channel: "slack", message: { text: "Hello" } });
14
+ * Composition over reinvention: every cross-cutting concern routes
15
+ * through an existing primitive — `b.retry.withRetry` for backoff +
16
+ * classification, `b.safeAsync.withTimeout` for per-call timeouts,
17
+ * `b.retry.CircuitBreaker` for per-channel breakers, `b.observability.
18
+ * tap` for span+counter wrapping, `b.audit.safeEmit` for audit rows
19
+ * (drop-silent on transport failure — observability sinks must not
20
+ * crash send), `b.requestHelpers.extractActorContext` for the 5 W's,
21
+ * `b.safeUrl.parse` + `b.httpClient.request` for HTTP I/O,
22
+ * `b.redact.redact` for default PII scrubbing of message contents
23
+ * before they hit the audit chain.
24
24
  *
25
- * Transport contract:
25
+ * Built-in transports: `httpJson` (POST JSON / form to a URL — the
26
+ * workhorse for Slack / Discord / generic incoming-webhook
27
+ * integrations, with optional `b.webhook.signer` injection),
28
+ * `log` (fire-and-forget developer logger via `b.log`),
29
+ * `test` (captures sends to `.sent[]` for fixture inspection).
30
+ * Operators bring their own SDK shims for Twilio / FCM / APNs /
31
+ * Slack-API (the framework intentionally ships no vendor SDKs).
26
32
  *
27
- * {
28
- * name: "slack",
29
- * send: async function (message, sendOpts) {
30
- * // Returns { id?, status, attempts?, durationMs? }
31
- * // Throws on permanent failure. For transient failures, throw
32
- * // with err.statusCode in b.retry.RETRYABLE_HTTP_STATUS or
33
- * // err.code in b.retry.RETRYABLE_NET_ERRORS — retry classifies
34
- * // them automatically. Operators can also set err.transient =
35
- * // true for shapes outside that classification.
36
- * },
37
- * }
33
+ * Out of scope by design: template rendering (use `b.template`),
34
+ * recipient preferences (operator concern), replacing `b.mail`
35
+ * (SMTP / MIME stays its own primitive), replacing
36
+ * `b.websocketChannels` (transient pub/sub vs retry-on-fail
37
+ * delivery).
38
38
  *
39
- * What this primitive intentionally does NOT do (per scope):
40
- * - Ship vendor SDKs (Twilio / FCM / APNs / Slack-API): operator brings
41
- * transports; the framework provides the contract + httpJson +
42
- * log + test built-ins.
43
- * - Render templates / handle i18n: operator uses b.template + b.i18n.
44
- * - Track recipient preferences: operator app concern.
45
- * - Replace b.mail (SMTP/MIME stays its own primitive).
46
- * - Replace b.websocketChannels (different abstraction —
47
- * transient pub/sub, not retry-on-fail delivery).
39
+ * @card
40
+ * Pluggable notification dispatcher.
48
41
  */
49
42
 
50
43
  var lazyRequire = require("./lazy-require");
@@ -162,9 +155,44 @@ function _validateCreateOpts(opts) {
162
155
 
163
156
  // ---- Built-in transports ----
164
157
 
165
- // httpJson — POST JSON to a URL via b.httpClient. Default `transient`
166
- // classifier defers to b.retry.isRetryable so HTTP/network classification
167
- // matches the rest of the framework.
158
+ /**
159
+ * @primitive b.notify.transports.httpJson
160
+ * @signature b.notify.transports.httpJson(opts)
161
+ * @since 0.6.0
162
+ * @status stable
163
+ * @related b.notify.create, b.webhook.signer
164
+ *
165
+ * Built-in transport that POSTs the message as JSON (or
166
+ * `application/x-www-form-urlencoded`) to a URL via `b.httpClient.
167
+ * request`. Validates the URL at create time so bad URLs surface at
168
+ * boot, not at first send. Optional `signing` slot accepts any object
169
+ * with a `sign(body) → headers | { headers }` function — drop a
170
+ * `b.webhook.signer` straight in for HMAC / PQC signed deliveries.
171
+ * The default success classifier accepts HTTP 2xx; non-success
172
+ * statuses throw a plain `Error` with `statusCode` set so
173
+ * `b.retry.isRetryable` classifies the response (429 / 503 / network
174
+ * errors retry; permanent rejections don't).
175
+ *
176
+ * @opts
177
+ * url: string, // required
178
+ * method: "POST" | "PUT" | "PATCH", // default "POST"
179
+ * bodyFormat: "json" | "form", // default "json"
180
+ * headers: { [k]: string },
181
+ * signing: { sign(body) => headers | { headers } },
182
+ * successStatus: function (status) => boolean,
183
+ * allowHttp: boolean, // default false (HTTPS-only)
184
+ * allowInternal: boolean,
185
+ * httpClient: object, // override b.httpClient
186
+ * name: string, // for audit + logs
187
+ *
188
+ * @example
189
+ * var b = require("@blamejs/core");
190
+ * var slack = b.notify.transports.httpJson({
191
+ * url: "https://hooks.slack.com/services/T0/B0/X",
192
+ * name: "slack",
193
+ * });
194
+ * // → { name: "slack", send: async function (message, sendOpts) { ... } }
195
+ */
168
196
  function httpJson(opts) {
169
197
  if (!opts || typeof opts !== "object") {
170
198
  throw _err("BAD_OPT", "notify.transports.httpJson: opts must be { url, ... }");
@@ -293,6 +321,49 @@ function testTransport() {
293
321
 
294
322
  // ---- Public create ----
295
323
 
324
+ /**
325
+ * @primitive b.notify.create
326
+ * @signature b.notify.create(opts)
327
+ * @since 0.6.0
328
+ * @status stable
329
+ * @compliance soc2, gdpr
330
+ * @related b.notify.transports.httpJson
331
+ *
332
+ * Build a dispatcher bound to a set of named channels. Returns
333
+ * `{ send, sendBatch, queue, addChannel, channels, transport }`:
334
+ * `send` delivers one message through one channel with the full retry /
335
+ * timeout / breaker / span+counter / audit stack; `sendBatch` settles
336
+ * each input independently so one channel down doesn't fail the rest;
337
+ * `queue` enqueues onto a `b.queue` handle for out-of-band delivery;
338
+ * `addChannel` registers a new channel post-construction;
339
+ * `channels()` lists registered names; `transport(name)` exposes the
340
+ * raw transport handle for diagnostics. Each channel entry is either
341
+ * a transport object directly (`{ send, name? }`) or a config wrapper
342
+ * (`{ transport, retry?, breaker?, timeoutMs?, serialize? }`) so
343
+ * operators tune retry / breaker / timeout / serialize per channel.
344
+ *
345
+ * @opts
346
+ * channels: { [name]: transport | { transport, retry?, breaker?, timeoutMs?, serialize? } },
347
+ * audit: object, // b.audit handle
348
+ * auditSuccess: boolean, // default true
349
+ * auditFailures: boolean, // default true
350
+ * redact: function (message) => any, // default b.redact.redact
351
+ * defaultTimeoutMs: number, // default 30s, 0 disables
352
+ * defaultRetry: object, // b.retry.withRetry opts
353
+ * defaultBreaker: object, // b.retry.CircuitBreaker opts
354
+ * queue: { enqueue(name, payload), registerHandler? },
355
+ * clock: function () => number, // ms
356
+ *
357
+ * @example
358
+ * var b = require("@blamejs/core");
359
+ * var notify = b.notify.create({
360
+ * channels: {
361
+ * slack: b.notify.transports.httpJson({ url: "https://hooks.slack.com/services/T0/B0/X" }),
362
+ * log: b.notify.transports.log(),
363
+ * },
364
+ * });
365
+ * // → { send, sendBatch, queue, addChannel, channels, transport }
366
+ */
296
367
  function create(opts) {
297
368
  opts = opts || {};
298
369
  validateOpts(opts, [
package/lib/ntp-check.js CHANGED
@@ -1,29 +1,48 @@
1
1
  "use strict";
2
2
  /**
3
- * Minimal SNTP client for boot-time clock-drift verification.
3
+ * @module b.ntpCheck
4
+ * @nav Production
5
+ * @title NTP Check
4
6
  *
5
- * Why: the audit chain's monotonicCounter orders events deterministically
6
- * even if the wall clock jumps, but recordedAt is the human-readable
7
- * timestamp auditors will rely on. A clock that's silently off by hours
8
- * (container with no RTC sync, NTP daemon stopped) makes the audit trail
9
- * misleading.
7
+ * @intro
8
+ * Boot-time clock-drift verification against an external NTP / NTS-KE
9
+ * reference. The audit chain's `monotonicCounter` orders events
10
+ * deterministically even when the wall clock jumps, but `recordedAt`
11
+ * is the human-readable timestamp auditors rely on — a clock silently
12
+ * off by hours (container with no RTC sync, NTP daemon stopped)
13
+ * makes the audit trail misleading without ever surfacing as an
14
+ * error.
10
15
  *
11
- * What this does:
12
- * - Sends a single SNTPv4 query to a configured server (default
13
- * pool.ntp.org) over UDP port 123.
14
- * - Computes drift = (server's transmit timestamp) - (local clock).
15
- * - Returns the drift in milliseconds.
16
+ * What this does: sends a single SNTPv4 query over UDP/123 (RFC 5905)
17
+ * to one or more configured servers, computes drift as
18
+ * `serverTransmit - localMidpoint` (round-trip-corrected), returns
19
+ * the drift in milliseconds. Falls through a server list in order;
20
+ * the first success wins.
16
21
  *
17
- * What this does NOT do:
18
- * - Continuous synchronization (use the OS NTP daemon for that).
19
- * - Authenticated NTP (NTS, autokey).
20
- * - Querying multiple servers and taking median (single-shot only).
22
+ * What this does NOT do: continuous synchronization (the host OS's
23
+ * NTP daemon does that), authenticated NTP / NTS / autokey (the
24
+ * external reference is trust-on-first-query), or median-of-N
25
+ * server reconciliation (single-shot only).
21
26
  *
22
- * The framework's policy in db.init():
23
- * - drift |x| < 5min → log info, continue
24
- * - drift |x| in [5min,1hr) log warning, continue
25
- * - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
26
- * - NTP unreachable → log warning, continue (network may not allow UDP/123)
27
+ * Policy thresholds at boot wired into `b.db.init`:
28
+ *
29
+ * drift |x| < warnMs (5 min default) info, continue
30
+ * drift |x| in [warnMs, fatalMs) warning, continue
31
+ * drift |x| >= fatalMs (1 hr default) → refuse to boot
32
+ * (BLAMEJS_NTP_STRICT=1)
33
+ * NTP unreachable → warning, continue
34
+ * (network may not allow
35
+ * UDP/123 outbound)
36
+ *
37
+ * `b.ntpCheck.monitor` runs the same check on a recurring interval
38
+ * after boot and emits `system.ntp.checked` /
39
+ * `system.ntp.drift_warn` / `system.ntp.drift_fatal` /
40
+ * `system.ntp.unreachable` audit events plus an `ntp.drift_ms`
41
+ * observability gauge — so silent clock drift mid-flight surfaces
42
+ * in the same evidence stream as boot drift.
43
+ *
44
+ * @card
45
+ * Boot-time clock-drift verification against an external NTP / NTS-KE reference.
27
46
  */
28
47
  var dgram = require("dgram");
29
48
  var C = require("./constants");
@@ -50,6 +69,32 @@ var thresholds = {
50
69
  fatalMs: DEFAULT_DRIFT_FATAL_MS,
51
70
  };
52
71
 
72
+ /**
73
+ * @primitive b.ntpCheck.setThresholds
74
+ * @signature b.ntpCheck.setThresholds(opts)
75
+ * @since 0.7.30
76
+ * @status stable
77
+ * @related b.ntpCheck.getThresholds, b.ntpCheck.bootCheck
78
+ *
79
+ * Override the warn / fatal drift thresholds applied by `bootCheck`
80
+ * and `monitor`. Validates that both values are non-negative finite
81
+ * numbers and that `warnMs <= fatalMs` (a fatal floor below the
82
+ * warning threshold would mean every warning is also fatal — likely
83
+ * a typo). Throws `TypeError` on bad shapes and `RangeError` on the
84
+ * ordering invariant.
85
+ *
86
+ * @opts
87
+ * warnMs: 300000, // ms; absolute drift at-or-above this logs warn
88
+ * fatalMs: 3600000, // ms; absolute drift at-or-above this refuses boot
89
+ *
90
+ * @example
91
+ * b.ntpCheck.setThresholds({
92
+ * warnMs: 60000,
93
+ * fatalMs: 600000,
94
+ * });
95
+ * var t = b.ntpCheck.getThresholds();
96
+ * // → { warnMs: 60000, fatalMs: 600000 }
97
+ */
53
98
  function setThresholds(opts) {
54
99
  opts = opts || {};
55
100
  if (opts.warnMs !== undefined) {
@@ -70,6 +115,21 @@ function setThresholds(opts) {
70
115
  }
71
116
  }
72
117
 
118
+ /**
119
+ * @primitive b.ntpCheck.getThresholds
120
+ * @signature b.ntpCheck.getThresholds()
121
+ * @since 0.7.30
122
+ * @status stable
123
+ * @related b.ntpCheck.setThresholds
124
+ *
125
+ * Read the currently-effective warn / fatal drift thresholds. Returns
126
+ * a fresh object so mutating the result doesn't accidentally rewrite
127
+ * framework state.
128
+ *
129
+ * @example
130
+ * var t = b.ntpCheck.getThresholds();
131
+ * // → { warnMs: 300000, fatalMs: 3600000 }
132
+ */
73
133
  function getThresholds() {
74
134
  return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
75
135
  }
@@ -80,11 +140,29 @@ function _resetThresholdsForTest() {
80
140
  }
81
141
 
82
142
  /**
83
- * Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
84
- * rejects with { code, message } where code is one of:
85
- * 'ntp/timeout' — server didn't reply within timeoutMs
86
- * 'ntp/refused' — DNS/connection error
87
- * 'ntp/bad-reply' — packet structure wrong
143
+ * @primitive b.ntpCheck.querySingle
144
+ * @signature b.ntpCheck.querySingle(server, opts)
145
+ * @since 0.0.7
146
+ * @status stable
147
+ * @related b.ntpCheck.checkDrift, b.ntpCheck.bootCheck
148
+ *
149
+ * Send one SNTPv4 query to a named server over UDP/123 and resolve
150
+ * with `{ driftMs, serverTimeMs, server }` (round-trip-corrected
151
+ * drift). Rejects with `{ code, message }` where `code` is one of
152
+ * `ntp/timeout` (no reply within `timeoutMs`), `ntp/refused`
153
+ * (DNS / connection error), `ntp/bad-reply` (packet too short), or
154
+ * `ntp/unsynchronized` (Stratum-16 peer with zero transmit
155
+ * timestamp). IPv4 / IPv6 socket family is selected from the host
156
+ * literal so an `fd00::...` server doesn't fail with EINVAL.
157
+ *
158
+ * @opts
159
+ * port: 123, // UDP port (almost always 123)
160
+ * timeoutMs: 3000, // single-query timeout
161
+ *
162
+ * @example
163
+ * b.ntpCheck.querySingle("time.cloudflare.com", { timeoutMs: 2000 })
164
+ * .then(function (r) { console.log("drift", r.driftMs, "ms"); })
165
+ * .catch(function (e) { console.error("ntp", e.code, e.message); });
88
166
  */
89
167
  function querySingle(server, opts) {
90
168
  opts = opts || {};
@@ -165,8 +243,28 @@ function querySingle(server, opts) {
165
243
  }
166
244
 
167
245
  /**
168
- * Try each server in turn; return the first successful drift measurement.
169
- * Resolves null if all servers fail (caller decides whether that's fatal).
246
+ * @primitive b.ntpCheck.checkDrift
247
+ * @signature b.ntpCheck.checkDrift(opts)
248
+ * @since 0.0.7
249
+ * @status stable
250
+ * @related b.ntpCheck.querySingle, b.ntpCheck.bootCheck
251
+ *
252
+ * Walk a server list in order; resolve with the first successful
253
+ * drift measurement (`{ driftMs, serverTimeMs, server }`). When
254
+ * every server in the list fails, resolves with
255
+ * `{ driftMs: null, error }` so the caller — typically `bootCheck` —
256
+ * can decide whether unreachable NTP is fatal or a soft warning.
257
+ *
258
+ * @opts
259
+ * servers: ["time.cloudflare.com", "pool.ntp.org"],
260
+ * port: 123,
261
+ * timeoutMs: 3000,
262
+ *
263
+ * @example
264
+ * var result = await b.ntpCheck.checkDrift({
265
+ * servers: ["time.cloudflare.com", "pool.ntp.org"],
266
+ * });
267
+ * // → { driftMs: 12, serverTimeMs: 1714694400000, server: "time.cloudflare.com" }
170
268
  */
171
269
  async function checkDrift(opts) {
172
270
  opts = opts || {};
@@ -183,9 +281,37 @@ async function checkDrift(opts) {
183
281
  }
184
282
 
185
283
  /**
186
- * Boot-time check that integrates with the framework's logging policy.
187
- * Returns a result object with { ok, driftMs, severity, message }.
188
- * Caller (db.init) decides whether to exit.
284
+ * @primitive b.ntpCheck.bootCheck
285
+ * @signature b.ntpCheck.bootCheck(opts)
286
+ * @since 0.0.7
287
+ * @status stable
288
+ * @related b.ntpCheck.checkDrift, b.ntpCheck.monitor, b.ntpCheck.setThresholds
289
+ *
290
+ * Boot-time clock-drift check that integrates with the framework's
291
+ * logging policy. Resolves with
292
+ * `{ ok, severity, driftMs, server, message }` where `severity` is
293
+ * `info` / `warning` / `fatal`. The framework's `b.db.init` calls
294
+ * this and refuses to boot when `ok === false` and the operator has
295
+ * set `BLAMEJS_NTP_STRICT=1`. NTP unreachable returns
296
+ * `severity: "warning"` (network may not allow UDP/123 outbound) so
297
+ * the boot doesn't fail closed without operator intent.
298
+ *
299
+ * @opts
300
+ * servers: ["time.cloudflare.com", "pool.ntp.org"],
301
+ * port: 123,
302
+ * timeoutMs: 3000,
303
+ * driftWarnMs: 300000, // override registered warn threshold
304
+ * driftFatalMs: 3600000, // override registered fatal threshold
305
+ *
306
+ * @example
307
+ * var result = await b.ntpCheck.bootCheck({
308
+ * servers: ["time.cloudflare.com"],
309
+ * driftWarnMs: 60000,
310
+ * driftFatalMs: 600000,
311
+ * });
312
+ * // → { ok: true, severity: "info", driftMs: 12,
313
+ * // server: "time.cloudflare.com",
314
+ * // message: "clock drift +12ms from time.cloudflare.com" }
189
315
  */
190
316
  async function bootCheck(opts) {
191
317
  opts = opts || {};
@@ -232,27 +358,42 @@ async function bootCheck(opts) {
232
358
  };
233
359
  }
234
360
 
235
- // Periodic drift monitor — runs checkDrift on a schedule and emits
236
- // audit + observability events on threshold crossings. Returns a
237
- // handle with `.stop()` for graceful shutdown.
238
- //
239
- // var mon = b.ntpCheck.monitor({
240
- // intervalMs: C.TIME.minutes(15),
241
- // servers: ["time.cloudflare.com", "pool.ntp.org"],
242
- // driftWarnMs: C.TIME.seconds(2),
243
- // driftFatalMs: C.TIME.seconds(30),
244
- // onDrift: function (result) { /* operator hook drift > warn */ },
245
- // });
246
- // ...
247
- // await mon.stop();
248
- //
249
- // Audit emissions:
250
- // system.ntp.checked — every check, success or fail
251
- // system.ntp.drift_warn — drift exceeds warn threshold
252
- // system.ntp.drift_fatal — drift exceeds fatal threshold
253
- // system.ntp.unreachable every server in the list failed to respond
254
- //
255
- // Observability events: ntp.drift_ms (gauge) on every successful check.
361
+ /**
362
+ * @primitive b.ntpCheck.monitor
363
+ * @signature b.ntpCheck.monitor(opts)
364
+ * @since 0.7.30
365
+ * @status stable
366
+ * @related b.ntpCheck.bootCheck, b.audit.safeEmit, b.observability.safeEvent
367
+ *
368
+ * Periodic drift monitor — runs `bootCheck` on a recurring interval
369
+ * and emits audit + observability events on threshold crossings.
370
+ * Returns a handle with `.stop()` for graceful shutdown. Audit
371
+ * emissions: `system.ntp.checked` on every tick,
372
+ * `system.ntp.drift_warn` and `system.ntp.drift_fatal` on threshold
373
+ * crossings, `system.ntp.unreachable` when every server in the list
374
+ * failed. Observability gauge `ntp.drift_ms` rides every successful
375
+ * check. The optional `onDrift` hook fires only when `severity`
376
+ * is `warning` or `fatal`, so operators can page on drift without
377
+ * inspecting every healthy tick.
378
+ *
379
+ * @opts
380
+ * intervalMs: 900000, // tick cadence
381
+ * servers: ["time.cloudflare.com", "pool.ntp.org"],
382
+ * driftWarnMs: 2000,
383
+ * driftFatalMs: 30000,
384
+ * audit: true, // emit audit events
385
+ * onDrift: function (result) {}, // operator hook
386
+ *
387
+ * @example
388
+ * var mon = b.ntpCheck.monitor({
389
+ * intervalMs: 900000,
390
+ * servers: ["time.cloudflare.com", "pool.ntp.org"],
391
+ * driftWarnMs: 2000,
392
+ * driftFatalMs: 30000,
393
+ * onDrift: function (r) { console.warn("ntp drift", r.driftMs); },
394
+ * });
395
+ * await mon.stop();
396
+ */
256
397
  function monitor(opts) {
257
398
  opts = opts || {};
258
399
  var intervalMs = opts.intervalMs || C.TIME.minutes(15);