@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/websocket.js CHANGED
@@ -1,84 +1,77 @@
1
1
  "use strict";
2
2
  /**
3
- * WebSocket server primitive — RFC 6455.
3
+ * @module b.websocket
4
+ * @nav HTTP
5
+ * @title Websocket
4
6
  *
5
- * Implements the server side of the WebSocket protocol on top of the
6
- * Node HTTP server's `'upgrade'` event. Built on node:net + node:crypto
7
- * with no npm runtime dep.
7
+ * @intro
8
+ * RFC 6455 WebSocket server on top of Node's `'upgrade'` event, plus
9
+ * RFC 8441 Extended CONNECT for HTTP/2. Built on `node:net` +
10
+ * `node:crypto` + `node:zlib` with no npm runtime dep.
8
11
  *
9
- * Surface:
12
+ * Three layers exposed to operators:
10
13
  *
11
- * websocket.handleUpgrade(req, socket, head, opts)
12
- * Wraps a TCP socket post-HTTP-upgrade. Validates the handshake,
13
- * enforces origin policy, negotiates subprotocol, sends 101
14
- * response, returns a WebSocketConnection. Throws / refuses on
15
- * bad handshake.
14
+ * 1. Handshake — `handleUpgrade(req, socket, head, opts)` for h1
15
+ * and `handleExtendedConnect(stream, headers, opts)` for h2.
16
+ * Validate the request, enforce same-origin (default) or an
17
+ * operator-supplied allowlist, negotiate subprotocol +
18
+ * permessage-deflate, return a `WebSocketConnection`. Refuse
19
+ * credential-shaped query parameters (`access_token`, `apikey`,
20
+ * `authorization`, …) — query strings leak via access logs,
21
+ * Referer headers, and browser history.
16
22
  *
17
- * new websocket.WebSocketConnection(socket, opts)
18
- * EventEmitter wrapping a post-upgrade socket. State machine
19
- * mirrors the browser WebSocket API:
20
- * conn.readyState 'open' | 'closing' | 'closed'
21
- * conn.lastError last diagnosable error, if any
22
- * conn.send(data) — Buffer or string. Routes to binary
23
- * or text frame. Throws if not OPEN.
24
- * conn.ping(payload?) — Send ping frame (no-op if not OPEN).
25
- * conn.close(code?, reason?) — Send close frame, wait
26
- * closeGraceMs for peer's echo, end
27
- * socket.
28
- * Events:
29
- * 'message' (data, isBinary)
30
- * 'ping' (payload)
31
- * 'pong' (payload)
32
- * 'close' (code, reason, wasClean) — fires exactly once at
33
- * lifecycle end. wasClean: true when
34
- * the close handshake completed in
35
- * both directions; false on socket
36
- * errors / abnormal closure (code
37
- * 1006) / heartbeat timeout / etc.
38
- * Operators usually only need this
39
- * listener for full lifecycle tracking.
40
- * 'error' (err) — diagnosable issue. Always followed
41
- * by 'close'. Optional listener;
42
- * missing listener does NOT crash the
43
- * process (gated by listenerCount).
23
+ * 2. Connection — `WebSocketConnection` is an EventEmitter
24
+ * mirroring the browser API. Read `conn.readyState`
25
+ * (`'open' | 'closing' | 'closed'`); call `conn.send(data)`,
26
+ * `conn.ping(payload?)`, `conn.close(code?, reason?)`. Listen
27
+ * on `'message'`, `'ping'`, `'pong'`, `'close'`, `'error'`.
44
28
  *
45
- * websocket.serializeFrame(opcode, payload, opts), websocket.FrameParser
46
- * Lower-level helpers exposed for tests + advanced callers.
29
+ * 3. Frame layer — `FrameParser` + `serializeFrame` exposed for
30
+ * tests and advanced callers (custom proxy / multiplexer use
31
+ * cases). Operator code rarely touches these directly.
47
32
  *
48
- * Spec compliance notes (the parts where naive impls get it wrong):
33
+ * Defenses wired in by default:
49
34
  *
50
- * 1. Mask handling (§5.3). All client→server frames MUST be masked.
51
- * Unmasked client frames close the connection with code 1002.
52
- * Server→client frames MUST NOT be masked. The serializer here
53
- * defaults mask:false (server side); a `mask:true` opt exists
54
- * for completeness / test fixtures only.
35
+ * - Same-origin Origin check on browser-initiated upgrades. Cross-
36
+ * site WebSocket hijacking (CSWSH) requires explicit opt-in via
37
+ * `origins: [...allowlist]` or `origins: "*"`.
38
+ * - Control-frame payload cap of 125 bytes (RFC 6455 §5.5).
39
+ * Without it, a 1 MiB PING echoes back as a 1 MiB PONG — a 2x
40
+ * outbound-bandwidth amplification DoS.
41
+ * - Strict UTF-8 validation on TEXT frames + close reasons (§5.6).
42
+ * - Close-code allowlist per §7.4.2 (1000–1011, 3000–4999;
43
+ * reserved 1004/1005/1006/1015 refused on the wire).
44
+ * - Frame + message length capped at `maxMessageBytes`
45
+ * (default 1 MiB).
46
+ * - Heartbeat: ping every 30s, abort after 35s without pong.
47
+ * - Cluster fan-out lives at the router/channel layer above this
48
+ * module; this primitive owns the per-connection protocol.
55
49
  *
56
- * 2. SHA-1 for Sec-WebSocket-Accept. RFC 6455 §1.3 mandates
57
- * SHA-1(key + GUID). The framework uses SHA3-512 elsewhere; SHA-1
58
- * here is NOT a security primitive the GUID is publicly known
59
- * and the hash is a protocol marker confirming both sides agree
60
- * on the upgrade. Nothing about the WebSocket connection's
61
- * security depends on SHA-1 collision resistance.
50
+ * Configurable handshake GUID: closed-ecosystem clients with a
51
+ * custom magic string pass `opts.handshakeGuid` (UUID-shaped). The
52
+ * default is the RFC 6455 §1.3 value so RFC-compliant clients
53
+ * interoperate out of the box. SHA-1 used in `Sec-WebSocket-Accept`
54
+ * is a protocol marker, not a security primitive — its collision
55
+ * resistance is irrelevant to the connection's security.
62
56
  *
63
- * 3. Close handshake reciprocity (§5.5.1). When the peer sends a
64
- * close frame, we MUST echo a close frame back, then close the
65
- * TCP socket. close() handles this; _handleClose echoes if we
66
- * haven't already initiated.
57
+ * Spec compliance notes (where naive implementations get it wrong):
67
58
  *
68
- * 4. Origin policy. Browser clients send `Origin: <scheme>://<host>`.
69
- * The framework matches the CORS module's pattern: if the operator
70
- * passes `origins: [...]`, enforce strictly. If `origins: "*"`,
71
- * accept all (explicit operator opt-in to no checking). If
72
- * `origins` is omitted, accept all but emit an audit warning at
73
- * registration (the safety check) — see lib/router.js where the
74
- * operator-facing API lives. Non-browser clients (Origin header
75
- * absent) bypass origin checks since Origin is a browser-only
76
- * enforcement signal.
59
+ * 1. Mask handling (§5.3). All client→server h1 frames MUST be
60
+ * masked; server→client frames MUST NOT be masked. The h2
61
+ * transport (RFC 8441) flips both: frames MUST NOT be masked
62
+ * because h2 already provides the framing guarantees masking
63
+ * defends.
64
+ * 2. Close handshake reciprocity (§5.5.1). Peer-initiated close
65
+ * echoes a close frame back before ending the TCP socket.
66
+ * 3. Subprotocol negotiation. Server picks the FIRST entry from
67
+ * `Sec-WebSocket-Protocol` that's in the operator's allowlist.
68
+ * If none match, response omits the header (§11.3.4).
69
+ * 4. permessage-deflate (RFC 7692). Negotiated when the client
70
+ * offers it; runs in `no_context_takeover` mode in both
71
+ * directions so each message uses a fresh zlib state.
77
72
  *
78
- * 5. Subprotocol negotiation. Server picks the FIRST entry from
79
- * Sec-WebSocket-Protocol that's in the operator's `subprotocols`
80
- * allowlist. If none match, the response omits the header (per
81
- * §11.3.4) and the client decides whether to proceed.
73
+ * @card
74
+ * RFC 6455 WebSocket server on top of Node's `'upgrade'` event, plus RFC 8441 Extended CONNECT for HTTP/2.
82
75
  */
