@blamejs/core 0.8.43 → 0.8.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -1,34 +1,66 @@
1
1
  "use strict";
2
2
  /**
3
- * Vault — sealed keystore for the framework's encryption keys.
3
+ * @module b.vault
4
+ * @featured true
5
+ * @nav Crypto
6
+ * @title Vault
4
7
  *
5
- * Holds the ML-KEM-1024 + P-384 hybrid keypair used by every other framework
6
- * subsystem that calls vault.seal() / vault.unseal() (db field encryption,
7
- * session storage, audit log signing, etc.). Keys never leave the process
8
- * after init() in any decrypted form except via the vault.seal/unseal API.
8
+ * @intro
9
+ * Sealed keystore that anchors every other framework subsystem holding
10
+ * secrets at rest: db field encryption, encrypted session storage,
11
+ * audit-log signing keys, OAuth refresh tokens, anything that flows
12
+ * through `b.vault.seal` / `b.vault.unseal`. The vault is the single
13
+ * trust root for the framework — rotate it and everything sealed under
14
+ * the old keys re-seals as part of the same operation.
9
15
  *
10
- * Modes (default is 'wrapped' highest-security; 'plaintext' is opt-out
11
- * with explicit boot warning per the framework's modernity stance):
16
+ * Keys held: an ML-KEM-1024 + ECDH P-384 hybrid keypair plus a
17
+ * per-deployment derivedHash salt. After `init()` the keypair never
18
+ * leaves the process in any decrypted form except via the seal /
19
+ * unseal API.
12
20
  *
13
- * wrapped vault.key.sealed file, passphrase-derived AEAD wrap (lib/vault-wrap.js).
14
- * Argon2id SHAKE256 XChaCha20-Poly1305. Default.
15
- * plaintext — vault.key file (JSON, mode 0o600). For development only.
16
- * Emits console.warn at boot. Opt-out only.
21
+ * Modes (`wrapped` is the default; `plaintext` is opt-out with an
22
+ * explicit boot warning per the framework's modernity stance):
17
23
  *
18
- * Two-API contract (sync seal/unseal, async init):
24
+ * - `wrapped` — `vault.key.sealed` file, passphrase-derived AEAD
25
+ * wrap (Argon2id → SHAKE256 → XChaCha20-Poly1305).
26
+ * The plaintext keypair never lands on disk.
27
+ * - `plaintext` — `vault.key` JSON at mode `0o600`. Development only.
28
+ * Emits a `console.warn` at every boot.
19
29
  *
20
- * await vault.init({ dataDir, mode? }) call once at app bootstrap
21
- * vault.seal(value) ← sync, post-init
22
- * vault.unseal(value) sync, post-init
30
+ * Two-API contract: bootstrap awaits `init()` once, and every other
31
+ * consumer (often at module-require time across hundreds of call
32
+ * sites) runs synchronously against the in-process key cache.
23
33
  *
24
- * Why two APIs: seal/unseal have hundreds of call sites across a typical app,
25
- * many at module-require time. Making them async would require an invasive
26
- * refactor of every consumer. Instead, the bootstrap awaits init() once, then
27
- * everything runs synchronously against the in-process key cache.
34
+ * ```js
35
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
36
+ * var sealed = b.vault.seal("4111-1111-1111-1111");
37
+ * sealed.startsWith("vault:"); // true
38
+ * b.vault.unseal(sealed); // → "4111-1111-1111-1111"
39
+ * ```
28
40
  *
29
- * Sealed-value format: "vault:" prefix + base64 envelope from lib/crypto.js.
30
- * Old envelopes always remain readable (envelope versioning); new writes use
31
- * the active KEM/CIPHER/KDF.
41
+ * Rotating the KEK (passphrase change, sealed-blob refresh,
42
+ * hardware-token swap) is a separate primitive `b.vaultRotate.rotate`
43
+ * walks every sealed column under the old keypair and re-seals it
44
+ * under the new one with batched commits and a round-trip verify.
45
+ * The vault module owns the in-process cache; the rotator owns the
46
+ * on-disk data sweep.
47
+ *
48
+ * ```js
49
+ * // Wrapped-mode bootstrap (first run): the vault generates an
50
+ * // ML-KEM-1024 + P-384 keypair, wraps it under the operator's
51
+ * // passphrase, and writes vault.key.sealed atomically.
52
+ * process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
53
+ * await b.vault.init({ dataDir: "/var/lib/blamejs" });
54
+ * b.vault.getMode(); // → "wrapped"
55
+ * ```
56
+ *
57
+ * Sealed-value format: `"vault:"` prefix + base64 envelope produced
58
+ * by `b.crypto.encrypt`. Old envelopes always remain readable
59
+ * (envelope versioning); new writes use whichever KEM / CIPHER / KDF
60
+ * the active framework version pins as default.
61
+ *
62
+ * @card
63
+ * Sealed keystore that anchors every other framework subsystem holding secrets at rest: db field encryption, encrypted session storage, audit-log signing keys, OAuth refresh tokens, anything that flows through `b.vault.seal` / `b.vault.unseal`.
32
64
  */
