@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/crypto.js CHANGED
@@ -1,32 +1,71 @@
1
1
  "use strict";
2
2
  /**
3
- * Centralized crypto module — envelope-versioned PQC primitives.
4
- *
5
- * Algorithm suite (modernity bar, per blamejs principle #8):
6
- * KEM: ML-KEM-1024 + P-384 ECDH hybrid (FIPS 203 + classical defense in depth)
7
- * Symmetric: XChaCha20-Poly1305 (24-byte nonce — no nonce-reuse risk under volume)
8
- * KDF: SHAKE256 (FIPS 202)
9
- * Hash: SHA3-512
10
- * HMAC: HMAC-SHA3-512
11
- * Signatures: ML-DSA-87 / SLH-DSA-SHAKE-256f (auto-detected from key PEM)
12
- *
13
- * Argon2id lives in lib/vault-wrap.js (used to derive vault and
14
- * audit-signing key passphrases), not here.
15
- *
16
- * Envelope versioning (lib/constants.js ENVELOPE_MAGIC, KEM_IDS, etc.):
17
- * byte 0: ENVELOPE_MAGIC (0xE1)
18
- * byte 1: KEM ID
19
- * byte 2: CIPHER ID
20
- * byte 3: KDF ID
21
- *
22
- * Old data decrypts under whichever IDs were written into its envelope; new
23
- * writes use ACTIVE.{KEM, CIPHER, KDF}. Algorithm rotation is forward-only —
24
- * see roadmap "Modernity posture" for the rotation policy.
3
+ * @module b.crypto
4
+ * @featured true
5
+ * @nav Crypto
6
+ * @title Crypto
7
+ *
8
+ * @intro
9
+ * The framework's PQC-first cryptography surface. Every default is
10
+ * post-quantum-aware: ML-KEM-1024 + ECDH P-384 hybrid for key
11
+ * encapsulation (FIPS 203 + classical defense-in-depth), XChaCha20-
12
+ * Poly1305 for authenticated symmetric encryption (24-byte nonce —
13
+ * no nonce-reuse risk under high volume), SHAKE256 as the KDF
14
+ * (FIPS 202 XOF — arbitrary output length), SHA3-512 for hashing,
15
+ * HMAC-SHA3-512 for keyed integrity, and ML-DSA-87 / SLH-DSA-SHAKE-
16
+ * 256f for signatures (auto-detected from the key PEM). Argon2id
17
+ * passphrase stretching lives in `b.vaultWrap`, not here.
18
+ *
19
+ * Envelope wire format (length-prefixed, self-describing):
20
+ *
21
+ * byte 0 : ENVELOPE_MAGIC
22
+ * byte 1 : KEM ID (ML_KEM_1024 / ML_KEM_1024_P384 / ML_KEM_768_X25519)
23
+ * byte 2 : CIPHER ID (XCHACHA20_POLY1305)
24
+ * byte 3 : KDF ID (SHAKE256)
25
+ * ... : KEM ciphertext, ephemeral ECDH pubkey, nonce, AEAD ciphertext
26
+ *
27
+ * The four-byte header is bound as AEAD AAD so an algorithm-
28
+ * substitution attack (a tampered byte-1 KEM ID, byte-2 cipher ID,
29
+ * etc.) fails Poly1305 verification. Old envelopes decrypt under the
30
+ * IDs written into their header; new writes use the active suite.
31
+ * The KDF additionally absorbs a NIST SP 800-56C r2 §4.1 FixedInfo
32
+ * suite-binding label so a key derived under one suite is not
33
+ * silently usable under another.
34
+ *
35
+ * Three KEM hybrids ship: ML-KEM-1024 KEM-only (legacy single-
36
+ * component), ML-KEM-1024 + ECDH P-384 (framework default), and
37
+ * ML-KEM-768 + X25519 (IETF / Cloudflare / Chrome TLS 1.3 codepoint
38
+ * 0x11EC — smaller payload, wider browser interop).
39
+ *
40
+ * SHA-1 / SHA-256 / AES-GCM / classical-only ECDH are intentionally
41
+ * absent from the public surface. Operators who genuinely need them
42
+ * call `node:crypto` directly so the choice surfaces in their code.
43
+ *
44
+ * @card
45
+ * The framework's PQC-first cryptography surface.
25
46
  */
26
47
  var nodeCrypto = require("crypto");
48
+ var nodeFs = require("fs");
49
+ var { pipeline } = require("stream/promises");
27
50
  var { xchacha20poly1305 } = require("./vendor/noble-ciphers.cjs");
28
51
  var C = require("./constants");
29
52
 
53
+ // Streaming-hash algorithm allowlist. Mirrors the framework's PQC-
54
+ // first crypto policy: SHA3 / SHAKE family is the default surface;
55
+ // SHA-512 is permitted for legitimate interop (signing artifacts that
56
+ // downstream verifiers compute as SHA-512). MD5 / SHA-1 / SHA-256 are
57
+ // not on the list — operators who genuinely need them call
58
+ // node:crypto directly so the choice surfaces in their code.
59
+ var STREAM_HASH_ALGORITHMS = Object.freeze({
60
+ "sha3-256": { algorithm: "sha3-256", needsOutputLength: false },
61
+ "sha3-384": { algorithm: "sha3-384", needsOutputLength: false },
62
+ "sha3-512": { algorithm: "sha3-512", needsOutputLength: false },
63
+ "sha512": { algorithm: "sha512", needsOutputLength: false },
64
+ "shake256": { algorithm: "shake256", needsOutputLength: true },
65
+ });
66
+ var STREAM_HASH_DEFAULT = "sha3-512";
67
+ var SHAKE256_DEFAULT_LEN = 64;
68
+
30
69
  // ===========================================================
31
70
  // Core primitives — everything else is built from these
32
71
  // ===========================================================
@@ -40,6 +79,74 @@ function hmac(key, data, algorithm) {
40
79
  return nodeCrypto.createHmac(algorithm, key).update(data).digest("hex");
41
80
  }
42
81
 