83
76
 
84
77
  var nodeCrypto = require("crypto");
@@ -208,6 +201,31 @@ var STATE_OPEN = "open";
208
201
  var STATE_CLOSING = "closing"; // we sent a close frame, awaiting peer's echo
209
202
  var STATE_CLOSED = "closed";
210
203
 
204
+ /**
205
+ * @primitive b.websocket.WebSocketError
206
+ * @signature b.websocket.WebSocketError(code, message, closeCode)
207
+ * @since 0.1.38
208
+ * @status stable
209
+ * @related b.websocket.WebSocketConnection
210
+ *
211
+ * Framework-error subclass thrown for protocol violations + invalid
212
+ * caller input (`send()` on a closed connection, malformed frame
213
+ * payload, frame-too-large detected at parse time). Carries the
214
+ * RFC 6455 §7.4.1 `closeCode` the connection layer uses when
215
+ * aborting (1002 protocol error, 1007 invalid payload, 1009 message
216
+ * too big, 1011 internal error). Operators usually catch via the
217
+ * shared `b.errors` surface and never construct one directly.
218
+ *
219
+ * @example
220
+ * try {
221
+ * conn.send("late message");
222
+ * } catch (err) {
223
+ * if (err.isWebSocketError) {
224
+ * console.log(err.code, err.closeCode);
225
+ * // → "ws/closed" 1002
226
+ * }
227
+ * }
228
+ */
211
229
  class WebSocketError extends FrameworkError {
212
230
  constructor(code, message, closeCode) {
213
231
  super(message, code);
@@ -219,6 +237,26 @@ class WebSocketError extends FrameworkError {
219
237
 
220
238
  // ---- Handshake helpers ----
221
239
 
240
+ /**
241
+ * @primitive b.websocket.computeAcceptKey
242
+ * @signature b.websocket.computeAcceptKey(secWebSocketKey, handshakeGuid)
243
+ * @since 0.1.38
244
+ * @status stable
245
+ * @related b.websocket.handleUpgrade, b.websocket.buildUpgradeResponse
246
+ *
247
+ * Compute the `Sec-WebSocket-Accept` value RFC 6455 §1.3 mandates:
248
+ * `base64(SHA1(secWebSocketKey || handshakeGuid))`. The SHA-1 is a
249
+ * protocol marker confirming both ends agree on the upgrade — it is
250
+ * NOT a security primitive, and the framework's other crypto stays
251
+ * SHA3 / SHAKE-based regardless. Pass `handshakeGuid` undefined to
252
+ * use the RFC value (`258EAFA5-E914-47DA-95CA-C5AB0DC85B11`); pass a
253
+ * UUID-shaped override only for closed-ecosystem clients with a
254
+ * matching custom magic string.
255
+ *
256
+ * @example
257
+ * var accept = b.websocket.computeAcceptKey("dGhlIHNhbXBsZSBub25jZQ==");
258
+ * // → "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
259
+ */
222
260
  function computeAcceptKey(secWebSocketKey, handshakeGuid) {
223
261
  // SHA-1 required by RFC 6455 §1.3 — see file-level note 2 above.
224
262
  // This is a protocol marker, not a security primitive.
@@ -229,6 +267,35 @@ function computeAcceptKey(secWebSocketKey, handshakeGuid) {
229
267
  return hash.digest("base64");
230
268
  }
231
269
 
270
+ /**
271
+ * @primitive b.websocket.validateUpgradeRequest
272
+ * @signature b.websocket.validateUpgradeRequest(req, opts)
273
+ * @since 0.1.38
274
+ * @status stable
275
+ * @related b.websocket.handleUpgrade, b.websocket.isOriginAllowed
276
+ *
277
+ * Strict shape check on the HTTP/1.1 upgrade request. Verifies the
278
+ * method is GET, `Upgrade: websocket` and `Connection: upgrade`
279
+ * tokens are present, `Sec-WebSocket-Version: 13`, and
280
+ * `Sec-WebSocket-Key` is a 24-character base64 of 16 random bytes
281
+ * (RFC 6455 §4.1). Refuses credential-shaped query parameters
282
+ * (`access_token`, `apikey`, `authorization`, …) unless the operator
283
+ * passes `opts.allowQueryAuthParams: true` with an audited reason.
284
+ * Returns `{ ok: true }` on success or
285
+ * `{ ok: false, status, reason }` on refusal — never throws, so the
286
+ * caller can write the refusal response and end the socket cleanly.
287
+ *
288
+ * @opts
289
+ * allowQueryAuthParams: boolean, // opt out of credential-query refusal
290
+ *
291
+ * @example
292
+ * var v = b.websocket.validateUpgradeRequest(req, {});
293
+ * if (!v.ok) {
294
+ * // → { ok: false, status: 400, reason: "..." }
295
+ * socket.write("HTTP/1.1 " + v.status + " Bad Request\r\n\r\n");
296
+ * socket.destroy();
297
+ * }
298
+ */
232
299
  function validateUpgradeRequest(req, opts) {
233
300
  if (req.method !== "GET") {
234
301
  return { ok: false, status: HTTP.METHOD_NOT_ALLOWED, reason: "method must be GET" };
@@ -306,6 +373,25 @@ function _findCredentialQueryParam(reqUrl) {
306
373
  return null;
307
374
  }
308
375
 
376
+ /**
377
+ * @primitive b.websocket.negotiateSubprotocol
378
+ * @signature b.websocket.negotiateSubprotocol(req, supported)
379
+ * @since 0.1.38
380
+ * @status stable
381
+ * @related b.websocket.handleUpgrade, b.websocket.buildUpgradeResponse
382
+ *
383
+ * Pick the first client-offered subprotocol that appears in
384
+ * `supported`. Returns the chosen string, or `null` when there is no
385
+ * intersection (per RFC 6455 §11.3.4 the response then omits the
386
+ * `Sec-WebSocket-Protocol` header and the client decides whether to
387
+ * proceed). `supported` falsy or empty is treated as "no preference"
388
+ * and always returns `null`.
389
+ *
390
+ * @example
391
+ * var req = { headers: { "sec-websocket-protocol": "chat.v2, chat.v1" } };
392
+ * var picked = b.websocket.negotiateSubprotocol(req, ["chat.v1"]);
393
+ * // → "chat.v1"
394
+ */
309
395
  function negotiateSubprotocol(req, supported) {
310
396
  if (!supported || supported.length === 0) return null;
311
397
  var raw = (req.headers || {})["sec-websocket-protocol"] || "";
@@ -328,6 +414,33 @@ function negotiateSubprotocol(req, supported) {
328
414
  // needing cross-origin opt in explicitly via
329
415
  // `origins: "*"` (with audited reason) or
330
416
  // `origins: [...allowlist]`.
417
+ /**
418
+ * @primitive b.websocket.isOriginAllowed
419
+ * @signature b.websocket.isOriginAllowed(req, origins)
420
+ * @since 0.1.38
421
+ * @status stable
422
+ * @related b.websocket.handleUpgrade, b.websocket.validateUpgradeRequest
423
+ *
424
+ * Browser-Origin policy gate. Behaviour by the `origins` shape:
425
+ *
426
+ * - Array — strict allowlist; the request's `Origin` header must
427
+ * match one entry exactly.
428
+ * - `"*"` — explicit accept-all (operator opt-in to no checking).
429
+ * - `null` / `undefined` — DEFAULT same-origin: the `Origin` host
430
+ * must match the `Host` header. Closes the cross-site WebSocket
431
+ * hijacking (CSWSH) class on browser-targeted routes.
432
+ *
433
+ * Non-browser clients (curl, server-to-server, native apps) don't
434
+ * send `Origin` and bypass the check — gating those callers is the
435
+ * operator's network-ACL / auth-middleware job, not Origin's.
436
+ *
437
+ * @example
438
+ * var req = { headers: { origin: "https://app.example.com",
439
+ * host: "app.example.com" } };
440
+ * b.websocket.isOriginAllowed(req, undefined); // → true
441
+ * b.websocket.isOriginAllowed(req, ["https://other.example"]); // → false
442
+ * b.websocket.isOriginAllowed(req, "*"); // → true
443
+ */
331
444
  function isOriginAllowed(req, origins) {
332
445
  if (origins === "*") return true;
333
446
  var origin = (req.headers || {}).origin;
@@ -351,6 +464,28 @@ function isOriginAllowed(req, origins) {
351
464
  return false;
352
465
  }
353
466
 
467
+ /**
468
+ * @primitive b.websocket.buildUpgradeResponse
469
+ * @signature b.websocket.buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid)
470
+ * @since 0.1.38
471
+ * @status stable
472
+ * @related b.websocket.handleUpgrade, b.websocket.computeAcceptKey
473
+ *
474
+ * Format the HTTP/1.1 101 Switching Protocols response that completes
475
+ * the WebSocket handshake. Always emits `Upgrade: websocket`,
476
+ * `Connection: Upgrade`, and `Sec-WebSocket-Accept`. Adds
477
+ * `Sec-WebSocket-Protocol` when `subprotocol` is non-null, and
478
+ * `Sec-WebSocket-Extensions` when `extensionHeader` is non-null
479
+ * (e.g. the `permessage-deflate; ...` echo). Pass `handshakeGuid`
480
+ * undefined to use the RFC 6455 default. Returns the raw
481
+ * `\r\n`-delimited response string ready for `socket.write()`.
482
+ *
483
+ * @example
484
+ * var resp = b.websocket.buildUpgradeResponse(
485
+ * "dGhlIHNhbXBsZSBub25jZQ==", "chat.v1", null);
486
+ * // → "HTTP/1.1 101 Switching Protocols\r\n..."
487
+ * socket.write(resp);
488
+ */
354
489
  function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid) {
355
490
  var lines = [
356
491
  "HTTP/1.1 101 Switching Protocols",
@@ -471,6 +606,33 @@ function _inflateMessage(payload, windowBits) {
471
606
  // socket and emits zero-or-more complete frames as they arrive. Holds
472
607
  // partial frame state across calls.
473
608
 
609
+ /**
610
+ * @primitive b.websocket.FrameParser
611
+ * @signature b.websocket.FrameParser(opts)
612
+ * @since 0.1.38
613
+ * @status stable
614
+ * @related b.websocket.serializeFrame, b.websocket.WebSocketConnection
615
+ *
616
+ * Incremental RFC 6455 §5.2 frame parser. `push(chunk)` accepts
617
+ * arbitrary buffer slices straight from the socket and returns zero
618
+ * or more complete frames; partial frame state persists across
619
+ * calls. Each emitted frame is
620
+ * `{ fin, rsv1, rsv2, rsv3, opcode, masked, payload }`. Throws a
621
+ * `WebSocketError` (closeCode = 1009 message-too-big) when a single
622
+ * frame's declared payload length exceeds `opts.maxFrameBytes`
623
+ * (default 1 MiB) — the caller catches it and aborts the connection.
624
+ * The parser does NOT enforce control-frame ≤125-byte caps,
625
+ * mask-direction policy, or RSV-bit-vs-extension consistency; those
626
+ * are the connection layer's job.
627
+ *
628
+ * @opts
629
+ * maxFrameBytes: number, // single-frame payload cap (default 1 MiB)
630
+ *
631
+ * @example
632
+ * var parser = new b.websocket.FrameParser({ maxFrameBytes: 65536 });
633
+ * var frames = parser.push(Buffer.from([0x81, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]));
634
+ * // → [{ fin: true, opcode: 1, payload: <Buffer 68 65 6c 6c 6f>, ... }]
635
+ */
474
636
  function FrameParser(opts) {
475
637
  opts = opts || {};
476
638
  this.maxFrameBytes = opts.maxFrameBytes || DEFAULT_MAX_MESSAGE_BYTES;
@@ -569,6 +731,33 @@ FrameParser.prototype._tryParseFrame = function () {
569
731
 
570
732
  // ---- Frame serializer ----
571
733
 
734
+ /**
735
+ * @primitive b.websocket.serializeFrame
736
+ * @signature b.websocket.serializeFrame(opcode, payload, opts)
737
+ * @since 0.1.38
738
+ * @status stable
739
+ * @related b.websocket.FrameParser, b.websocket.WebSocketConnection
740
+ *
741
+ * Build a single RFC 6455 §5.2 frame. `opcode` is one of
742
+ * `b.websocket.OPCODE_TEXT` / `OPCODE_BINARY` / `OPCODE_CLOSE` /
743
+ * `OPCODE_PING` / `OPCODE_PONG` / `OPCODE_CONTINUATION`. `payload`
744
+ * is a `Buffer` or string (string is UTF-8 encoded). Server-side
745
+ * frames default to unmasked; pass `mask: true` only for
746
+ * client-shaped fixtures or test harnesses. `rsv1: true` marks the
747
+ * first frame of a permessage-deflate-compressed message (RFC 7692).
748
+ * Returns the framed `Buffer`.
749
+ *
750
+ * @opts
751
+ * fin: boolean, // FIN bit, default true (single-frame message)
752
+ * mask: boolean, // mask the payload, default false (server side)
753
+ * rsv1: boolean, // RSV1 bit (permessage-deflate), default false
754
+ *
755
+ * @example
756
+ * var frame = b.websocket.serializeFrame(
757
+ * b.websocket.OPCODE_TEXT, "hello");
758
+ * // → <Buffer 81 05 68 65 6c 6c 6f>
759
+ * socket.write(frame);
760
+ */
572
761
  function serializeFrame(opcode, payload, opts) {
573
762
  opts = opts || {};
574
763
  var fin = opts.fin !== false;
@@ -623,6 +812,58 @@ function serializeFrame(opcode, payload, opts) {
623
812
 
624
813
  // ---- Connection ----
625
814
 
815
+ /**
816
+ * @primitive b.websocket.WebSocketConnection
817
+ * @signature b.websocket.WebSocketConnection(socket, opts)
818
+ * @since 0.1.38
819
+ * @status stable
820
+ * @related b.websocket.handleUpgrade, b.websocket.handleExtendedConnect, b.websocket.WebSocketError
821
+ *
822
+ * EventEmitter wrapping a post-upgrade socket / h2 stream. State
823
+ * machine mirrors the browser WebSocket API:
824
+ *
825
+ * - `conn.readyState` — `'open' | 'closing' | 'closed'`
826
+ * - `conn.send(data)` — `Buffer` or string. Routes to BINARY or
827
+ * TEXT frame; throws `WebSocketError` if not OPEN.
828
+ * - `conn.ping(payload?)` — send PING (no-op if not OPEN).
829
+ * - `conn.close(code?, reason?)` — send CLOSE, wait `closeGraceMs`
830
+ * for the peer's echo, end the underlying socket.
831
+ *
832
+ * Events: `'message' (data, isBinary)`, `'ping' (payload)`,
833
+ * `'pong' (payload)`, `'close' (code, reason, wasClean)` (fires
834
+ * exactly once at lifecycle end), `'error' (err)`.
835
+ *
836
+ * Cluster fan-out / channel broadcast lives at the router layer that
837
+ * owns the connection registry; this primitive owns the per-
838
+ * connection protocol. To broadcast, iterate the operator-side
839
+ * registry and call `send` on each.
840
+ *
841
+ * @opts
842
+ * subprotocol: string, // negotiated value from handleUpgrade
843
+ * transport: "h1" | "h2", // mask-direction policy, default "h1"
844
+ * maxMessageBytes: number, // total reassembled-message cap, default 1 MiB
845
+ * pingIntervalMs: number, // heartbeat interval, default 30s
846
+ * pongTimeoutMs: number, // abort threshold without pong, default 35s
847
+ * closeGraceMs: number, // peer-echo wait after close(), default 2s
848
+ * permessageDeflate: object | null, // negotiated state, usually from handleUpgrade
849
+ *
850
+ * @example
851
+ * server.on("upgrade", function (req, socket, head) {
852
+ * var conn = b.websocket.handleUpgrade(req, socket, head, {
853
+ * subprotocols: ["chat.v1"],
854
+ * });
855
+ * if (!conn) return;
856
+ * conn.on("message", function (data, isBinary) {
857
+ * conn.send(isBinary ? data : "echo: " + data);
858
+ * });
859
+ * conn.on("ping", function (payload) { void payload; }); // framework auto-pongs
860
+ * conn.on("pong", function (payload) { void payload; }); // heartbeat reply
861
+
862
+ * conn.on("close", function (code, reason, wasClean) {
863
+ * // → 1000, "", true
864
+ * });
865
+ * });
866
+ */
626
867
  class WebSocketConnection extends EventEmitter {
627
868
  constructor(socket, opts) {
628
869
  super();
@@ -990,6 +1231,48 @@ class WebSocketConnection extends EventEmitter {
990
1231
  // this function. Operators usually don't call it directly; they pass
991
1232
  // a handler to router.ws(path, opts).
992
1233
 
1234
+ /**
1235
+ * @primitive b.websocket.handleUpgrade
1236
+ * @signature b.websocket.handleUpgrade(req, socket, head, opts)
1237
+ * @since 0.1.38
1238
+ * @status stable
1239
+ * @related b.websocket.handleExtendedConnect, b.websocket.WebSocketConnection
1240
+ *
1241
+ * RFC 6455 HTTP/1.1 upgrade entry point. Wire it to the HTTP
1242
+ * server's `'upgrade'` event. Validates the handshake, enforces the
1243
+ * Origin policy (same-origin by default), negotiates subprotocol +
1244
+ * permessage-deflate, writes the 101 response, and returns a
1245
+ * `WebSocketConnection`. Returns `null` and writes a refusal HTTP
1246
+ * response on bad handshake / origin mismatch — the caller does not
1247
+ * need a try/catch around the normal refusal paths. Throws
1248
+ * synchronously only when `opts.handshakeGuid` is supplied with a
1249
+ * malformed value (config-time typo).
1250
+ *
1251
+ * @opts
1252
+ * origins: string[] | "*", // allowlist, or "*" accept-all; default same-origin
1253
+ * subprotocols: string[], // negotiation allowlist
1254
+ * handshakeGuid: string, // UUID-shape override of RFC 6455 §1.3 GUID
1255
+ * permessageDeflate: boolean, // RFC 7692 negotiation, default true
1256
+ * maxMessageBytes: number, // total message cap, default 1 MiB
1257
+ * pingIntervalMs: number, // heartbeat interval, default 30s
1258
+ * pongTimeoutMs: number, // abort-after-silence, default 35s
1259
+ * allowQueryAuthParams: boolean, // opt out of credential-query refusal
1260
+ *
1261
+ * @example
1262
+ * var http = require("http");
1263
+ * var server = http.createServer();
1264
+ * server.on("upgrade", function (req, socket, head) {
1265
+ * var conn = b.websocket.handleUpgrade(req, socket, head, {
1266
+ * origins: ["https://app.example.com"],
1267
+ * subprotocols: ["chat.v1"],
1268
+ * });
1269
+ * if (!conn) return; // refusal already written + socket destroyed
1270
+ * conn.on("message", function (data, isBinary) {
1271
+ * // → "hello", false
1272
+ * conn.send("ack: " + data);
1273
+ * });
1274
+ * });
1275
+ */
993
1276
  function handleUpgrade(req, socket, head, opts) {
994
1277
  opts = opts || {};
995
1278
 
@@ -1082,6 +1365,48 @@ function handleUpgrade(req, socket, head, opts) {
1082
1365
  // — pass `settings: { enableConnectProtocol: true }` to
1083
1366
  // http2.createServer / createSecureServer.
1084
1367
 
1368
+ /**
1369
+ * @primitive b.websocket.handleExtendedConnect
1370
+ * @signature b.websocket.handleExtendedConnect(stream, requestHeaders, opts)
1371
+ * @since 0.1.39
1372
+ * @status stable
1373
+ * @related b.websocket.handleUpgrade, b.websocket.WebSocketConnection
1374
+ *
1375
+ * RFC 8441 Extended CONNECT entry point for HTTP/2. Wire it to the
1376
+ * h2 server's `'stream'` event when `:method` is `CONNECT` and
1377
+ * `:protocol` is `websocket`. Same Origin / subprotocol policy as
1378
+ * `handleUpgrade`. Responds with `:status 200` (NOT 101 — Extended
1379
+ * CONNECT is a CONNECT, not an Upgrade) and returns a
1380
+ * `WebSocketConnection` wrapping the h2 stream with mask-direction
1381
+ * flipped (h2 frames MUST NOT be masked). The h2 server must
1382
+ * advertise `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1`; pass
1383
+ * `settings: { enableConnectProtocol: true }` to
1384
+ * `http2.createSecureServer`. Returns `null` on refusal.
1385
+ *
1386
+ * @opts
1387
+ * origins: string[] | "*", // allowlist / accept-all; default same-origin
1388
+ * subprotocols: string[], // negotiation allowlist
1389
+ * maxMessageBytes: number, // total message cap, default 1 MiB
1390
+ * pingIntervalMs: number, // heartbeat interval, default 30s
1391
+ * pongTimeoutMs: number, // abort-after-silence, default 35s
1392
+ *
1393
+ * @example
1394
+ * var http2 = require("http2");
1395
+ * var server = http2.createSecureServer({
1396
+ * key: fs.readFileSync("/etc/blamejs/tls.key"),
1397
+ * cert: fs.readFileSync("/etc/blamejs/tls.crt"),
1398
+ * settings: { enableConnectProtocol: true },
1399
+ * });
1400
+ * server.on("stream", function (stream, headers) {
1401
+ * if (headers[":method"] !== "CONNECT") return;
1402
+ * var conn = b.websocket.handleExtendedConnect(stream, headers, {
1403
+ * origins: ["https://app.example.com"],
1404
+ * subprotocols: ["chat.v1"],
1405
+ * });
1406
+ * if (!conn) return;
1407
+ * conn.close(1000, "shutdown"); // → 1000, "shutdown", true on the peer's 'close'
1408
+ * });
1409
+ */
1085
1410
  function handleExtendedConnect(stream, requestHeaders, opts) {
1086
1411
  opts = opts || {};
1087
1412