33
65
  var fs = require("fs");
34
66
  var path = require("path");
@@ -102,6 +134,40 @@ function _readOrCreateDerivedHashSalt() {
102
134
  }
103
135
 
104
136
  var _cachedDerivedHashSalt = null;
137
+ /**
138
+ * @primitive b.vault.getDerivedHashSalt
139
+ * @signature b.vault.getDerivedHashSalt()
140
+ * @since 0.8.42
141
+ * @related b.vault.init, b.vault.seal
142
+ *
143
+ * Returns the 32-byte per-deployment salt used by crypto-field's
144
+ * derivedHash columns. The salt is generated once on first init,
145
+ * persisted at `vault.derived-hash-salt` (mode `0o600`) inside
146
+ * `dataDir`, and read back on subsequent boots. It survives vault
147
+ * KEK rotations — different file from `vault.key.sealed` — so
148
+ * indexed-lookup determinism for derivedHash columns holds across a
149
+ * passphrase change.
150
+ *
151
+ * Why per-deployment: pre-v0.8.42 the deterministic
152
+ * `sha3(namespace + plaintext)` shape allowed cross-deployment
153
+ * rainbow tables and cross-table correlation between deployments
154
+ * sharing a namespace. Binding a 32-byte salt closes that class
155
+ * without losing the determinism inside a single deployment that
156
+ * makes the index lookup possible.
157
+ *
158
+ * Throws `VaultError("vault/not-initialized")` if `init()` has not
159
+ * been awaited yet. Throws `vault/derived-hash-salt-corrupted` if
160
+ * the on-disk file exists but is not exactly 32 bytes.
161
+ *
162
+ * @example
163
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
164
+ * var salt = b.vault.getDerivedHashSalt();
165
+ * salt.length; // → 32
166
+ * Buffer.isBuffer(salt); // → true
167
+ *
168
+ * // Same value on every call within a process — cached.
169
+ * b.vault.getDerivedHashSalt() === salt; // → true
170
+ */
105
171
  function getDerivedHashSalt() {
106
172
  if (_cachedDerivedHashSalt === null) {
107
173
  _cachedDerivedHashSalt = _readOrCreateDerivedHashSalt();
@@ -111,6 +177,55 @@ function getDerivedHashSalt() {
111
177
 
112
178
  // ---- Init dispatch ----
113
179
 
180
+ /**
181
+ * @primitive b.vault.init
182
+ * @signature b.vault.init(opts)
183
+ * @since 0.1.0
184
+ * @related b.vault.seal, b.vault.unseal, b.vault.getMode, b.vaultRotate.rotate
185
+ *
186
+ * Bootstraps the vault. Call once at application startup before any
187
+ * code path that reads sealed values from the database, opens the
188
+ * encrypted session store, or signs audit-log entries. Subsequent
189
+ * calls after a successful init are no-ops, so guard-rail wrappers
190
+ * that re-call `init()` from worker entry points are safe.
191
+ *
192
+ * Mode dispatch:
193
+ *
194
+ * - `wrapped` (default) — if `vault.key.sealed` exists, prompts for the
195
+ * passphrase via `b.vaultPassphraseSource` and unwraps. If neither
196
+ * sealed nor plaintext file is present, generates a fresh keypair
197
+ * and wraps it under a freshly-prompted passphrase.
198
+ * - `plaintext` — reads `vault.key` if present, generates a fresh
199
+ * keypair and writes it at mode `0o600` otherwise. Logs a `WARNING`
200
+ * line at every boot.
201
+ *
202
+ * Refuses to guess when both `vault.key` and `vault.key.sealed` exist
203
+ * in `dataDir`, or when the requested mode mismatches the on-disk
204
+ * shape (sealed file present but `mode: "plaintext"` requested, or
205
+ * vice versa). Throws a `VaultError` in either case so the bootstrap
206
+ * exits cleanly instead of silently picking one.
207
+ *
208
+ * @opts
209
+ * {
210
+ * dataDir: string, // required — directory holding vault.key /
211
+ * // vault.key.sealed / derived-hash-salt
212
+ * mode: string, // "wrapped" (default) | "plaintext"
213
+ * }
214
+ *
215
+ * @example
216
+ * // Wrapped-mode bootstrap with passphrase from the env var
217
+ * // b.vaultPassphraseSource consults by default.
218
+ * process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
219
+ * await b.vault.init({
220
+ * dataDir: "/var/lib/blamejs",
221
+ * mode: "wrapped",
222
+ * });
223
+ * b.vault.getMode(); // → "wrapped"
224
+ *
225
+ * // Re-calling init() after a successful boot is a no-op.
226
+ * await b.vault.init({ dataDir: "/var/lib/blamejs" });
227
+ * b.vault.getMode(); // → "wrapped"
228
+ */
114
229
  async function init(opts) {
115
230
  if (initialized) return;
116
231
  opts = opts || {};
@@ -296,6 +411,43 @@ function _requireInit() {
296
411
  }
297
412
  }
298
413
 
414
+ /**
415
+ * @primitive b.vault.seal
416
+ * @signature b.vault.seal(plaintext)
417
+ * @since 0.1.0
418
+ * @related b.vault.unseal, b.vaultRotate.rotate
419
+ *
420
+ * Synchronously encrypts `plaintext` under the in-process keypair and
421
+ * returns a `"vault:"`-prefixed string suitable for storage in any
422
+ * column declared sealed in the field-crypto schema. Called from
423
+ * hundreds of call sites across a typical application — keep it sync.
424
+ *
425
+ * Idempotent on already-sealed input: a value that already starts
426
+ * with the vault prefix is returned unchanged so seal-on-write paths
427
+ * survive code that re-seals the same row twice. Empty / falsy input
428
+ * passes through verbatim — there's nothing to encrypt and the
429
+ * caller likely meant `null` to land in the column.
430
+ *
431
+ * Throws `VaultError("vault/not-initialized")` if `init()` has not
432
+ * been awaited yet — the seal/unseal API is sync, but the keypair
433
+ * cache it consults is populated by the async init.
434
+ *
435
+ * Sealed values from this primitive decrypt regardless of which
436
+ * row / column / table they came from. Use `b.vault.aad.seal` for
437
+ * AEAD-bound seals when copy-paste between rows is part of the
438
+ * threat model.
439
+ *
440
+ * @example
441
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
442
+ * var sealed = b.vault.seal("4111-1111-1111-1111");
443
+ * sealed.indexOf("vault:"); // → 0
444
+ *
445
+ * // Idempotent: re-sealing returns the input unchanged.
446
+ * b.vault.seal(sealed) === sealed; // → true
447
+ *
448
+ * // Falsy input is passed through verbatim.
449
+ * b.vault.seal("") === ""; // → true
450
+ */
299
451
  function seal(plaintext) {
300
452
  if (!plaintext) return plaintext;
301
453
  if (String(plaintext).startsWith(VAULT_PREFIX)) return plaintext;
@@ -305,6 +457,37 @@ function seal(plaintext) {
305
457
  });
306
458
  }
307
459
 
460
+ /**
461
+ * @primitive b.vault.unseal
462
+ * @signature b.vault.unseal(value)
463
+ * @since 0.1.0
464
+ * @related b.vault.seal
465
+ *
466
+ * Synchronously decrypts a `"vault:"`-prefixed string produced by
467
+ * `b.vault.seal` and returns the plaintext. Idempotent on
468
+ * non-sealed input: a value that does not start with the vault
469
+ * prefix is returned unchanged so read paths that select a column
470
+ * before knowing whether it's sealed don't have to branch.
471
+ *
472
+ * The envelope inside the prefix is versioned — values sealed under
473
+ * older KEM / KDF / cipher choices remain readable across framework
474
+ * upgrades. New seals always use the active algorithm set, so a
475
+ * full read-write cycle migrates a row forward.
476
+ *
477
+ * Throws `VaultError("vault/not-initialized")` if `init()` has not
478
+ * been awaited yet. Throws on AEAD-tag failure (corrupted ciphertext,
479
+ * wrong keypair) — operators rotating keys validate the rotation
480
+ * via `b.vaultRotate.verify` rather than catching here.
481
+ *
482
+ * @example
483
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
484
+ * var sealed = b.vault.seal("hello");
485
+ * b.vault.unseal(sealed); // → "hello"
486
+ *
487
+ * // Non-sealed input passes through unchanged.
488
+ * b.vault.unseal("plain-string"); // → "plain-string"
489
+ * b.vault.unseal(null); // → null
490
+ */
308
491
  function unseal(value) {
309
492
  if (!value || !String(value).startsWith(VAULT_PREFIX)) return value;
310
493
  _requireInit();
@@ -314,15 +497,94 @@ function unseal(value) {
314
497
  });
315
498
  }
316
499
 
500
+ /**
501
+ * @primitive b.vault.getKeysJson
502
+ * @signature b.vault.getKeysJson()
503
+ * @since 0.6.0
504
+ * @related b.vault.init, b.vaultRotate.rotate
505
+ *
506
+ * Returns the in-process keypair as a pretty-printed JSON string —
507
+ * the same shape that lives on disk for `mode: "plaintext"` and
508
+ * inside the wrapped envelope for `mode: "wrapped"`. Used by the
509
+ * rotation pipeline to feed `oldKeys` into a fresh
510
+ * `b.vaultRotate.rotate({ oldKeys, newKeys, ... })` call without
511
+ * round-tripping through disk.
512
+ *
513
+ * The returned JSON has four properties: `publicKey`, `privateKey`
514
+ * (ML-KEM-1024), `ecPublicKey`, `ecPrivateKey` (P-384). Operators
515
+ * routing this through structured logging or telemetry must redact
516
+ * — these are the production keys, not metadata.
517
+ *
518
+ * Throws `VaultError("vault/not-initialized")` if `init()` has not
519
+ * been awaited yet.
520
+ *
521
+ * @example
522
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
523
+ * var json = b.vault.getKeysJson();
524
+ * var keys = JSON.parse(json);
525
+ * Object.keys(keys).sort().join(",");
526
+ * // → "ecPrivateKey,ecPublicKey,privateKey,publicKey"
527
+ */
317
528
  function getKeysJson() {
318
529
  _requireInit();
319
530
  return JSON.stringify(keys, null, 2);
320
531
  }
321
532
 
533
+ /**
534
+ * @primitive b.vault.getCurrentPassphrase
535
+ * @signature b.vault.getCurrentPassphrase()
536
+ * @since 0.6.0
537
+ * @related b.vault.init, b.vaultPassphraseOps.changePassphrase, b.vaultRotate.rotate
538
+ *
539
+ * Returns the Buffer holding the passphrase the vault was unsealed
540
+ * with on this boot, or `null` for `mode: "plaintext"` and for
541
+ * any future scenario where the vault was bootstrapped without
542
+ * one. Used by passphrase-rotation flows that re-wrap the keypair
543
+ * under a fresh passphrase without prompting the operator twice.
544
+ *
545
+ * The Buffer is already in the JS heap during unwrap; retaining it
546
+ * does not change the threat model meaningfully and is what makes
547
+ * `b.vaultPassphraseOps.changePassphrase` ergonomic. Operators
548
+ * concerned about heap residency rotate the passphrase and let the
549
+ * old Buffer get zeroed and replaced.
550
+ *
551
+ * @example
552
+ * process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
553
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
554
+ * var pass = b.vault.getCurrentPassphrase();
555
+ * Buffer.isBuffer(pass); // → true
556
+ * pass.toString("utf8"); // → "S0meStrongPassphr@se!"
557
+ *
558
+ * // Plaintext mode never holds a passphrase.
559
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
560
+ * b.vault.getCurrentPassphrase(); // → null
561
+ */
322
562
  function getCurrentPassphrase() {
323
563
  return currentPassphrase;
324
564
  }
325
565
 
566
+ /**
567
+ * @primitive b.vault.getMode
568
+ * @signature b.vault.getMode()
569
+ * @since 0.6.0
570
+ * @related b.vault.init
571
+ *
572
+ * Returns the active vault mode: `"wrapped"`, `"plaintext"`, or
573
+ * `null` before `init()` has been awaited. Useful from health-check
574
+ * endpoints that surface a deployment-posture badge ("plaintext mode
575
+ * — DEV ONLY") or refuse to start the public listener until the
576
+ * vault is in `wrapped` mode in production.
577
+ *
578
+ * @example
579
+ * b.vault.getMode(); // → null (pre-init)
580
+ *
581
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
582
+ * b.vault.getMode(); // → "wrapped"
583
+ *
584
+ * if (process.env.NODE_ENV === "production" && b.vault.getMode() !== "wrapped") {
585
+ * throw new Error("refusing to start: vault must be in wrapped mode");
586
+ * }
587
+ */
326
588
  function getMode() {
327
589
  return currentMode;
328
590
  }
@@ -90,6 +90,72 @@ var DEFAULT_POLL_MS = 500;
90
90
  // opts.maxSourceBytes.
91
91
  var DEFAULT_MAX_SOURCE_BYTES = C.BYTES.mib(1);
92
92
 
93
+ /**
94
+ * @primitive b.vault.sealPemFile
95
+ * @signature b.vault.sealPemFile(opts)
96
+ * @since 0.8.42
97
+ * @related b.vault.seal, b.vault.init, b.vaultRotate.rotate
98
+ *
99
+ * Watches a plaintext PEM file (typically certbot's
100
+ * `/etc/letsencrypt/live/<domain>/privkey.pem` after an ACME renewal)
101
+ * and re-seals it to a destination path under the vault keypair on
102
+ * every mtime / size change. Closes the renewal-window gap where a
103
+ * fresh PEM lives unencrypted on disk between certbot's write and
104
+ * the next operator-driven re-seal.
105
+ *
106
+ * Crash-safe write protocol: write `<destination>.tmp` at mode
107
+ * `0o600`, fsync, create a `<destination>.rewriting` marker, atomic
108
+ * rename, fsync the destination directory, remove the marker. If
109
+ * the framework crashes between marker create and marker remove,
110
+ * the next `sealPemFile()` start re-seals from source idempotently.
111
+ *
112
+ * Refuses to seal in place (source === destination), refuses to
113
+ * follow a symlinked source (TOCTOU defense), and refuses when the
114
+ * destination's parent directory is group- or other-writable on
115
+ * POSIX. Source size is capped (`maxSourceBytes`, default 1 MiB)
116
+ * so an attacker with write access to source can't OOM the host
117
+ * with a 10 GiB file.
118
+ *
119
+ * Returns a watcher handle: `start` (auto-called by the constructor
120
+ * unless overridden), `stop`, `forceReseal({ actorId, reason })`,
121
+ * plus read-only `generation` / `lastResealedAt` / `lastError` /
122
+ * `watching` properties.
123
+ *
124
+ * @opts
125
+ * {
126
+ * source: string, // plaintext PEM path (required)
127
+ * destination: string, // sealed-output path (required, must differ from source)
128
+ * audit: boolean, // emit b.audit events on every reseal (default true)
129
+ * pollInterval: number, // fs.watchFile cadence in ms (default 500)
130
+ * onResealed: function, // (info) => void — { srcPath, destPath, bytes, resealedAt, generation }
131
+ * onError: function, // (err) => void — sealing failed
132
+ * maxSourceBytes: number, // refuse source larger than this (default 1 MiB)
133
+ * }
134
+ *
135
+ * @example
136
+ * await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
137
+ *
138
+ * var watcher = b.vault.sealPemFile({
139
+ * source: "/etc/letsencrypt/live/example.com/privkey.pem",
140
+ * destination: "/var/lib/blamejs/server.key.sealed",
141
+ * pollInterval: b.constants.TIME.seconds(2),
142
+ * onResealed: function (info) {
143
+ * console.log("resealed", info.bytes, "bytes, gen", info.generation);
144
+ * },
145
+ * onError: function (err) {
146
+ * console.error("reseal failed:", err.message);
147
+ * },
148
+ * });
149
+ *
150
+ * watcher.generation; // → 1 (initial seal completed)
151
+ * typeof watcher.lastResealedAt; // → "number"
152
+ *
153
+ * // Force a reseal after a manual ACME renewal — captured in audit.
154
+ * watcher.forceReseal({ actorId: "ops-bot", reason: "manual-renewal" });
155
+ *
156
+ * // Stop watching at shutdown.
157
+ * watcher.stop();
158
+ */
93
159
  function sealPemFile(opts) {
94
160
  opts = opts || {};
95
161
  validateOpts(opts, [