@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/testing.js CHANGED
@@ -1,62 +1,45 @@
1
1
  "use strict";
2
2
  /**
3
- * b.testing — operator-facing test helpers.
3
+ * @module b.testing
4
+ * @featured true
5
+ * @nav Observability
6
+ * @title Testing
4
7
  *
5
- * Each helper threads through an existing framework primitive rather
6
- * than rolling its own timer races or polling loops.
8
+ * @intro
9
+ * Operator-facing test helpers. Every helper threads through an
10
+ * existing framework primitive rather than rolling its own timer
11
+ * races or polling loops, so test code exercises the same code
12
+ * paths production traffic does.
7
13
  *
8
- * var t = b.testing;
9
- * var req = t.mockReq({ url: "/users/42", method: "GET" });
10
- * var res = t.mockRes();
11
- * await myHandler(req, res, function () {});
14
+ * The surface covers four concerns: HTTP request/response fixture
15
+ * builders (`mockReq` / `mockRes` / `bodyReq` / `bodyRes` /
16
+ * `streamingRes`), controllable time / network / fs fakes
17
+ * (`fakeClock` / `fakeHttpClient` / `tempDir` / `listenOnRandomPort`
18
+ * / `makeFakeOtelApi`), capturing taps for the framework's emit
19
+ * surfaces (`captureAudit` / `captureObservability` /
20
+ * `captureMetricsTap`), and async test helpers including a
21
+ * supertest-style chainable HTTP runner (`runMiddleware` / `waitFor`
22
+ * / `request`).
12
23
  *
13
- * var clk = t.fakeClock(1_000_000);
14
- * var cache = b.cache.create({ namespace: "x", clock: clk.now });
15
- * clk.advance(C.TIME.minutes(5)); // jump forward
24
+ * Primitive-mapping for the threaded-through helpers:
16
25
  *
17
- * var hc = t.fakeHttpClient(function (req) {
18
- * return { statusCode: 200, body: Buffer.from("ok") };
19
- * });
20
- * // ...inject hc as the operator's b.httpClient stand-in
21
- *
22
- * var audit = t.captureAudit();
23
- * var notify = b.notify.create({ channels: { ... }, audit: audit });
24
- * await notify.send({ ... });
25
- * assert(audit.byAction("notify.send.success").length === 1);
26
- *
27
- * var captured = await t.runMiddleware(myMiddleware, req, res);
28
- * assert(captured.nextCalled);
26
+ * - waitFor poll loop → b.safeAsync.sleep
27
+ * - waitFor / runMiddleware overall cap → b.safeAsync.withTimeout
28
+ * - mockReq actor shape → compatible with b.requestHelpers.extractActorContext
29
+ * - captureObservability matches b.observability.tap + .event contracts
30
+ * - captureAudit → matches b.audit.safeEmit (drop-silent)
31
+ * - fakeHttpClient → matches b.httpClient.request response shape
32
+ * - tempDir path safety → mirrors lib/static.js _resolveSafe containment check
33
+ * - TestingError → b.frameworkError.defineClass(...{ alwaysPermanent: true })
29
34
  *
30
- * await t.waitFor(function () { return jobsTable.count() > 0; });
35
+ * What is intentionally NOT here: assertion library / test runner,
36
+ * DB transaction-rollback wrapper (`b.db.transaction` already
37
+ * exists), snapshot or property-based testing helpers (operator
38
+ * brings their own), and framework-internal fixtures that boot
39
+ * `b.db` with vault (those stay in `test/helpers/db.js`).
31
40
  *
32
- * var dir = t.tempDir("my-fixture");
33
- * try {
34
- * fs.writeFileSync(path.join(dir.path, "fixture.json"), "...");
35
- * } finally { dir.cleanup(); }
36
- *
37
- * Primitive-mapping:
38
- *
39
- * - waitFor poll loop → b.safeAsync.sleep
40
- * - waitFor / runMiddleware overall cap → b.safeAsync.withTimeout
41
- * - mockReq actor shape → compatible with b.requestHelpers.extractActorContext
42
- * - captureObservability → matches b.observability.tap + .event contracts
43
- * - captureAudit → matches b.audit.safeEmit (drop-silent)
44
- * - fakeHttpClient → matches b.httpClient.request response shape
45
- * - tempDir path safety → mirrors lib/static.js _resolveSafe containment check
46
- * - TestingError → b.frameworkError.defineClass(...{ alwaysPermanent: true })
47
- * - lazy require → b.lazyRequire (avoids load-order cycles with safe-async)
48
- *
49
- * What is intentionally NOT here:
50
- *
51
- * - Assertion library / test runner / mocking lib (Node's `assert`,
52
- * `node:test`, vitest, jest, etc. — operator's choice)
53
- * - Supertest-style request builder (operators use native http.request
54
- * or bring real supertest)
55
- * - DB transaction-rollback wrapper (b.db.transaction already exists)
56
- * - Snapshot testing / property-based testing helpers (operator brings
57
- * their own snapshotter / fast-check)
58
- * - Built-in fixtures that boot b.db with vault — that's framework-test-
59
- * specific and stays in test/helpers/db.js
41
+ * @card
42
+ * Operator-facing test helpers.
60
43
  */