82
+ /**
83
+ * @primitive b.crypto.hashStream
84
+ * @signature b.crypto.hashStream(readable, algorithm)
85
+ * @since 0.5.0
86
+ * @related b.crypto.hashFile, b.crypto.sha3Hash
87
+ *
88
+ * Streams a Readable through `createHash(algorithm)` and resolves with
89
+ * the raw digest Buffer. Default algorithm is SHA3-512. Algorithm is
90
+ * validated against the allowlist (sha3-256 / sha3-384 / sha3-512 /
91
+ * sha512 / shake256) so a typo or weak choice throws at config time
92
+ * rather than producing a digest under a surprise algorithm. Read-
93
+ * only — no audit emit.
94
+ *
95
+ * @example
96
+ * var fs = require("fs");
97
+ * var stream = fs.createReadStream("/etc/hosts");
98
+ * b.crypto.hashStream(stream, "sha3-512").then(function (digest) {
99
+ * digest.toString("hex");
100
+ * // → "abcd0123...e8f9" (128 hex chars, SHA3-512 = 64 bytes)
101
+ * });
102
+ */
103
+ function hashStream(readable, algorithm) {
104
+ var alg = (algorithm || STREAM_HASH_DEFAULT).toLowerCase();
105
+ var entry = STREAM_HASH_ALGORITHMS[alg];
106
+ if (!entry) {
107
+ return Promise.reject(new TypeError(
108
+ "crypto.hashStream: unsupported algorithm '" + algorithm +
109
+ "' (allowed: " + Object.keys(STREAM_HASH_ALGORITHMS).join(", ") + ")"
110
+ ));
111
+ }
112
+ if (!readable || typeof readable.pipe !== "function") {
113
+ return Promise.reject(new TypeError(
114
+ "crypto.hashStream: readable must be a Readable stream"
115
+ ));
116
+ }
117
+ var hashOpts = entry.needsOutputLength ? { outputLength: SHAKE256_DEFAULT_LEN } : undefined;
118
+ var digester = nodeCrypto.createHash(entry.algorithm, hashOpts);
119
+ return pipeline(readable, digester).then(function () {
120
+ return digester.digest();
121
+ });
122
+ }
123
+
124
+ /**
125
+ * @primitive b.crypto.hashFile
126
+ * @signature b.crypto.hashFile(filePath, algorithm)
127
+ * @since 0.5.0
128
+ * @related b.crypto.hashStream, b.crypto.sha3Hash
129
+ *
130
+ * Opens `filePath` as a Readable and streams it through `hashStream`.
131
+ * Resolves with the raw digest Buffer. Default algorithm is SHA3-512.
132
+ * Read-only — no audit emit; the path is operator-supplied and the
133
+ * digest is the only observable side-effect.
134
+ *
135
+ * @example
136
+ * b.crypto.hashFile("/etc/hosts", "sha3-256").then(function (digest) {
137
+ * digest.toString("hex");
138
+ * // → "0123abcd...ef89" (64 hex chars, SHA3-256 = 32 bytes)
139
+ * });
140
+ */
141
+ function hashFile(filePath, algorithm) {
142
+ if (typeof filePath !== "string" || filePath.length === 0) {
143
+ return Promise.reject(new TypeError(
144
+ "crypto.hashFile: path must be a non-empty string"
145
+ ));
146
+ }
147
+ return hashStream(nodeFs.createReadStream(filePath), algorithm);
148
+ }
149
+
43
150
  function random(byteLength) {
44
151
  var n = byteLength || 32;
45
152
  // SHAKE256 over OS-RNG bytes. The OS RNG (nodeCrypto.randomBytes) is
@@ -65,6 +172,25 @@ function generateKeyPair(algorithm, options) {
65
172
  return { publicKey: pair.publicKey, privateKey: pair.privateKey };
66
173
  }
67
174
 
175
+ /**
176
+ * @primitive b.crypto.timingSafeEqual
177
+ * @signature b.crypto.timingSafeEqual(a, b)
178
+ * @since 0.1.0
179
+ * @related b.crypto.hmacSha3
180
+ *
181
+ * Constant-time equality comparison. Coerces non-Buffer inputs via
182
+ * `Buffer.from(String(...))`, returns `false` immediately when lengths
183
+ * differ (length itself is not a secret), then routes equal-length
184
+ * inputs through `crypto.timingSafeEqual`. Use when comparing HMAC
185
+ * digests, session tokens, password-reset codes, or any
186
+ * attacker-influenced value where a timing oracle would leak bits.
187
+ *
188
+ * @example
189
+ * var expected = b.crypto.hmacSha3("server-key", "payload");
190
+ * var supplied = "ab12...e9"; // from request header / body
191
+ * var ok = b.crypto.timingSafeEqual(supplied, expected);
192
+ * // → true when bytes match, false otherwise (no early exit on mismatch)
193
+ */
68
194
  function timingSafeEqual(a, b) {
69
195
  var bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
70
196
  var bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
@@ -77,7 +203,39 @@ function timingSafeEqual(a, b) {
77
203
  // ===========================================================
78
204
 
79
205
  // ---- Hashing ----
206
+ /**
207
+ * @primitive b.crypto.sha3Hash
208
+ * @signature b.crypto.sha3Hash(data)
209
+ * @since 0.1.0
210
+ * @related b.crypto.hmacSha3, b.crypto.kdf, b.crypto.hashFile
211
+ *
212
+ * Returns the lowercase-hex SHA3-512 digest of the input. SHA3-512 is
213
+ * the framework's default hash — collision-resistant, sponge-based,
214
+ * and PQC-aligned (no quantum speedup beyond Grover's). Suitable for
215
+ * content fingerprints, integrity checks, derived-column inputs, and
216
+ * Merkle-tree leaves.
217
+ *
218
+ * @example
219
+ * var digest = b.crypto.sha3Hash("hello world");
220
+ * // → "75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df..."
221
+ */
80
222
  function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
223
+
224
+ /**
225
+ * @primitive b.crypto.hmacSha3
226
+ * @signature b.crypto.hmacSha3(key, data)
227
+ * @since 0.1.0
228
+ * @related b.crypto.sha3Hash, b.crypto.timingSafeEqual
229
+ *
230
+ * Returns the lowercase-hex HMAC-SHA3-512 of `data` keyed by `key`.
231
+ * Use for keyed integrity checks (webhook signatures, request
232
+ * authentication tags, audit-chain links). Pair with
233
+ * `b.crypto.timingSafeEqual` when comparing supplied vs computed tags.
234
+ *
235
+ * @example
236
+ * var tag = b.crypto.hmacSha3("shared-secret", "POST /webhook|123");
237
+ * // → "8f1c...d4e2" (128 hex chars, HMAC-SHA3-512 = 64 bytes)
238
+ */
81
239
  function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
82
240
 
83
241
  // (SHA-1 is intentionally NOT exported from b.crypto. The framework's
@@ -89,8 +247,133 @@ function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
89
247
  // framework spent every other line keeping out.)
90
248
 
91
249
  // ---- KDF ----
250
+ /**
251
+ * @primitive b.crypto.kdf
252
+ * @signature b.crypto.kdf(input, outputLength)
253
+ * @since 0.1.0
254
+ * @related b.crypto.sha3Hash, b.crypto.generateBytes
255
+ *
256
+ * SHAKE256-based key derivation. Returns a Buffer of exactly
257
+ * `outputLength` bytes derived from `input`. SHAKE256 is an XOF
258
+ * (extendable-output function) — arbitrary output length without the
259
+ * truncation pitfalls of fixed-width SHA3 + slice. Used internally
260
+ * for envelope symmetric-key derivation; operators reach for it when
261
+ * they need application-specific subkeys with explicit length.
262
+ *
263
+ * @example
264
+ * var seed = Buffer.from("master-secret|session-42", "utf8");
265
+ * var subkey = b.crypto.kdf(seed, 32);
266
+ * subkey.length;
267
+ * // → 32 (32-byte XChaCha20 key)
268
+ */
92
269
  function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
93
270
 
271
+ // ---- App-namespaced indexable hash (for derived-hash columns) ----
272
+ //
273
+ // b.crypto.namespaceHash(prefix, value) → hex-encoded SHA3-512 of
274
+ // `prefix + ":" + value`. Operators wire this into derived-hash
275
+ // columns (emailHash, certFpHash, externalIdHash) where the goal is
276
+ // indexed exact-match lookup, NOT credential storage. Returns hex —
277
+ // not the envelope-versioned base64 b.credentialHash.hash returns —
278
+ // because hex strings are stable, indexable column values across
279
+ // every database backend the framework supports.
280
+ //
281
+ // Why a separate primitive vs `b.crypto.sha3Hash(prefix + ":" + value)`:
282
+ // - Centralized prefix-shape validation (NUL/CR/LF rejection,
283
+ // length bound) — operator can't accidentally smuggle a
284
+ // framework-derived prefix through a user-controlled value.
285
+ // - Clear name documents the intent ("indexable namespace hash"
286
+ // vs "raw content digest"), so callers are less likely to
287
+ // reach for the credential-storage primitive when they want a
288
+ // read-only lookup hash.
289
+ //
290
+ // Read-only / deterministic — no audit emit (the input is operator-
291
+ // supplied; the digest is the only observable side-effect, returned
292
+ // to the caller). NUL / CR / LF in `prefix` are refused so an
293
+ // operator can't smuggle a control sequence into framework or
294
+ // downstream tooling that consumes the audit log; the bound on
295
+ // `prefix` length prevents oversized namespace separators (the
296
+ // framework's HASH_PREFIX entries are <= 16 bytes).
297
+ var NAMESPACE_HASH_PREFIX_MAX_BYTES = 64;
298
+
299
+ /**
300
+ * @primitive b.crypto.namespaceHash
301
+ * @signature b.crypto.namespaceHash(prefix, value, opts)
302
+ * @since 0.6.0
303
+ * @related b.crypto.sha3Hash, b.credentialHash.hash
304
+ *
305
+ * App-namespaced indexable SHA3-512 hash for derived-hash columns
306
+ * (emailHash, certFpHash, externalIdHash). Returns lowercase hex —
307
+ * stable, indexable column values across every supported database.
308
+ * Centralizes prefix-shape validation: NUL / CR / LF in `prefix` are
309
+ * refused outright, and `prefix` is bounded to 64 UTF-8 bytes so an
310
+ * operator can't smuggle log-injection or oversized labels into
311
+ * derived-column inputs. Use when the goal is exact-match lookup,
312
+ * NOT credential storage — for password-style storage use
313
+ * `b.credentialHash.hash`.
314
+ *
315
+ * @opts
316
+ * reserved: object, // accepted but ignored — reserved for a future algorithm-selection knob
317
+ *
318
+ * @example
319
+ * var emailHash = b.crypto.namespaceHash("email", "alice@example.com");
320
+ * // → "1f3a...c08d" (128 hex chars, SHA3-512 of "email:alice@example.com")
321
+ *
322
+ * var certFpHash = b.crypto.namespaceHash("cert-fp", Buffer.from([1, 2, 3, 4]));
323
+ * // Buffer/Uint8Array values are coerced to UTF-8 string before hashing.
324
+ */
325
+ function namespaceHash(prefix, value, opts) {
326
+ // opts reserved for future extension (algorithm selection); current
327
+ // surface is fixed to SHA3-512 — no operator demand for SHAKE256
328
+ // variable-length output here, since the indexed column shape is
329
+ // fixed-width hex.
330
+ if (opts && typeof opts !== "object") {
331
+ throw new TypeError("crypto.namespaceHash: opts must be a plain object when provided");
332
+ }
333
+ if (typeof prefix !== "string") {
334
+ throw new TypeError("crypto.namespaceHash: prefix must be a string");
335
+ }
336
+ if (prefix.length === 0) {
337
+ throw new TypeError("crypto.namespaceHash: prefix must be non-empty");
338
+ }
339
+ // Byte-length bound — operator's prefix is the namespace label and
340
+ // shouldn't bloat the hash input. Use Buffer.byteLength so multi-
341
+ // byte UTF-8 prefixes can't slip through a code-unit-only check.
342
+ if (Buffer.byteLength(prefix, "utf8") > NAMESPACE_HASH_PREFIX_MAX_BYTES) {
343
+ throw new TypeError(
344
+ "crypto.namespaceHash: prefix exceeds " + NAMESPACE_HASH_PREFIX_MAX_BYTES +
345
+ " bytes (UTF-8); operator-derived prefixes should be short labels"
346
+ );
347
+ }
348
+ // NUL / CR / LF in prefix — refuse outright. NUL truncates in many
349
+ // C-string consumers (audit-log path, downstream DB tooling); CR/LF
350
+ // smuggles log-injection patterns into anything that renders the
351
+ // prefix verbatim.
352
+ // eslint-disable-next-line no-control-regex
353
+ if (/[\u0000\r\n]/.test(prefix)) {
354
+ throw new TypeError(
355
+ "crypto.namespaceHash: prefix contains NUL / CR / LF — refuse"
356
+ );
357
+ }
358
+ // value is the operator-supplied content. Coerce Buffer/Uint8Array
359
+ // to utf-8 string for concatenation; reject anything else so the
360
+ // caller surfaces the type error explicitly rather than silently
361
+ // hashing `[object Object]`.
362
+ var valueStr;
363
+ if (typeof value === "string") {
364
+ valueStr = value;
365
+ } else if (Buffer.isBuffer(value)) {
366
+ valueStr = value.toString("utf8");
367
+ } else if (value instanceof Uint8Array) {
368
+ valueStr = Buffer.from(value).toString("utf8");
369
+ } else {
370
+ throw new TypeError(
371
+ "crypto.namespaceHash: value must be a string, Buffer, or Uint8Array"
372
+ );
373
+ }
374
+ return hash(prefix + ":" + valueStr, "sha3-512").toString("hex");
375
+ }
376
+
94
377
  // _suiteFixedInfo — NIST SP 800-56C r2 §4.1 OtherInfo / RFC 9180
95
378
  // (HPKE) §5.1 suite_id binding. Returns the byte string that the KDF
96
379
  // MUST absorb alongside the shared-secret(s) so a key derived under
@@ -105,7 +388,48 @@ function _suiteFixedInfo(kemId, cipherId, kdfId) {
105
388
  }
106
389
 
107
390
  // ---- Random ----
391
+ /**
392
+ * @primitive b.crypto.generateBytes
393
+ * @signature b.crypto.generateBytes(byteLength)
394
+ * @since 0.1.0
395
+ * @related b.crypto.generateToken, b.uuid.v4
396
+ *
397
+ * Cryptographically secure random Buffer of length `byteLength`
398
+ * (default 32). The bytes are SHAKE256(OS-RNG bytes) — defense-in-
399
+ * depth over `crypto.randomBytes` so a hypothetical OS-RNG weakness
400
+ * is not directly observable downstream. Use for session IDs, KDF
401
+ * salts, AEAD nonces, anything requiring unpredictable bytes.
402
+ *
403
+ * @example
404
+ * var sessionId = b.crypto.generateBytes(16).toString("hex");
405
+ * // → "5b8f2a4c7d1e9f0b3c6a8d2e4f7c1b5d" (32 hex chars, 16 random bytes)
406
+ *
407
+ * var nonce = b.crypto.generateBytes(24); // XChaCha20-Poly1305 nonce
408
+ * nonce.length;
409
+ * // → 24
410
+ */
108
411
  function generateBytes(byteLength) { return Buffer.from(random(byteLength)); }
412
+
413
+ /**
414
+ * @primitive b.crypto.generateToken
415
+ * @signature b.crypto.generateToken(byteLength)
416
+ * @since 0.1.0
417
+ * @related b.crypto.generateBytes, b.uuid.v4
418
+ *
419
+ * Hex-encoded random token. Same entropy source as `generateBytes`
420
+ * (SHAKE256 over OS-RNG bytes) but returned as a lowercase hex string
421
+ * — convenient for HTTP headers, URL parameters, log fields, or any
422
+ * context where a Buffer would need to be encoded anyway. Default
423
+ * `byteLength` is 32 (64 hex chars, ~256 bits of entropy).
424
+ *
425
+ * @example
426
+ * var token = b.crypto.generateToken();
427
+ * token.length;
428
+ * // → 64 (32 bytes hex-encoded)
429
+ *
430
+ * var shortId = b.crypto.generateToken(8);
431
+ * // → "a3f9...b1" (16 hex chars, 8 random bytes)
432
+ */
109
433
  function generateToken(byteLength) { return random(byteLength || 32).toString("hex"); }
110
434
 
111
435
  // ---- Subresource Integrity (W3C SRI 1.0) ----
@@ -127,6 +451,32 @@ function generateToken(byteLength) { return random(byteLength || 32).toString("h
127
451
  // → "sha384-X1... sha384-X2..." (per W3C §3.3 multi-integrity)
128
452
  var SRI_ALGORITHMS = { "sha256": "sha256", "sha384": "sha384", "sha512": "sha512" };
129
453
 
454
+ /**
455
+ * @primitive b.crypto.sri
456
+ * @signature b.crypto.sri(content, opts)
457
+ * @since 0.5.0
458
+ * @related b.staticServe
459
+ *
460
+ * Computes a W3C Subresource Integrity 1.0 attribute string —
461
+ * `sha###-base64` — that operators paste into `<script integrity>` or
462
+ * `<link integrity>` tags. Defends against CDN compromise and ISP
463
+ * MITM injection: the browser refuses to load the resource when its
464
+ * computed hash diverges from the integrity attribute. SRI 1.0 §3.2
465
+ * supports sha256 / sha384 / sha512; sha384 is the recommended
466
+ * default (collision margin without sha512's 64-byte overhead). Pass
467
+ * an array of contents to emit multiple integrity tokens space-
468
+ * separated per §3.3 (browser picks the strongest it recognizes).
469
+ *
470
+ * @opts
471
+ * algorithm: string, // "sha256" | "sha384" | "sha512" — default "sha384"
472
+ *
473
+ * @example
474
+ * var attr = b.crypto.sri(Buffer.from("alert(1);", "utf8"), { algorithm: "sha384" });
475
+ * // → "sha384-pNdyOuHIPKgRPnYJTBxEEEZcJj1qHxJzNheCuHGRy3Cm0UpVbcnruIvMRIs5VcDb"
476
+ *
477
+ * var multi = b.crypto.sri(["payload-a", "payload-b"], { algorithm: "sha512" });
478
+ * // → "sha512-... sha512-..." (two tokens, space-separated)
479
+ */
130
480
  function sri(content, opts) {
131
481
  opts = opts || {};
132
482
  var algorithm = (opts.algorithm || "sha384").toLowerCase();
@@ -149,6 +499,31 @@ function sri(content, opts) {
149
499
  }
150
500
 
151
501
  // ---- Key generation ----
502
+ /**
503
+ * @primitive b.crypto.generateEncryptionKeyPair
504
+ * @signature b.crypto.generateEncryptionKeyPair()
505
+ * @since 0.1.0
506
+ * @related b.crypto.encrypt, b.crypto.decrypt, b.crypto.generateMlkem768X25519KeyPair
507
+ *
508
+ * Generates a hybrid recipient keypair for `b.crypto.encrypt`:
509
+ * ML-KEM-1024 (FIPS 203 PQC KEM) plus ECDH P-384 (classical defense-
510
+ * in-depth). Returns `{ publicKey, privateKey, ecPublicKey,
511
+ * ecPrivateKey }` — all four PEMs. Persist the private halves in
512
+ * sealed storage; publish the public halves to recipients. The
513
+ * framework default for at-rest envelopes and api-encrypt strategies.
514
+ *
515
+ * @example
516
+ * var pair = b.crypto.generateEncryptionKeyPair();
517
+ * var sealed = b.crypto.encrypt("secret payload", {
518
+ * publicKey: pair.publicKey,
519
+ * ecPublicKey: pair.ecPublicKey,
520
+ * });
521
+ * var roundTrip = b.crypto.decrypt(sealed, {
522
+ * privateKey: pair.privateKey,
523
+ * ecPrivateKey: pair.ecPrivateKey,
524
+ * });
525
+ * // → "secret payload"
526
+ */
152
527
  function generateEncryptionKeyPair() {
153
528
  var mlkem = generateKeyPair("ml-kem-1024");
154
529
  var ec = generateKeyPair("ec", { namedCurve: "P-384" });
@@ -160,15 +535,76 @@ function generateEncryptionKeyPair() {
160
535
  };
161
536
  }
162
537
 
538
+ /**
539
+ * @primitive b.crypto.generateSigningKeyPair
540
+ * @signature b.crypto.generateSigningKeyPair(algorithm)
541
+ * @since 0.1.0
542
+ * @related b.crypto.sign, b.crypto.verify
543
+ *
544
+ * Generates a PQC signature keypair. Default algorithm is `ml-dsa-87`
545
+ * (FIPS 204 — lattice-based, fast verify); pass `slh-dsa-shake-256f`
546
+ * for hash-based signatures (larger, slower, but minimal cryptographic
547
+ * assumptions — useful for long-lived audit-chain keys). Returns
548
+ * `{ publicKey, privateKey }` PEMs. The signing primitives auto-
549
+ * detect the algorithm from the key PEM, so callers don't need to
550
+ * pass it explicitly to `sign` / `verify`.
551
+ *
552
+ * @example
553
+ * var pair = b.crypto.generateSigningKeyPair();
554
+ * var sig = b.crypto.sign("audit:row=42|action=delete", pair.privateKey);
555
+ * var ok = b.crypto.verify("audit:row=42|action=delete", sig, pair.publicKey);
556
+ * // → true
557
+ *
558
+ * // Hash-based alternative:
559
+ * var slh = b.crypto.generateSigningKeyPair("slh-dsa-shake-256f");
560
+ */
163
561
  function generateSigningKeyPair(algorithm) {
164
562
  return generateKeyPair(algorithm || "ml-dsa-87");
165
563
  }
166
564
 
167
565
  // ---- Signatures (auto-detect algorithm from key PEM) ----
566
+ /**
567
+ * @primitive b.crypto.sign
568
+ * @signature b.crypto.sign(data, privateKeyPem)
569
+ * @since 0.1.0
570
+ * @related b.crypto.verify, b.crypto.generateSigningKeyPair
571
+ *
572
+ * Produces a PQC signature over `data`. Algorithm is auto-detected
573
+ * from the private-key PEM (ML-DSA-87 lattice / SLH-DSA-SHAKE-256f
574
+ * hash-based). Returns a Buffer. Pair with `b.crypto.verify` on the
575
+ * recipient side; use for audit-chain links, webhook tags,
576
+ * cross-service request signatures.
577
+ *
578
+ * @example
579
+ * var pair = b.crypto.generateSigningKeyPair();
580
+ * var sig = b.crypto.sign("payload-to-sign", pair.privateKey);
581
+ * sig.length > 0;
582
+ * // → true (ML-DSA-87 signature ~ 4627 bytes)
583
+ */
168
584
  function sign(data, privateKeyPem) {
169
585
  return nodeCrypto.sign(null, Buffer.from(data), privateKeyPem);
170
586
  }
171
587
 
588
+ /**
589
+ * @primitive b.crypto.verify
590
+ * @signature b.crypto.verify(data, signature, publicKeyPem)
591
+ * @since 0.1.0
592
+ * @related b.crypto.sign, b.crypto.generateSigningKeyPair
593
+ *
594
+ * Verifies a signature produced by `b.crypto.sign`. Returns `true` on
595
+ * a valid signature, `false` otherwise — never throws on a malformed
596
+ * signature, so operators don't need to wrap the call. Algorithm is
597
+ * auto-detected from the public-key PEM.
598
+ *
599
+ * @example
600
+ * var pair = b.crypto.generateSigningKeyPair();
601
+ * var sig = b.crypto.sign("hello", pair.privateKey);
602
+ * var ok = b.crypto.verify("hello", sig, pair.publicKey);
603
+ * // → true
604
+ *
605
+ * var tampered = b.crypto.verify("HELLO", sig, pair.publicKey);
606
+ * // → false (data mismatch)
607
+ */
172
608
  function verify(data, signature, publicKeyPem) {
173
609
  return nodeCrypto.verify(null, Buffer.from(data), publicKeyPem, signature);
174
610
  }
@@ -181,6 +617,38 @@ function verify(data, signature, publicKeyPem) {
181
617
  var _hybridDisabledAuditEmitted = false;
182
618
 
183
619
  // ---- Envelope encrypt (ML-KEM-1024 + P-384 ECDH hybrid + SHAKE256 + XChaCha20) ----
620
+ /**
621
+ * @primitive b.crypto.encrypt
622
+ * @signature b.crypto.encrypt(plaintext, publicKeys)
623
+ * @since 0.1.0
624
+ * @related b.crypto.decrypt, b.crypto.generateEncryptionKeyPair, b.crypto.encryptMlkem768X25519
625
+ *
626
+ * Seals `plaintext` into a base64 envelope under the recipient's
627
+ * keypair. Default suite is ML-KEM-1024 + ECDH P-384 hybrid (FIPS 203
628
+ * KEM with classical defense-in-depth) plus SHAKE256 KDF and
629
+ * XChaCha20-Poly1305 AEAD. The 4-byte envelope header (magic + KEM
630
+ * ID + cipher ID + KDF ID) is bound as AEAD AAD so an algorithm-
631
+ * substitution attack on the header fails Poly1305 verification.
632
+ * Pass `{ publicKey, ecPublicKey }` for the hybrid path; passing only
633
+ * an ML-KEM PEM falls back to KEM-only and emits a one-shot
634
+ * `system.crypto.hybrid_disabled` audit (operators wanting the silent
635
+ * KEM-only path call `encryptMlkem768X25519` or seal manually).
636
+ *
637
+ * @example
638
+ * var pair = b.crypto.generateEncryptionKeyPair();
639
+ * var sealed = b.crypto.encrypt("PHI: patient-42 dx=...", {
640
+ * publicKey: pair.publicKey,
641
+ * ecPublicKey: pair.ecPublicKey,
642
+ * });
643
+ * typeof sealed;
644
+ * // → "string" (base64 envelope)
645
+ *
646
+ * var plain = b.crypto.decrypt(sealed, {
647
+ * privateKey: pair.privateKey,
648
+ * ecPrivateKey: pair.ecPrivateKey,
649
+ * });
650
+ * // → "PHI: patient-42 dx=..."
651
+ */
184
652
  function encrypt(plaintext, publicKeys) {
185
653
  var mlkemPubPem = typeof publicKeys === "string" ? publicKeys : publicKeys.publicKey;
186
654
  var ecPubPem = typeof publicKeys === "string" ? null : publicKeys.ecPublicKey;
@@ -256,6 +724,33 @@ function encryptMlkemOnly(plaintext, publicKeyPem) {
256
724
  }
257
725
 
258
726
  // ---- Envelope decrypt (dispatches on envelope IDs, supports both KEM IDs) ----
727
+ /**
728
+ * @primitive b.crypto.decrypt
729
+ * @signature b.crypto.decrypt(ciphertext, privateKeys)
730
+ * @since 0.1.0
731
+ * @related b.crypto.encrypt, b.crypto.generateEncryptionKeyPair, b.crypto.decryptMlkem768X25519
732
+ *
733
+ * Opens a base64 envelope produced by `b.crypto.encrypt`. The
734
+ * envelope header is parsed first and the decrypt path dispatches by
735
+ * KEM ID — ML-KEM-1024 + P-384, ML-KEM-1024 KEM-only, or ML-KEM-768 +
736
+ * X25519 — so old envelopes continue to decrypt under whichever suite
737
+ * sealed them while new writes use the active suite. Throws on
738
+ * malformed magic, unsupported cipher / KDF, or Poly1305 tag failure.
739
+ * Pass `{ privateKey, ecPrivateKey }` for the default hybrid; the
740
+ * ML-KEM-768 + X25519 KEM ID also requires `x25519PrivateKey`.
741
+ *
742
+ * @example
743
+ * var pair = b.crypto.generateEncryptionKeyPair();
744
+ * var sealed = b.crypto.encrypt("session-token=abc123", {
745
+ * publicKey: pair.publicKey,
746
+ * ecPublicKey: pair.ecPublicKey,
747
+ * });
748
+ * var opened = b.crypto.decrypt(sealed, {
749
+ * privateKey: pair.privateKey,
750
+ * ecPrivateKey: pair.ecPrivateKey,
751
+ * });
752
+ * // → "session-token=abc123"
753
+ */
259
754
  function decrypt(ciphertext, privateKeys) {
260
755
  var packed = Buffer.from(ciphertext, "base64");
261
756
  if (packed[0] === 0xE1) { // allow:raw-byte-literal — legacy envelope magic
@@ -339,6 +834,32 @@ function decryptEnvelope(packed, privateKeys) {
339
834
  // binding (b.breakGlass.encryptCell binds (table, rowId, column) so a
340
835
  // ciphertext from row A literally cannot decrypt as row B even with
341
836
  // the same key).
837
+ /**
838
+ * @primitive b.crypto.encryptPacked
839
+ * @signature b.crypto.encryptPacked(buffer, key, aad)
840
+ * @since 0.1.0
841
+ * @related b.crypto.decryptPacked, b.crypto.encrypt
842
+ *
843
+ * Symmetric (key-already-known) authenticated encryption. Returns a
844
+ * self-describing Buffer: 1-byte format ID + 24-byte XChaCha20-
845
+ * Poly1305 nonce + ciphertext+tag. Operators who already hold a
846
+ * symmetric key (sealed-storage cell encryption, break-glass row
847
+ * encryption) reach for this instead of the envelope variants. The
848
+ * optional `aad` (additional authenticated data) is mixed into the
849
+ * Poly1305 tag; encrypt-time and decrypt-time AAD must match exactly
850
+ * or decryption fails. Wire it for context-binding (e.g. `(table,
851
+ * rowId, column)` so a ciphertext from row A literally cannot decrypt
852
+ * as row B even with the same key).
853
+ *
854
+ * @example
855
+ * var key = b.crypto.generateBytes(32);
856
+ * var data = Buffer.from("row-42 column-ssn", "utf8");
857
+ * var aad = Buffer.from("patients|42|ssn", "utf8");
858
+ * var packed = b.crypto.encryptPacked(data, key, aad);
859
+ * var plain = b.crypto.decryptPacked(packed, key, aad);
860
+ * plain.toString("utf8");
861
+ * // → "row-42 column-ssn"
862
+ */
342
863
  function encryptPacked(buffer, key, aad) {
343
864
  var nonce = random(C.BYTES.bytes(24));
344
865
  var ct = xchacha20poly1305(key, nonce, aad ? Buffer.from(aad) : undefined).encrypt(buffer);
@@ -349,6 +870,26 @@ function encryptPacked(buffer, key, aad) {
349
870
  ]);
350
871
  }
351
872
 
873
+ /**
874
+ * @primitive b.crypto.decryptPacked
875
+ * @signature b.crypto.decryptPacked(packed, key, aad)
876
+ * @since 0.1.0
877
+ * @related b.crypto.encryptPacked
878
+ *
879
+ * Inverse of `encryptPacked`. Reads the 1-byte format ID, extracts
880
+ * the 24-byte XChaCha20-Poly1305 nonce, and decrypts the trailing
881
+ * ciphertext under `key` + `aad`. Throws on unsupported format byte
882
+ * or AAD / tag mismatch — operators wrap when a graceful per-cell
883
+ * fallback is required.
884
+ *
885
+ * @example
886
+ * var key = b.crypto.generateBytes(32);
887
+ * var aad = Buffer.from("audit|2026-05-08", "utf8");
888
+ * var pkt = b.crypto.encryptPacked(Buffer.from("hello", "utf8"), key, aad);
889
+ * var open = b.crypto.decryptPacked(pkt, key, aad);
890
+ * open.toString("utf8");
891
+ * // → "hello"
892
+ */
352
893
  function decryptPacked(packed, key, aad) {
353
894
  if (packed[0] !== C.FORMAT.XCHACHA20_POLY1305) {
354
895
  throw new Error("Invalid packed format: unsupported version");
@@ -384,6 +925,32 @@ function decryptPacked(packed, key, aad) {
384
925
  // x25519PrivateKey } — privateKey is the ML-KEM-768 PEM, NOT the
385
926
  // default ML-KEM-1024.
386
927
 
928
+ /**
929
+ * @primitive b.crypto.generateMlkem768X25519KeyPair
930
+ * @signature b.crypto.generateMlkem768X25519KeyPair()
931
+ * @since 0.7.28
932
+ * @related b.crypto.encryptMlkem768X25519, b.crypto.decryptMlkem768X25519, b.crypto.generateEncryptionKeyPair
933
+ *
934
+ * Generates the IETF / Cloudflare / Chrome TLS 1.3 hybrid keypair
935
+ * (codepoint 0x11EC): ML-KEM-768 (FIPS 203) + X25519 (RFC 7748).
936
+ * Smaller payload than ML-KEM-1024 + P-384 (~1.1 KB vs ~1.6 KB) and
937
+ * wider interop with peers using the same hybrid (Cloudflare
938
+ * Workers, Chrome, browsers offering hybrid PQ key share). Returns
939
+ * `{ mlkemPublicKey, mlkemPrivateKey, x25519PublicKey,
940
+ * x25519PrivateKey }`.
941
+ *
942
+ * @example
943
+ * var pair = b.crypto.generateMlkem768X25519KeyPair();
944
+ * var sealed = b.crypto.encryptMlkem768X25519("interop payload", {
945
+ * mlkemPublicKey: pair.mlkemPublicKey,
946
+ * x25519PublicKey: pair.x25519PublicKey,
947
+ * });
948
+ * var plain = b.crypto.decryptMlkem768X25519(sealed, {
949
+ * privateKey: pair.mlkemPrivateKey,
950
+ * x25519PrivateKey: pair.x25519PrivateKey,
951
+ * });
952
+ * // → "interop payload"
953
+ */
387
954
  function generateMlkem768X25519KeyPair() {
388
955
  var mlkem = generateKeyPair("ml-kem-768");
389
956
  var x25519 = generateKeyPair("x25519");
@@ -395,6 +962,30 @@ function generateMlkem768X25519KeyPair() {
395
962
  };
396
963
  }
397
964
 
965
+ /**
966
+ * @primitive b.crypto.encryptMlkem768X25519
967
+ * @signature b.crypto.encryptMlkem768X25519(plaintext, recipient)
968
+ * @since 0.7.28
969
+ * @related b.crypto.decryptMlkem768X25519, b.crypto.encrypt, b.crypto.generateMlkem768X25519KeyPair
970
+ *
971
+ * Seals `plaintext` under the IETF / Cloudflare / Chrome TLS 1.3
972
+ * hybrid (ML-KEM-768 + X25519). Recipient shape is
973
+ * `{ mlkemPublicKey, x25519PublicKey }` — both PEMs. Same envelope
974
+ * wire format as the default hybrid; the KEM ID byte is
975
+ * `KEM_IDS.ML_KEM_768_X25519` so `b.crypto.decrypt` dispatches
976
+ * correctly on the receive side. Reach for this when the recipient
977
+ * publishes ML-KEM-768 + X25519 keys (TLS-1.3 codepoint 0x11EC peers,
978
+ * cross-stack interop with Cloudflare Workers or Chrome-side WebCrypto).
979
+ *
980
+ * @example
981
+ * var pair = b.crypto.generateMlkem768X25519KeyPair();
982
+ * var sealed = b.crypto.encryptMlkem768X25519("cross-stack message", {
983
+ * mlkemPublicKey: pair.mlkemPublicKey,
984
+ * x25519PublicKey: pair.x25519PublicKey,
985
+ * });
986
+ * typeof sealed;
987
+ * // → "string" (base64 envelope, ~1.1 KB for short plaintexts)
988
+ */
398
989
  function encryptMlkem768X25519(plaintext, recipient) {
399
990
  if (!recipient || !recipient.mlkemPublicKey || !recipient.x25519PublicKey) {
400
991
  throw new Error("encryptMlkem768X25519 requires { mlkemPublicKey, x25519PublicKey }");
@@ -440,6 +1031,31 @@ function encryptMlkem768X25519(plaintext, recipient) {
440
1031
  //
441
1032
  // recipient: { privateKey, x25519PrivateKey } — operator's keys
442
1033
  // ciphertext: base64 envelope from encryptMlkem768X25519
1034
+ /**
1035
+ * @primitive b.crypto.decryptMlkem768X25519
1036
+ * @signature b.crypto.decryptMlkem768X25519(ciphertext, recipient)
1037
+ * @since 0.7.28
1038
+ * @related b.crypto.encryptMlkem768X25519, b.crypto.decrypt
1039
+ *
1040
+ * Symmetric named-pair to `encryptMlkem768X25519`. Rejects any
1041
+ * envelope whose KEM ID byte is not `ML_KEM_768_X25519` so an
1042
+ * operator who calls this with a ciphertext sealed under a different
1043
+ * algorithm gets a clear error rather than the generic dispatch path.
1044
+ * Recipient shape is `{ privateKey, x25519PrivateKey }` — `privateKey`
1045
+ * is the ML-KEM-768 PEM, NOT the framework default ML-KEM-1024.
1046
+ *
1047
+ * @example
1048
+ * var pair = b.crypto.generateMlkem768X25519KeyPair();
1049
+ * var sealed = b.crypto.encryptMlkem768X25519("interop", {
1050
+ * mlkemPublicKey: pair.mlkemPublicKey,
1051
+ * x25519PublicKey: pair.x25519PublicKey,
1052
+ * });
1053
+ * var plain = b.crypto.decryptMlkem768X25519(sealed, {
1054
+ * privateKey: pair.mlkemPrivateKey,
1055
+ * x25519PrivateKey: pair.x25519PrivateKey,
1056
+ * });
1057
+ * // → "interop"
1058
+ */
443
1059
  function decryptMlkem768X25519(ciphertext, recipient) {
444
1060
  if (!recipient || typeof recipient !== "object" ||
445
1061
  !recipient.privateKey || !recipient.x25519PrivateKey) {
@@ -501,6 +1117,36 @@ function _extractEcdhP384FromCert(certDer) {
501
1117
  // peerCertDer: Buffer | Uint8Array, // peer's TLS cert (DER)
502
1118
  // peerKemPubkey: string, // peer's ML-KEM-1024 pubkey PEM
503
1119
  // });
1120
+ /**
1121
+ * @primitive b.crypto.encryptEnvelopeAsCertPeer
1122
+ * @signature b.crypto.encryptEnvelopeAsCertPeer(plaintext, opts)
1123
+ * @since 0.7.0
1124
+ * @related b.crypto.decryptEnvelopeAsCertPeer, b.crypto.encrypt
1125
+ *
1126
+ * Produces an envelope sealed to a peer identified by their TLS cert
1127
+ * (P-384 ECDH half) plus a peer-supplied ML-KEM-1024 pubkey. The wire
1128
+ * format is identical to `b.crypto.encrypt` — only the input keys
1129
+ * differ. Use for sealed-storage records with peer recipients,
1130
+ * cross-service messages between cert-identified peers without a
1131
+ * shared framework keypair, or audit-log entries tagged with peer
1132
+ * recipients. The cert must carry an ECDH P-384 SubjectPublicKeyInfo
1133
+ * — anything else throws `crypto/cert-key-not-ecdh-p384`.
1134
+ *
1135
+ * @opts
1136
+ * peerCertDer: Buffer, // peer's TLS cert as DER bytes (Buffer or Uint8Array)
1137
+ * peerKemPubkey: string, // peer's ML-KEM-1024 pubkey PEM (non-empty string)
1138
+ *
1139
+ * @example
1140
+ * var fs = require("fs");
1141
+ * var peerCertDer = fs.readFileSync("/etc/ssl/peer.cert.der");
1142
+ * var peerKemPubkey = fs.readFileSync("/etc/ssl/peer.mlkem.pem", "utf8");
1143
+ * var sealed = b.crypto.encryptEnvelopeAsCertPeer("cross-peer payload", {
1144
+ * peerCertDer: peerCertDer,
1145
+ * peerKemPubkey: peerKemPubkey,
1146
+ * });
1147
+ * typeof sealed;
1148
+ * // → "string" (base64 envelope)
1149
+ */
504
1150
  function encryptEnvelopeAsCertPeer(plaintext, opts) {
505
1151
  if (!opts || typeof opts !== "object") {
506
1152
  throw new Error("encryptEnvelopeAsCertPeer: opts object required");
@@ -534,6 +1180,35 @@ function encryptEnvelopeAsCertPeer(plaintext, opts) {
534
1180
  // certPrivateKey: KeyObject | string, // this operator's cert P-384 priv
535
1181
  // kemSecret: string, // this operator's ML-KEM-1024 priv PEM
536
1182
  // });
1183
+ /**
1184
+ * @primitive b.crypto.decryptEnvelopeAsCertPeer
1185
+ * @signature b.crypto.decryptEnvelopeAsCertPeer(envelope, opts)
1186
+ * @since 0.7.0
1187
+ * @related b.crypto.encryptEnvelopeAsCertPeer, b.crypto.decrypt
1188
+ *
1189
+ * Decrypts an envelope sealed to this operator's TLS cert ECDH-pubkey
1190
+ * + ML-KEM-1024 pubkey. `certPrivateKey` accepts either a node:crypto
1191
+ * `KeyObject` (ECDH P-384, namedCurve secp384r1) or its PEM-encoded
1192
+ * pkcs8 string; `kemSecret` is always the ML-KEM-1024 PEM. A non-
1193
+ * P-384 cert key throws `crypto/cert-key-not-ecdh-p384`. Mirror of
1194
+ * `encryptEnvelopeAsCertPeer` for the receive side.
1195
+ *
1196
+ * @opts
1197
+ * certPrivateKey: object, // KeyObject or PEM string — ECDH P-384 priv (secp384r1)
1198
+ * kemSecret: string, // operator's ML-KEM-1024 priv PEM (non-empty)
1199
+ *
1200
+ * @example
1201
+ * var fs = require("fs");
1202
+ * var ourCertPriv = fs.readFileSync("/etc/ssl/our.cert.key.pem", "utf8");
1203
+ * var ourKemSecret = fs.readFileSync("/etc/ssl/our.mlkem.priv.pem", "utf8");
1204
+ * var sealed = "AaECA..."; // base64 envelope received from peer
1205
+ * var plain = b.crypto.decryptEnvelopeAsCertPeer(sealed, {
1206
+ * certPrivateKey: ourCertPriv,
1207
+ * kemSecret: ourKemSecret,
1208
+ * });
1209
+ * typeof plain;
1210
+ * // → "string"
1211
+ */
537
1212
  function decryptEnvelopeAsCertPeer(envelope, opts) {
538
1213
  if (!opts || typeof opts !== "object") {
539
1214
  throw new Error("decryptEnvelopeAsCertPeer: opts object required");
@@ -608,6 +1283,30 @@ function _pemToDer(pemOrDer) {
608
1283
  }
609
1284
  return Buffer.from(match[1].replace(/\s+/g, ""), "base64");
610
1285
  }
1286
+ /**
1287
+ * @primitive b.crypto.hashCertFingerprint
1288
+ * @signature b.crypto.hashCertFingerprint(pemOrDer)
1289
+ * @since 0.7.0
1290
+ * @related b.crypto.isCertRevoked, b.crypto.sha3Hash
1291
+ *
1292
+ * Computes a stable SHA3-512 fingerprint of an X.509 certificate.
1293
+ * Accepts either DER bytes (Buffer) or a PEM string (BEGIN/END
1294
+ * envelope is stripped, base64 body decoded). Returns
1295
+ * `{ hex, colon }` so callers can compare against either rendering
1296
+ * style — lowercase hex (concise, log-friendly) or uppercase
1297
+ * colon-separated hex (matches `openssl x509 -fingerprint` output
1298
+ * shape). Use for peer-cert pinning, mTLS bootstrap allowlists,
1299
+ * webhook verification, certificate-transparency cross-checks.
1300
+ *
1301
+ * @example
1302
+ * var fs = require("fs");
1303
+ * var pem = fs.readFileSync("/etc/ssl/peer.cert.pem", "utf8");
1304
+ * var fp = b.crypto.hashCertFingerprint(pem);
1305
+ * fp.hex.length;
1306
+ * // → 128 (SHA3-512 = 64 bytes hex-encoded)
1307
+ * fp.colon.split(":").length;
1308
+ * // → 64 (one byte per group)
1309
+ */
611
1310
  function hashCertFingerprint(pemOrDer) {
612
1311
  var der = _pemToDer(pemOrDer);
613
1312
  var digest = hash(der, "sha3-512");
@@ -622,6 +1321,26 @@ function hashCertFingerprint(pemOrDer) {
622
1321
  // fingerprints. Allowlist entries may be the colon form, the lower-
623
1322
  // case hex form, or both — every comparison runs through
624
1323
  // timingSafeEqual to avoid leaking which entry matched.
1324
+ /**
1325
+ * @primitive b.crypto.isCertRevoked
1326
+ * @signature b.crypto.isCertRevoked(pemOrDer, denyList)
1327
+ * @since 0.7.0
1328
+ * @related b.crypto.hashCertFingerprint, b.crypto.timingSafeEqual
1329
+ *
1330
+ * Returns `true` when the cert's SHA3-512 fingerprint matches any
1331
+ * entry in `denyList`. `denyList` entries may be the colon-separated
1332
+ * uppercase hex form, the lowercase hex form, or both — every
1333
+ * comparison runs through `crypto.timingSafeEqual` so the answer
1334
+ * doesn't leak which entry matched. Use for cert-transparency-style
1335
+ * deny lists, revoked-peer sweeps, or compromised-CA blocking.
1336
+ *
1337
+ * @example
1338
+ * var fs = require("fs");
1339
+ * var pem = fs.readFileSync("/etc/ssl/peer.cert.pem", "utf8");
1340
+ * var deny = ["DEADBEEF:CAFEBABE:1234:5678:..."];
1341
+ * var revoked = b.crypto.isCertRevoked(pem, deny);
1342
+ * // → false (when the fingerprint is not in the deny list)
1343
+ */
625
1344
  function isCertRevoked(pemOrDer, denyList) {
626
1345
  if (!Array.isArray(denyList)) {
627
1346
  throw new TypeError("crypto.isCertRevoked: denyList must be an array of fingerprint strings");
@@ -654,6 +1373,9 @@ module.exports = {
654
1373
  // Hashing
655
1374
  sha3Hash: sha3Hash,
656
1375
  hmacSha3: hmacSha3,
1376
+ hashFile: hashFile,
1377
+ hashStream: hashStream,
1378
+ namespaceHash: namespaceHash,
657
1379
  kdf: kdf,
658
1380
  // Comparison
659
1381
  timingSafeEqual: timingSafeEqual,