@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
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * b.network.byteQuota — per-key rolling 24-hour byte budget primitive.
4
+ *
5
+ * Provides a callable preflight + record surface used by handlers that
6
+ * already know the byte cost of an operation BEFORE accepting it (e.g.
7
+ * a multipart upload whose Content-Length is known at headers-parsed
8
+ * time, an SSE feed whose payload size is computed by the originator,
9
+ * a webhook whose body size is asserted via signed manifest):
10
+ *
11
+ * var quota = b.network.byteQuota.create({
12
+ * bytesPerDay: b.constants.BYTES.gib(2),
13
+ * audit: b.audit,
14
+ * });
15
+ *
16
+ * // Preflight — returns { allowed, remaining, total, quota } without
17
+ * // mutating the counter. Refusal emits network.byte_quota.exceeded.
18
+ * var verdict = await quota.check(req.ip, fileSize);
19
+ * if (!verdict.allowed) return res.writeHead(429).end();
20
+ *
21
+ * // Commit — mutates the counter for the rolling-window slot.
22
+ * await quota.record(req.ip, fileSize);
23
+ *
24
+ * // Operator helpers
25
+ * await quota.reset(req.ip);
26
+ * var snap = await quota.snapshot(); // [{ key, total, quota, remaining }]
27
+ *
28
+ * The middleware in lib/middleware/daily-byte-quota.js composes this
29
+ * primitive — there's no parallel byte-counter store.
30
+ *
31
+ * Failure modes:
32
+ * - cache backend unreachable → fail-open on check (verdict.allowed
33
+ * true with verdict.degraded = true) so a flaky cache can't take
34
+ * down the framework; record swallows the error after audit so the
35
+ * handler that already accepted the bytes isn't punished. Both
36
+ * paths emit network.byte_quota.backend_error.
37
+ * - bytesPerDay <= 0 / non-finite at create() throws. Per-call byte
38
+ * counts <0 / non-finite at check/record throw NetworkError.
39
+ */
40
+
41
+ var C = require("./constants");
42
+ var defineClass = require("./framework-error").defineClass;
43
+ var lazyRequire = require("./lazy-require");
44
+ var validateOpts = require("./validate-opts");
45
+
46
+ var auditFwk = lazyRequire(function () { return require("./audit"); });
47
+ var observability = lazyRequire(function () { return require("./observability"); });
48
+
49
+ var ByteQuotaError = defineClass("ByteQuotaError", { alwaysPermanent: true });
50
+
51
+ var BINS_PER_DAY = 24; // allow:raw-byte-literal — 24 hours in a day
52
+ var BIN_MS = C.TIME.hours(1);
53
+
54
+ function _hourBin(nowMs) { return Math.floor(nowMs / BIN_MS); }
55
+ function _newEntry() { return { bins: new Array(BINS_PER_DAY).fill(0), startHour: 0 }; }
56
+
57
+ // Shared sliding-window helper — both backends call this so the per-bin
58
+ // shift / zero / total math lives in one place. Caller persists the
59
+ // returned entry when it's shared state (cache backend writes back).
60
+ function _slideAndSum(entry, nowHour) {
61
+ if (entry.startHour === 0) entry.startHour = nowHour - (BINS_PER_DAY - 1);
62
+ var advance = nowHour - (entry.startHour + (BINS_PER_DAY - 1));
63
+ var moved = false;
64
+ if (advance > 0) {
65
+ moved = true;
66
+ if (advance >= BINS_PER_DAY) {
67
+ for (var i = 0; i < BINS_PER_DAY; i++) entry.bins[i] = 0;
68
+ } else {
69
+ for (var j = 0; j < BINS_PER_DAY - advance; j++) entry.bins[j] = entry.bins[j + advance];
70
+ for (var k = BINS_PER_DAY - advance; k < BINS_PER_DAY; k++) entry.bins[k] = 0;
71
+ }
72
+ entry.startHour = nowHour - (BINS_PER_DAY - 1);
73
+ }
74
+ var total = 0;
75
+ for (var t = 0; t < BINS_PER_DAY; t++) total += entry.bins[t];
76
+ return { entry: entry, total: total, moved: moved };
77
+ }
78
+
79
+ function _memoryBackend() {
80
+ var store = new Map();
81
+ function _get(key) {
82
+ var entry = store.get(key);
83
+ if (!entry) { entry = _newEntry(); store.set(key, entry); }
84
+ return entry;
85
+ }
86
+ return {
87
+ async total(key, nowMs) {
88
+ return _slideAndSum(_get(key), _hourBin(nowMs)).total;
89
+ },
90
+ async account(key, bytes, nowMs) {
91
+ var slid = _slideAndSum(_get(key), _hourBin(nowMs));
92
+ slid.entry.bins[BINS_PER_DAY - 1] += bytes;
93
+ },
94
+ async reset(key) {
95
+ store.delete(key);
96
+ },
97
+ async snapshot(nowMs) {
98
+ var nowHour = _hourBin(nowMs);
99
+ var out = [];
100
+ for (var key of store.keys()) {
101
+ var slid = _slideAndSum(_get(key), nowHour);
102
+ out.push({ key: key, total: slid.total });
103
+ }
104
+ return out;
105
+ },
106
+ _resetForTest: function () { store.clear(); },
107
+ };
108
+ }
109
+
110
+ function _cacheBackend(cache) {
111
+ function _key(k) { return "byteQuota:" + k; }
112
+ async function _read(key) {
113
+ var raw = await cache.get(_key(key));
114
+ return raw && typeof raw === "object" && Array.isArray(raw.bins) ? raw : _newEntry();
115
+ }
116
+ return {
117
+ async total(key, nowMs) {
118
+ var entry = await _read(key);
119
+ var slid = _slideAndSum(entry, _hourBin(nowMs));
120
+ if (slid.moved) await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
121
+ return slid.total;
122
+ },
123
+ async account(key, bytes, nowMs) {
124
+ var entry = await _read(key);
125
+ var slid = _slideAndSum(entry, _hourBin(nowMs));
126
+ slid.entry.bins[BINS_PER_DAY - 1] += bytes;
127
+ await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
128
+ },
129
+ async reset(key) {
130
+ if (typeof cache.delete === "function") await cache.delete(_key(key));
131
+ else if (typeof cache.del === "function") await cache.del(_key(key));
132
+ else await cache.set(_key(key), _newEntry(), { ttlMs: 1 });
133
+ },
134
+ // Cache backends don't enumerate by prefix portably — snapshot()
135
+ // returns an empty list when wired with a cache backend. Operators
136
+ // that need cluster-wide enumeration query the cache directly with
137
+ // their backend's idiomatic scan op.
138
+ async snapshot(_nowMs) { return []; },
139
+ };
140
+ }
141
+
142
+ function _requirePositiveBytes(name, value) {
143
+ if (typeof value !== "number" || !isFinite(value) || value <= 0) {
144
+ throw new ByteQuotaError(
145
+ "byte-quota/bad-quota",
146
+ "network.byteQuota: " + name + " must be a positive finite number; " +
147
+ "use b.constants.BYTES.gib(N) / mib(N) for readable values"
148
+ );
149
+ }
150
+ }
151
+
152
+ function _requireNonNegativeBytes(name, value) {
153
+ if (typeof value !== "number" || !isFinite(value) || value < 0) {
154
+ throw new ByteQuotaError(
155
+ "byte-quota/bad-bytes",
156
+ "network.byteQuota: " + name + " must be a non-negative finite number, got " + JSON.stringify(value)
157
+ );
158
+ }
159
+ }
160
+
161
+ function _requireKey(key) {
162
+ if (typeof key !== "string" || key.length === 0) {
163
+ throw new ByteQuotaError(
164
+ "byte-quota/bad-key",
165
+ "network.byteQuota: key must be a non-empty string, got " + JSON.stringify(key)
166
+ );
167
+ }
168
+ }
169
+
170
+ function create(opts) {
171
+ opts = opts || {};
172
+ validateOpts(opts, ["bytesPerDay", "cache", "audit", "now"], "network.byteQuota");
173
+ _requirePositiveBytes("bytesPerDay", opts.bytesPerDay);
174
+ var bytesPerDay = opts.bytesPerDay;
175
+ var auditOn = opts.audit !== false;
176
+ var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
177
+ var backend = opts.cache && typeof opts.cache.get === "function"
178
+ ? _cacheBackend(opts.cache)
179
+ : _memoryBackend();
180
+
181
+ function _emitAudit(action, outcome, metadata) {
182
+ if (!auditOn) return;
183
+ try {
184
+ auditFwk().safeEmit({
185
+ action: "network.byte_quota." + action,
186
+ outcome: outcome,
187
+ metadata: metadata || {},
188
+ });
189
+ } catch (_e) { /* drop-silent — audit is best-effort */ }
190
+ }
191
+
192
+ function _emitMetric(verb, n, labels) {
193
+ try { observability().safeEvent("network.byte_quota." + verb, n || 1, labels || {}); }
194
+ catch (_e) { /* drop-silent */ }
195
+ }
196
+
197
+ // check(key, bytes) — preflight without mutation. Returns
198
+ // { allowed, total, remaining, quota, retryAfterSec, degraded }
199
+ // `degraded: true` indicates a backend error caused the verdict to
200
+ // fail-open; operators that want fail-closed inspect this flag.
201
+ async function check(key, bytes) {
202
+ _requireKey(key);
203
+ _requireNonNegativeBytes("bytes", bytes);
204
+ var nowMs = now();
205
+ var total;
206
+ try { total = await backend.total(key, nowMs); }
207
+ catch (e) {
208
+ _emitAudit("backend_error", "failure", { phase: "check", error: (e && e.message) || String(e) });
209
+ return {
210
+ allowed: true,
211
+ total: 0,
212
+ remaining: bytesPerDay,
213
+ quota: bytesPerDay,
214
+ retryAfterSec: 0,
215
+ degraded: true,
216
+ };
217
+ }
218
+ var projected = total + bytes;
219
+ var remaining = Math.max(0, bytesPerDay - total);
220
+ if (projected > bytesPerDay) {
221
+ _emitMetric("refused", 1, { reason: "quota-exceeded" });
222
+ _emitAudit("exceeded", "denied", { key: key, total: total, requested: bytes, quota: bytesPerDay });
223
+ return {
224
+ allowed: false,
225
+ total: total,
226
+ remaining: remaining,
227
+ quota: bytesPerDay,
228
+ retryAfterSec: Math.ceil(BIN_MS / C.TIME.seconds(1)),
229
+ degraded: false,
230
+ };
231
+ }
232
+ return {
233
+ allowed: true,
234
+ total: total,
235
+ remaining: Math.max(0, bytesPerDay - projected),
236
+ quota: bytesPerDay,
237
+ retryAfterSec: 0,
238
+ degraded: false,
239
+ };
240
+ }
241
+
242
+ // record(key, bytes) — commit the mutation. Used after the operation
243
+ // succeeded (or for in-flight middleware accounting via the rolling-
244
+ // counter middleware wrapper).
245
+ async function record(key, bytes) {
246
+ _requireKey(key);
247
+ _requireNonNegativeBytes("bytes", bytes);
248
+ if (bytes === 0) return;
249
+ var nowMs = now();
250
+ try { await backend.account(key, bytes, nowMs); }
251
+ catch (e) {
252
+ _emitAudit("backend_error", "failure", { phase: "record", key: key, bytes: bytes, error: (e && e.message) || String(e) });
253
+ // Drop-silent after audit — the operation already succeeded; the
254
+ // alternative throw would punish the handler that already accepted bytes.
255
+ return;
256
+ }
257
+ _emitMetric("recorded", bytes, {});
258
+ }
259
+
260
+ async function reset(key) {
261
+ _requireKey(key);
262
+ try { await backend.reset(key); }
263
+ catch (e) {
264
+ _emitAudit("backend_error", "failure", { phase: "reset", error: (e && e.message) || String(e) });
265
+ }
266
+ }
267
+
268
+ async function snapshot() {
269
+ var nowMs = now();
270
+ try {
271
+ var rows = await backend.snapshot(nowMs);
272
+ return rows.map(function (r) {
273
+ return {
274
+ key: r.key,
275
+ total: r.total,
276
+ quota: bytesPerDay,
277
+ remaining: Math.max(0, bytesPerDay - r.total),
278
+ };
279
+ });
280
+ } catch (e) {
281
+ _emitAudit("backend_error", "failure", { phase: "snapshot", error: (e && e.message) || String(e) });
282
+ return [];
283
+ }
284
+ }
285
+
286
+ return {
287
+ check: check,
288
+ record: record,
289
+ reset: reset,
290
+ snapshot: snapshot,
291
+ // Internals exposed for the middleware composition seam — same
292
+ // backend instance can serve both APIs (so middleware account()
293
+ // and standalone record() agree on the counter state).
294
+ _backend: backend,
295
+ _bytesPerDay: bytesPerDay,
296
+ _now: now,
297
+ };
298
+ }
299
+
300
+ module.exports = {
301
+ create: create,
302
+ ByteQuotaError: ByteQuotaError,
303
+ BINS_PER_DAY: BINS_PER_DAY,
304
+ // Internals exposed for tests + the middleware composition seam.
305
+ _memoryBackend: _memoryBackend,
306
+ _cacheBackend: _cacheBackend,
307
+ _slideAndSum: _slideAndSum,
308
+ };
@@ -18,6 +18,13 @@ var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
18
18
  var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