61
44
 
62
45
  var fs = require("node:fs");
@@ -98,6 +81,40 @@ var _isFiniteNonNegative = numericChecks.isFiniteNonNegative;
98
81
  // These mimic Node's `http` module's request/response shapes. They're
99
82
  // the surface every framework primitive's middleware tests use.
100
83
 
84
+ /**
85
+ * @primitive b.testing.mockReq
86
+ * @signature b.testing.mockReq(opts)
87
+ * @since 0.1.0
88
+ * @status stable
89
+ * @related b.testing.mockRes, b.testing.bodyReq, b.testing.runMiddleware
90
+ *
91
+ * Build a plain-object request fixture that satisfies every field
92
+ * `b.requestHelpers.extractActorContext` reads (`headers`, `socket`,
93
+ * `connection`, `method`, `url`). Sensible defaults for every field
94
+ * so passing `{}` produces a complete, self-consistent request.
95
+ *
96
+ * @opts
97
+ * method: string, // HTTP method; defaults to "GET"
98
+ * url: string, // request-target; defaults to "/"
99
+ * pathname: string, // optional override; defaults to URL pre-`?`
100
+ * headers: object, // header map (lower-cased on read)
101
+ * userAgent: string, // shorthand for headers["user-agent"]
102
+ * requestId: string, // shorthand for headers["x-request-id"]
103
+ * ip: string, // socket/connection.remoteAddress
104
+ * socket: object, // explicit socket override
105
+ * connection: object, // explicit connection override
106
+ *
107
+ * @example
108
+ * var req = b.testing.mockReq({
109
+ * method: "POST",
110
+ * url: "/users/42",
111
+ * userAgent: "test-agent/1.0",
112
+ * ip: "10.0.0.1",
113
+ * });
114
+ * req.method; // → "POST"
115
+ * req.headers["user-agent"]; // → "test-agent/1.0"
116
+ * req.socket.remoteAddress; // → "10.0.0.1"
117
+ */
101
118
  function mockReq(opts) {
102
119
  opts = opts || {};
103
120
  var headers = Object.assign({}, opts.headers || {});
@@ -119,6 +136,29 @@ function mockReq(opts) {
119
136
  };
120
137
  }
121
138
 
139
+ /**
140
+ * @primitive b.testing.mockRes
141
+ * @signature b.testing.mockRes()
142
+ * @since 0.1.0
143
+ * @status stable
144
+ * @related b.testing.mockReq, b.testing.streamingRes, b.testing.runMiddleware
145
+ *
146
+ * Build a buffered response fixture that captures `setHeader`,
147
+ * `writeHead`, and `end` calls. The hidden `_captured()` accessor
148
+ * returns `{ status, headers, body, ended }` for assertions. Use this
149
+ * when the middleware under test writes a single response body — for
150
+ * streaming responses, use `streamingRes()` instead.
151
+ *
152
+ * @example
153
+ * var res = b.testing.mockRes();
154
+ * res.writeHead(200, { "Content-Type": "text/plain" });
155
+ * res.end("hello");
156
+ * var captured = res._captured();
157
+ * captured.status; // → 200
158
+ * captured.headers["content-type"]; // → "text/plain"
159
+ * captured.body; // → "hello"
160
+ * captured.ended; // → true
161
+ */
122
162
  function mockRes() {
123
163
  var headers = {};
124
164
  var statusCode = null;
@@ -144,6 +184,27 @@ function mockRes() {
144
184
  };
145
185
  }
146
186
 
187
+ /**
188
+ * @primitive b.testing.bodyReq
189
+ * @signature b.testing.bodyReq(method, headers, body)
190
+ * @since 0.1.0
191
+ * @status stable
192
+ * @related b.testing.mockReq, b.testing.bodyRes
193
+ *
194
+ * Build an EventEmitter-backed request that emits a single `data`
195
+ * chunk and an `end` event on the next tick. Use this for body-parser
196
+ * / form / file-upload middleware tests where the consumer reads via
197
+ * `req.on("data", ...)` and `req.on("end", ...)`.
198
+ *
199
+ * @example
200
+ * var req = b.testing.bodyReq("POST", { "content-type": "application/json" }, '{"a":1}');
201
+ * var chunks = [];
202
+ * req.on("data", function (c) { chunks.push(c); });
203
+ * req.on("end", function () {
204
+ * var body = Buffer.concat(chunks).toString("utf8");
205
+ * body; // → '{"a":1}'
206
+ * });
207
+ */
147
208
  function bodyReq(method, headers, body) {
148
209
  var req = new EventEmitter();
149
210
  req.method = method || "GET";
@@ -159,6 +220,27 @@ function bodyReq(method, headers, body) {
159
220
  return req;
160
221
  }
161
222
 
223
+ /**
224
+ * @primitive b.testing.bodyRes
225
+ * @signature b.testing.bodyRes()
226
+ * @since 0.1.0
227
+ * @status stable
228
+ * @related b.testing.bodyReq, b.testing.mockRes, b.testing.streamingRes
229
+ *
230
+ * Build an EventEmitter-backed response that captures the `end()`
231
+ * payload onto `res._captured` (string-concatenated) and emits a
232
+ * `finish` event when ended. Use this paired with `bodyReq` for body-
233
+ * parser middleware tests that need to await the `finish` lifecycle.
234
+ *
235
+ * @example
236
+ * var res = b.testing.bodyRes();
237
+ * res.on("finish", function () {
238
+ * res.statusCode; // → 201
239
+ * res._captured; // → "ok"
240
+ * });
241
+ * res.writeHead(201, { "content-type": "text/plain" });
242
+ * res.end("ok");
243
+ */
162
244
  function bodyRes() {
163
245
  var res = new EventEmitter();
164
246
  res.statusCode = null;
@@ -176,6 +258,28 @@ function bodyRes() {
176
258
  return res;
177
259
  }
178
260
 
261
+ /**
262
+ * @primitive b.testing.streamingRes
263
+ * @signature b.testing.streamingRes()
264
+ * @since 0.1.0
265
+ * @status stable
266
+ * @related b.testing.mockRes, b.testing.bodyRes
267
+ *
268
+ * Build an EventEmitter-backed response that buffers every `write()`
269
+ * chunk into `res._chunks` and exposes `res._captured()` as a single
270
+ * `Buffer` of the full payload. Use this for middleware that streams
271
+ * via repeated `res.write(chunk)` calls (gzip, server-sent events,
272
+ * NDJSON producers, range responses).
273
+ *
274
+ * @example
275
+ * var res = b.testing.streamingRes();
276
+ * res.writeHead(200, { "content-type": "application/octet-stream" });
277
+ * res.write(Buffer.from("hel"));
278
+ * res.write("lo");
279
+ * res.end();
280
+ * res._captured().toString("utf8"); // → "hello"
281
+ * res._statusCode; // → 200
282
+ */
179
283
  function streamingRes() {
180
284
  var res = new EventEmitter();
181
285
  res._chunks = [];
@@ -219,6 +323,28 @@ function streamingRes() {
219
323
  // can pass `clk.now` as the `clock` opt to b.cache / b.api-key /
220
324
  // b.permissions / b.scheduler / b.seeders / etc.
221
325
 
326
+ /**
327
+ * @primitive b.testing.fakeClock
328
+ * @signature b.testing.fakeClock(initialMs)
329
+ * @since 0.1.0
330
+ * @status stable
331
+ * @related b.testing.runMiddleware, b.testing.waitFor
332
+ *
333
+ * Build a controllable clock whose `.now` method is suitable as the
334
+ * `clock` opt on every framework primitive that takes one
335
+ * (`b.cache.create`, `b.apiKey.*`, `b.permissions`, `b.scheduler`,
336
+ * `b.seeders`, `b.session.*`, …). `initialMs` defaults to `1_000_000`.
337
+ * `advance(ms)` jumps forward, `set(ms)` jumps to an absolute instant,
338
+ * and the `ms` getter reads the current value without invoking `.now`.
339
+ *
340
+ * @example
341
+ * var clk = b.testing.fakeClock(1700000000000);
342
+ * clk.now(); // → 1700000000000
343
+ * clk.advance(60000);
344
+ * clk.now(); // → 1700000060000
345
+ * clk.set(1800000000000);
346
+ * clk.ms; // → 1800000000000
347
+ */
222
348
  function fakeClock(initialMs) {
223
349
  if (initialMs === undefined) initialMs = 1_000_000; // arbitrary positive default
224
350
  if (typeof initialMs !== "number" || !isFinite(initialMs)) {
@@ -254,6 +380,27 @@ function fakeClock(initialMs) {
254
380
  // canned response object — sync OR async. `calls` is the captured
255
381
  // request array.
256
382
 
383
+ /**
384
+ * @primitive b.testing.fakeHttpClient
385
+ * @signature b.testing.fakeHttpClient(responder)
386
+ * @since 0.1.0
387
+ * @status stable
388
+ * @related b.testing.captureAudit, b.testing.captureObservability
389
+ *
390
+ * Drop-in stand-in for `b.httpClient`. The `responder(req)` callback
391
+ * receives the request object every call site passes to `.request`
392
+ * and returns the canned response (sync or via a Promise). Every
393
+ * outbound request is recorded on `.calls` for later assertion.
394
+ *
395
+ * @example
396
+ * var hc = b.testing.fakeHttpClient(function (req) {
397
+ * return { statusCode: 200, body: Buffer.from('{"ok":true}') };
398
+ * });
399
+ * var res = await hc.request({ method: "GET", url: "https://api.example.com/health" });
400
+ * res.statusCode; // → 200
401
+ * hc.calls.length; // → 1
402
+ * hc.calls[0].url; // → "https://api.example.com/health"
403
+ */
257
404
  function fakeHttpClient(responder) {
258
405
  if (typeof responder !== "function") {
259
406
  throw _err("BAD_INPUT", "fakeHttpClient: responder must be a function");
@@ -273,6 +420,28 @@ function fakeHttpClient(responder) {
273
420
  // Matches b.audit.safeEmit's drop-silent contract. captured holds
274
421
  // every event pushed; clear() empties; byAction(name) filters.
275
422
 
423
+ /**
424
+ * @primitive b.testing.captureAudit
425
+ * @signature b.testing.captureAudit()
426
+ * @since 0.1.0
427
+ * @status stable
428
+ * @related b.testing.captureObservability, b.testing.captureMetricsTap
429
+ *
430
+ * Build a capturing stand-in for `b.audit` that satisfies the
431
+ * `safeEmit(event)` contract every framework primitive uses. Pass the
432
+ * returned object as the `audit` opt; events flow into `.captured`,
433
+ * `.clear()` empties the buffer, and `.byAction(name)` filters by
434
+ * the `action` field (the convention every framework emit uses).
435
+ *
436
+ * @example
437
+ * var audit = b.testing.captureAudit();
438
+ * audit.safeEmit({ action: "notify.send.success", actor: "alice" });
439
+ * audit.safeEmit({ action: "notify.send.failure", actor: "alice" });
440
+ * audit.captured.length; // → 2
441
+ * audit.byAction("notify.send.success").length; // → 1
442
+ * audit.clear();
443
+ * audit.captured.length; // → 0
444
+ */
276
445
  function captureAudit() {
277
446
  var captured = [];
278
447
  return {
@@ -298,6 +467,29 @@ function captureAudit() {
298
467
  // active under capture) and returns fn's return value, sync OR promise.
299
468
  // Both sides are captured.
300
469
 
470
+ /**
471
+ * @primitive b.testing.captureObservability
472
+ * @signature b.testing.captureObservability()
473
+ * @since 0.1.0
474
+ * @status stable
475
+ * @related b.testing.captureAudit, b.testing.captureMetricsTap
476
+ *
477
+ * Build a capturing stand-in for `b.observability`. The returned
478
+ * object exposes `event(name, value, labels)` and `tap(name, attrs,
479
+ * fn)` matching the framework's contracts; each call appends an
480
+ * entry to `.captured` (`{ kind: "event" | "tap" | "tap.end" |
481
+ * "tap.error", … }`). `tap` runs `fn(null)` (no tracer active under
482
+ * capture) and forwards both sync return values and promise
483
+ * resolutions/rejections.
484
+ *
485
+ * @example
486
+ * var obs = b.testing.captureObservability();
487
+ * obs.event("cache.hit", 1, { ns: "users" });
488
+ * var ret = obs.tap("widgets.load", { id: 42 }, function () { return "ok"; });
489
+ * ret; // → "ok"
490
+ * obs.captured.length; // → 3
491
+ * obs.byName("cache.hit").length; // → 1
492
+ */
301
493
  function captureObservability() {
302
494
  var captured = [];
303
495
  function event(name, value, labels) {
@@ -347,6 +539,29 @@ function captureObservability() {
347
539
  // observability.test.js, …) — codified here so the swap+restore
348
540
  // sequence is no longer copy-pasted.
349
541
 
542
+ /**
543
+ * @primitive b.testing.captureMetricsTap
544
+ * @signature b.testing.captureMetricsTap()
545
+ * @since 0.1.0
546
+ * @status stable
547
+ * @related b.testing.captureAudit, b.testing.captureObservability
548
+ *
549
+ * Swap `b.metrics.tap` with a capturing function and return a handle
550
+ * with `.captured`, `.byName(name)`, `.clear()`, and crucially
551
+ * `.restore()`. The operator MUST call `.restore()` in a `finally`
552
+ * to revert — failure to restore leaks state across tests.
553
+ *
554
+ * @example
555
+ * var taps = b.testing.captureMetricsTap();
556
+ * try {
557
+ * b.metrics.tap("widgets.created", 1, { kind: "alpha" });
558
+ * b.metrics.tap("widgets.created", 1, { kind: "beta" });
559
+ * taps.captured.length; // → 2
560
+ * taps.byName("widgets.created").length; // → 2
561
+ * } finally {
562
+ * taps.restore();
563
+ * }
564
+ */
350
565
  function captureMetricsTap() {
351
566
  var m = metricsModule();
352
567
  var original = m.tap;
@@ -370,6 +585,42 @@ function captureMetricsTap() {
370
585
  // completion. b.safeAsync.withTimeout caps the wait so a buggy
371
586
  // middleware that never settles fails the test fast.
372
587
 
588
+ /**
589
+ * @primitive b.testing.runMiddleware
590
+ * @signature b.testing.runMiddleware(middleware, req, res, opts)
591
+ * @since 0.1.0
592
+ * @status stable
593
+ * @related b.testing.mockReq, b.testing.mockRes, b.testing.waitFor
594
+ *
595
+ * Drive a 3-arg `(req, res, next)` middleware to either `next()` OR
596
+ * `res.end()` completion and return `{ nextCalled, nextError, req,
597
+ * res, ended }`. Sync throws and rejected-promise returns are mapped
598
+ * onto `nextError`. `b.safeAsync.withTimeout` caps the wait so a
599
+ * middleware that never settles fails the test fast instead of
600
+ * hanging. `req` / `res` default to fresh `mockReq()` / `mockRes()`.
601
+ *
602
+ * @opts
603
+ * timeoutMs: number, // overall cap; defaults to 5000 (0 disables)
604
+ *
605
+ * @example
606
+ * var auth = function (req, res, next) {
607
+ * if (!req.headers.authorization) {
608
+ * res.writeHead(401);
609
+ * res.end("unauthorized");
610
+ * return;
611
+ * }
612
+ * next();
613
+ * };
614
+ * var captured = await b.testing.runMiddleware(
615
+ * auth,
616
+ * b.testing.mockReq({ url: "/secret" }),
617
+ * b.testing.mockRes(),
618
+ * { timeoutMs: 1000 }
619
+ * );
620
+ * captured.nextCalled; // → false
621
+ * captured.ended; // → true
622
+ * captured.res._captured().status; // → 401
623
+ */
373
624
  async function runMiddleware(middleware, req, res, opts) {
374
625
  if (typeof middleware !== "function") {
375
626
  throw _err("BAD_INPUT", "runMiddleware: middleware must be a 3-arg (req, res, next) function");
@@ -432,6 +683,34 @@ async function runMiddleware(middleware, req, res, opts) {
432
683
  // the poll interval (NOT raw setTimeout) and b.safeAsync.withTimeout
433
684
  // for the overall cap. Operator can pass `{ signal }` to abort.
434
685
 
686
+ /**
687
+ * @primitive b.testing.waitFor
688
+ * @signature b.testing.waitFor(predicate, opts)
689
+ * @since 0.1.0
690
+ * @status stable
691
+ * @related b.testing.runMiddleware, b.testing.fakeClock
692
+ *
693
+ * Poll `predicate()` (sync or async) until it returns truthy or
694
+ * `timeoutMs` elapses. The poll loop uses `b.safeAsync.sleep` (NOT
695
+ * raw `setTimeout`) so timer cleanup is uniform with the rest of the
696
+ * framework, and `b.safeAsync.withTimeout` enforces the overall cap.
697
+ * Pass `opts.signal` to abort early (a cancelled `AbortController`
698
+ * resolves the loop without throwing).
699
+ *
700
+ * @opts
701
+ * timeoutMs: number, // overall cap; defaults to 1000
702
+ * intervalMs: number, // poll interval; defaults to 10
703
+ * signal: AbortSignal, // cooperative cancellation
704
+ *
705
+ * @example
706
+ * var jobs = [];
707
+ * setTimeout(function () { jobs.push({ id: 1 }); }, 30);
708
+ * var result = await b.testing.waitFor(
709
+ * function () { return jobs.length > 0 ? jobs[0] : false; },
710
+ * { timeoutMs: 500, intervalMs: 5 }
711
+ * );
712
+ * result.id; // → 1
713
+ */
435
714
  async function waitFor(predicate, opts) {
436
715
  if (typeof predicate !== "function") {
437
716
  throw _err("BAD_INPUT", "waitFor: predicate must be a function returning truthy/falsy or Promise<bool>");
@@ -460,7 +739,7 @@ async function waitFor(predicate, opts) {
460
739
  // semantics apply uniformly. The internal signal aborts the inner
461
740
  // sleep when withTimeout rejects.
462
741
  var loop = (async function () {
463
-
742
+
464
743
  while (!ac.signal.aborted) {
465
744
  var v = await predicate();
466
745
  if (v) return v;
@@ -468,7 +747,7 @@ async function waitFor(predicate, opts) {
468
747
  catch (_e) { return undefined; } // sleep aborted = loop exits cleanly
469
748
  }
470
749
  return undefined;
471
-
750
+
472
751
  })();
473
752
 
474
753
  try {
@@ -496,6 +775,31 @@ async function waitFor(predicate, opts) {
496
775
  // prefix (mirrors lib/static.js _resolveSafe containment). Cleanup is
497
776
  // idempotent.
498
777
 
778
+ /**
779
+ * @primitive b.testing.tempDir
780
+ * @signature b.testing.tempDir(prefix)
781
+ * @since 0.1.0
782
+ * @status stable
783
+ * @related b.testing.listenOnRandomPort
784
+ *
785
+ * Create an `os.tmpdir()`-rooted directory and return `{ path,
786
+ * cleanup }`. `prefix` must be an identifier-like string (no `..`,
787
+ * `/`, `\`, or null bytes); a containment check mirroring
788
+ * `lib/static.js _resolveSafe` verifies the resolved path stays
789
+ * inside `os.tmpdir()` before any file is written. `cleanup()` is
790
+ * idempotent and best-effort on Windows-locked files.
791
+ *
792
+ * @example
793
+ * var dir = b.testing.tempDir("my-fixture");
794
+ * try {
795
+ * var nodeFs = require("node:fs");
796
+ * var nodePath2 = require("node:path");
797
+ * nodeFs.writeFileSync(nodePath2.join(dir.path, "fixture.json"), '{"ok":1}');
798
+ * dir.path.indexOf("my-fixture-") !== -1; // → true
799
+ * } finally {
800
+ * dir.cleanup();
801
+ * }
802
+ */
499
803
  function tempDir(prefix) {
500
804
  if (prefix === undefined || prefix === null) prefix = "blamejs-test";
501
805
  if (typeof prefix !== "string" || prefix.length === 0) {
@@ -534,6 +838,30 @@ function tempDir(prefix) {
534
838
  // Standalone — Node http.Server only, no framework primitive overlap.
535
839
  // The helper IS the boilerplate.
536
840
 
841
+ /**
842
+ * @primitive b.testing.listenOnRandomPort
843
+ * @signature b.testing.listenOnRandomPort(server, host)
844
+ * @since 0.1.0
845
+ * @status stable
846
+ * @related b.testing.request, b.testing.tempDir
847
+ *
848
+ * Bind a Node `http.Server` to an OS-assigned ephemeral port on the
849
+ * loopback interface (`host` defaults to `"127.0.0.1"`) and resolve
850
+ * with the chosen port number. Use this when a test needs a real
851
+ * listening server but doesn't want to hard-code a port. For
852
+ * supertest-style routing, prefer `b.testing.request` which manages
853
+ * the listen/close lifecycle automatically.
854
+ *
855
+ * @example
856
+ * var nodeHttp = require("node:http");
857
+ * var server = nodeHttp.createServer(function (req, res) {
858
+ * res.writeHead(200);
859
+ * res.end("ok");
860
+ * });
861
+ * var port = await b.testing.listenOnRandomPort(server);
862
+ * typeof port === "number" && port > 0; // → true
863
+ * server.close();
864
+ */
537
865
  function listenOnRandomPort(server, host) {
538
866
  host = host || "127.0.0.1";
539
867
  return new Promise(function (resolve, reject) {
@@ -569,6 +897,40 @@ function listenOnRandomPort(server, host) {
569
897
  // the request flows through the full Node http stack — same code path
570
898
  // production traffic takes. Server is closed automatically when the
571
899
  // promise resolves or rejects.
900
+
901
+ /**
902
+ * @primitive b.testing.request
903
+ * @signature b.testing.request(target)
904
+ * @since 0.1.0
905
+ * @status stable
906
+ * @related b.testing.runMiddleware, b.testing.listenOnRandomPort
907
+ *
908
+ * Supertest-style chainable HTTP test runner. `target` may be a
909
+ * `b.router` instance (uses `.handle(req, res)`), a `(req, res) =>
910
+ * void` listener function, or an existing `http.Server` /
911
+ * `https.Server`. The runner spins up a real ephemeral-port
912
+ * `http.Server` so the request flows through the full Node HTTP
913
+ * stack — the same code path production traffic takes — and tears it
914
+ * down when the awaited chain resolves or rejects.
915
+ *
916
+ * Each verb (`get` / `post` / `put` / `patch` / `delete` / `head` /
917
+ * `options`) returns a chain with `.set(k, v)` / `.set(obj)`,
918
+ * `.send(body)` (Buffer / string / JSON-serialized object), and
919
+ * `.expect(statusOrFn)`. Awaiting the chain resolves to `{ status,
920
+ * headers, body, text, json }`.
921
+ *
922
+ * @example
923
+ * var listener = function (req, res) {
924
+ * res.writeHead(200, { "content-type": "application/json" });
925
+ * res.end('{"ok":true}');
926
+ * };
927
+ * var res = await b.testing.request(listener)
928
+ * .get("/health")
929
+ * .set("X-Request-Id", "abc")
930
+ * .expect(200);
931
+ * res.status; // → 200
932
+ * res.json.ok; // → true
933
+ */
572
934
  function request(target) {
573
935
  // Resolve target → request listener
574
936
  var server;
@@ -719,6 +1081,31 @@ function request(target) {
719
1081
  };
720
1082
  }
721
1083
 
1084
+ /**
1085
+ * @primitive b.testing.makeFakeOtelApi
1086
+ * @signature b.testing.makeFakeOtelApi()
1087
+ * @since 0.1.0
1088
+ * @status stable
1089
+ * @related b.testing.captureObservability
1090
+ *
1091
+ * Build a minimal fake of `@opentelemetry/api` covering exactly the
1092
+ * subset `b.tracing` consumes (`trace.getTracer`, `trace.setSpan`,
1093
+ * `trace.getActiveSpan`, `context.active`, `context.with`, plus a
1094
+ * `SpanKind` enum). Each started span records attributes, events,
1095
+ * exceptions, status, and end into `_spans` for assertion. Operators
1096
+ * inject this where `b.tracing` would normally be wired to the real
1097
+ * OTel API.
1098
+ *
1099
+ * @example
1100
+ * var fake = b.testing.makeFakeOtelApi();
1101
+ * var tracer = fake.trace.getTracer("test");
1102
+ * var span = tracer.startSpan("widgets.load", { attributes: { id: 42 } });
1103
+ * span.setAttribute("status", "ok");
1104
+ * span.end();
1105
+ * fake._spans.length; // → 1
1106
+ * fake._spans[0]._attrs.id; // → 42
1107
+ * fake._spans[0]._ended; // → true
1108
+ */
722
1109
  function makeFakeOtelApi() {
723
1110
  var spans = [];
724
1111
  var activeSpan = null;