19
19
  var DEFAULT_THRESHOLD = 3;
20
20
 
21
+ // Passive heartbeats — caller (typically a WebSocket / SSE / long-poll
22
+ // handler) records each inbound ping/pong and the framework fires
23
+ // `onTimeout` once when the keepalive grace window elapses without a
24
+ // recordPong call. Distinct from `start()`, which runs an active
25
+ // outbound probe loop.
26
+ var DEFAULT_PASSIVE_TIMEOUT_MS = C.TIME.seconds(90);
27
+
21
28
  var TARGETS = new Map();
22
29
 
23
30
  function _validateTarget(t, idx) {
@@ -275,6 +282,133 @@ function _emitAuditStateChange(entry, prevState) {
275
282
  } catch (_e) { /* audit best-effort — never break the caller */ }
276
283
  }
277
284
 
285
+ // b.network.heartbeat.passive(opts) — passive (server-pushes-pings)
286
+ // keepalive watchdog. Caller invokes the returned `recordPong()` each
287
+ // time a heartbeat frame arrives from the peer; if `timeoutMs` elapses
288
+ // with no `recordPong`, the watchdog fires `onTimeout()` exactly once
289
+ // and stops. Operator restarts surveillance by calling `passive()`
290
+ // again — the primitive deliberately doesn't auto-rearm because the
291
+ // post-timeout strategy (close socket, re-handshake, retry, alert) is
292
+ // caller-specific.
293
+ //
294
+ // Returns:
295
+ // { recordPong, stop }
296
+ //
297
+ // `onPong` is the per-pong observability hook (optional). `onTimeout`
298
+ // is the callback fired when the timeout elapses (required). Both
299
+ // callbacks are invoked outside try/catch — operator callbacks throw
300
+ // only if the operator wants the host process to crash.
301
+ function passive(opts) {
302
+ opts = opts || {};
303
+ validateOpts(opts, ["onPong", "timeoutMs", "onTimeout"], "heartbeat.passive");
304
+ if (typeof opts.onTimeout !== "function") {
305
+ throw new HeartbeatError("heartbeat/bad-on-timeout",
306
+ "heartbeat.passive: onTimeout must be a function");
307
+ }
308
+ validateOpts.optionalFunction(opts.onPong, "heartbeat.passive: onPong",
309
+ HeartbeatError, "heartbeat/bad-on-pong");
310
+ var timeoutMs = opts.timeoutMs === undefined ? DEFAULT_PASSIVE_TIMEOUT_MS : opts.timeoutMs;
311
+ if (typeof timeoutMs !== "number" || !isFinite(timeoutMs) || timeoutMs <= 0) {
312
+ throw new HeartbeatError("heartbeat/bad-timeout",
313
+ "heartbeat.passive: timeoutMs must be a positive finite number");
314
+ }
315
+
316
+ var state = {
317
+ timer: null,
318
+ stopped: false,
319
+ timedOut: false,
320
+ startMs: Date.now(),
321
+ lastPongMs: null,
322
+ pongCount: 0,
323
+ onPong: opts.onPong || null,
324
+ onTimeout: opts.onTimeout,
325
+ timeoutMs: timeoutMs,
326
+ };
327
+
328
+ function _arm() {
329
+ state.timer = setTimeout(_fire, state.timeoutMs);
330
+ if (state.timer && typeof state.timer.unref === "function") state.timer.unref();
331
+ }
332
+
333
+ function _fire() {
334
+ if (state.stopped || state.timedOut) return;
335
+ state.timedOut = true;
336
+ state.stopped = true;
337
+ state.timer = null;
338
+ _emitObsTimeout(state);
339
+ _emitAuditPassiveTimeout(state);
340
+ try { state.onTimeout({ pongCount: state.pongCount, lastPongMs: state.lastPongMs, timeoutMs: state.timeoutMs }); }
341
+ catch (_e) { /* operator callback best-effort */ }
342
+ }
343
+
344
+ function recordPong() {
345
+ if (state.stopped || state.timedOut) return false;
346
+ state.pongCount += 1;
347
+ state.lastPongMs = Date.now();
348
+ if (state.timer) {
349
+ try { clearTimeout(state.timer); } catch (_e) { /* best-effort timer teardown */ }
350
+ state.timer = null;
351
+ }
352
+ _emitObsPong(state);
353
+ if (typeof state.onPong === "function") {
354
+ try { state.onPong({ pongCount: state.pongCount, lastPongMs: state.lastPongMs }); }
355
+ catch (_e) { /* operator callback best-effort */ }
356
+ }
357
+ _arm();
358
+ return true;
359
+ }
360
+
361
+ function stop() {
362
+ if (state.stopped) return false;
363
+ state.stopped = true;
364
+ if (state.timer) {
365
+ try { clearTimeout(state.timer); } catch (_e) { /* best-effort timer teardown */ }
366
+ state.timer = null;
367
+ }
368
+ return true;
369
+ }
370
+
371
+ _arm();
372
+ return { recordPong: recordPong, stop: stop };
373
+ }
374
+
375
+ function _emitObsPong(state) {
376
+ try {
377
+ observability().emit("network.heartbeat.passive.pong", {
378
+ pongCount: state.pongCount,
379
+ timeoutMs: state.timeoutMs,
380
+ });
381
+ } catch (_e) { /* obs best-effort */ }
382
+ }
383
+
384
+ function _emitObsTimeout(state) {
385
+ try {
386
+ observability().emit("network.heartbeat.passive.timeout", {
387
+ pongCount: state.pongCount,
388
+ lastPongMs: state.lastPongMs,
389
+ timeoutMs: state.timeoutMs,
390
+ });
391
+ } catch (_e) { /* obs best-effort */ }
392
+ }
393
+
394
+ function _emitAuditPassiveTimeout(state) {
395
+ var sink;
396
+ try { sink = audit(); } catch (_e) { return; }
397
+ if (!sink || typeof sink.safeEmit !== "function") return;
398
+ try {
399
+ sink.safeEmit({
400
+ action: "networkheartbeat.passive.timeout",
401
+ outcome: "failure",
402
+ metadata: {
403
+ pongCount: state.pongCount,
404
+ lastPongMs: state.lastPongMs,
405
+ timeoutMs: state.timeoutMs,
406
+ startMs: state.startMs,
407
+ },
408
+ });
409
+ } catch (_e) { /* audit best-effort — never break the caller */ }
410
+ }
411
+
278
412
  function _resetForTest() {
279
413
  stopAll();
280
414
  }
@@ -285,6 +419,7 @@ module.exports = {
285
419
  stopAll: stopAll,
286
420
  status: status,
287
421
  statuses: statuses,
422
+ passive: passive,
288
423
  HeartbeatError: HeartbeatError,
289
424
  _resetForTest: _resetForTest,
290
425
  };