@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,35 +1,42 @@
1
1
  "use strict";
2
2
  /**
3
- * Atomic file I/O with integrity verification, retry on transient errors,
4
- * and cross-process locking.
5
- *
6
- * The framework already does atomic writes for vault.key.sealed (lib/vault.js)
7
- * and audit.tip (lib/db.js). This module exposes the same primitives for
8
- * any caller that needs:
9
- *
10
- * - Crash-safe writes via temp + fsync + atomic rename + dir fsync
11
- * - Optional integrity hash (SHA3-512) computed on write, verified on read
12
- * - Retry on EBUSY / EAGAIN / ENFILE with exponential backoff
13
- * - Cross-process locking for read-modify-write sequences
14
- * - JSON convenience wrappers using b.json's security defaults
15
- *
16
- * The framework's "fail closed" stance applies: a partially-written file
17
- * NEVER survives a crash to the caller either the new contents are
18
- * fully on disk (atomic rename succeeded) or the original (or absence)
19
- * remains. fsync calls are best-effort across platforms (Windows rejects
20
- * directory fsync, etc.); the rename remains atomic at the FS level
21
- * regardless.
22
- *
23
- * Public API:
24
- * atomicFile.write(filepath, data, opts?) → { bytesWritten, hash? }
25
- * atomicFile.read(filepath, opts?) → Buffer (or string if encoding)
26
- * atomicFile.readSync(filepath, opts?) → same, sync (for boot paths)
27
- * atomicFile.writeJson(filepath, value, opts?) → { bytesWritten, hash? }
28
- * atomicFile.readJson(filepath, opts?) → parsed value
29
- * atomicFile.copy(src, dst, opts?) → { bytesWritten, hash? }
30
- * atomicFile.exists(filepath) → boolean
31
- * atomicFile.lock(filepath, fn, opts?) → fn's return value
32
- * atomicFile.AtomicFileError → error class
3
+ * @module b.atomicFile
4
+ * @nav Data
5
+ * @title Atomic File
6
+ *
7
+ * @intro
8
+ * Atomic file I/O with integrity verification, retry on transient
9
+ * errors, and cross-process locking.
10
+ *
11
+ * Every write goes through the same crash-safe sequence:
12
+ * 1. write payload to a sibling temp file (`<filepath>.tmp-<token>`)
13
+ * 2. fsync the file descriptor before close
14
+ * 3. fs.rename() the temp file over the destination — POSIX rename
15
+ * is atomic on the same filesystem; on Windows, fs.rename uses
16
+ * MoveFileEx with REPLACE_EXISTING for the same guarantee
17
+ * 4. fsync the parent directory so the rename itself is durable
18
+ *
19
+ * Result: a partially-written file NEVER survives a crash to the
20
+ * caller. Either the new contents are fully on disk (rename
21
+ * succeeded) or the original (or absence) remains. No torn writes,
22
+ * no half-flushed pages.
23
+ *
24
+ * fsync calls are best-effort across platforms — Windows rejects
25
+ * directory fsync, some FUSE filesystems no-op file fsync — but the
26
+ * rename remains atomic at the FS level regardless. The framework
27
+ * already uses this primitive internally for vault.key.sealed and
28
+ * audit.tip; this module exposes the same surface for any caller
29
+ * that needs durable write-replace semantics.
30
+ *
31
+ * Optional `computeHash: true` returns SHA3-512 over the written
32
+ * bytes; passing the same digest as `expectedHash` on a later read
33
+ * gates retrieval on integrity. Transient FS errors (EBUSY / EAGAIN /
34
+ * ENFILE / EMFILE / EPERM) retry with exponential backoff via
35
+ * b.retry.withRetry — sync paths skip the loop because they can't
36
+ * usefully await a backoff.
37
+ *
38
+ * @card
39
+ * Atomic file I/O with integrity verification, retry on transient errors, and cross-process locking.
33
40
  */
34
41
  var fs = require("fs");
35
42
  var path = require("path");
@@ -100,10 +107,44 @@ async function _withRetry(fn, opts) {
100
107
  // bundle, restore-rollback, bundler) duplicated them inline. Hoisted
101
108
  // here so the framework has one shared implementation per concern.
102
109
 
110
+ /**
111
+ * @primitive b.atomicFile.fsync
112
+ * @signature b.atomicFile.fsync(fd)
113
+ * @since 0.7.0
114
+ * @status stable
115
+ * @related b.atomicFile.fsyncDir, b.atomicFile.write
116
+ *
117
+ * Best-effort fs.fsyncSync wrapper. Silently swallows errors because
118
+ * not every platform / fd type supports fsync (some FUSE mounts, some
119
+ * device fds). Use this when you want the durability hint but don't
120
+ * want a non-fsyncable target to crash the caller.
121
+ *
122
+ * @example
123
+ * var fs = require("fs");
124
+ * var fd = fs.openSync("/tmp/note.txt", "w");
125
+ * fs.writeSync(fd, "hello\n");
126
+ * b.atomicFile.fsync(fd);
127
+ * fs.closeSync(fd);
128
+ */
103
129
  function fsync(fd) {
104
130
  try { fs.fsyncSync(fd); } catch (_e) { /* not all platforms support fsync on every fd type */ }
105
131
  }
106
132
 
133
+ /**
134
+ * @primitive b.atomicFile.fsyncDir
135
+ * @signature b.atomicFile.fsyncDir(dirPath)
136
+ * @since 0.7.0
137
+ * @status stable
138
+ * @related b.atomicFile.fsync, b.atomicFile.write
139
+ *
140
+ * Best-effort fsync of a directory inode. Required after a rename to
141
+ * make the directory entry itself durable on POSIX filesystems.
142
+ * Windows refuses directory fsync — the call is wrapped so the caller
143
+ * can run the same code on every platform without branching.
144
+ *
145
+ * @example
146
+ * b.atomicFile.fsyncDir("/var/lib/blamejs/data");
147
+ */
107
148
  function fsyncDir(dirPath) {
108
149
  try {
109
150
  var fd = fs.openSync(dirPath, "r");
@@ -116,9 +157,25 @@ function fsyncDir(dirPath) {
116
157
  function _fsync(fd) { return fsync(fd); }
117
158
  function _fsyncDir(dirPath) { return fsyncDir(dirPath); }
118
159
 
119
- // ensureDir — mkdirSync with recursive: true and a default mode of
120
- // 0o700 (owner-only) suitable for framework data directories. Caller
121
- // passes a different mode for less-restricted dirs.
160
+ /**
161
+ * @primitive b.atomicFile.ensureDir
162
+ * @signature b.atomicFile.ensureDir(dirPath, mode)
163
+ * @since 0.7.0
164
+ * @status stable
165
+ * @related b.atomicFile.write, b.atomicFile.copyDirRecursive
166
+ *
167
+ * Create `dirPath` (recursive) with a chosen mode. Default mode is
168
+ * 0o700 — owner-only — suitable for framework data directories
169
+ * holding sealed vaults, audit chains, or session state. Returns the
170
+ * dirPath unchanged so calls compose into path-building chains.
171
+ *
172
+ * @example
173
+ * var dir = b.atomicFile.ensureDir("/var/lib/blamejs/audit", 0o700);
174
+ * // → "/var/lib/blamejs/audit"
175
+ *
176
+ * // Less-restricted dir for a public asset folder:
177
+ * b.atomicFile.ensureDir("/var/www/uploads", 0o755);
178
+ */
122
179
  function ensureDir(dirPath, mode) {
123
180
  if (typeof dirPath !== "string" || dirPath.length === 0) {
124
181
  throw new AtomicFileError("ensureDir: path must be a non-empty string", "atomic-file/bad-path");
@@ -127,11 +184,32 @@ function ensureDir(dirPath, mode) {
127
184
  return dirPath;
128
185
  }
129
186
 
130
- // copyDirRecursive — synchronous, file-by-file copy that mirrors the
131
- // source's directory structure. Skips symlinks (operator wanting symlink
132
- // preservation should use a real archive tool). Refuses to overwrite
133
- // existing files at dest by default — pass opts.overwrite=true to
134
- // replace. dest is created with mode 0o700 by default.
187
+ /**
188
+ * @primitive b.atomicFile.copyDirRecursive
189
+ * @signature b.atomicFile.copyDirRecursive(src, dest, opts)
190
+ * @since 0.7.0
191
+ * @status stable
192
+ * @related b.atomicFile.copy, b.atomicFile.ensureDir
193
+ *
194
+ * Synchronous, file-by-file copy that mirrors the source directory
195
+ * structure. Skips symlinks, sockets, and devices — operators wanting
196
+ * symlink preservation should use a real archive tool. Refuses to
197
+ * overwrite existing files at dest by default; pass `overwrite: true`
198
+ * to replace. The dest tree is created with mode 0o700 by default
199
+ * (override with `dirMode`). Returns `{ fileCount, byteCount }`.
200
+ *
201
+ * @opts
202
+ * overwrite: false, // when true, overwrite files that already exist at dest
203
+ * dirMode: 0o700, // mode for newly-created destination directories
204
+ *
205
+ * @example
206
+ * var stats = b.atomicFile.copyDirRecursive(
207
+ * "/var/lib/blamejs/data",
208
+ * "/var/lib/blamejs/snapshot-2026-01-01",
209
+ * { overwrite: false, dirMode: 0o700 }
210
+ * );
211
+ * // → { fileCount: 42, byteCount: 1048576 }
212
+ */
135
213
  function copyDirRecursive(src, dest, opts) {
136
214
  if (typeof src !== "string" || src.length === 0) {
137
215
  throw new AtomicFileError("copyDirRecursive: src must be a non-empty string", "atomic-file/bad-path");
@@ -169,32 +247,59 @@ function copyDirRecursive(src, dest, opts) {
169
247
  return { fileCount: fileCount, byteCount: byteCount };
170
248
  }
171
249
 
172
- // pathTimestamp — filesystem-safe ISO-8601 timestamp suitable for use as
173
- // a directory or file name on every platform. Standard
174
- // Date.toISOString() embeds ':' and '.' which Windows reserves for
175
- // drive letters and extension separators. This helper substitutes both
176
- // with '-' so the result works as a path segment unmodified. String
177
- // sort still gives chronological order.
178
- //
179
- // atomicFile.pathTimestamp()
180
- // → "2026-04-27T14-00-00-123Z"
181
- // atomicFile.pathTimestamp(new Date(0))
182
- // → "1970-01-01T00-00-00-000Z"
250
+ /**
251
+ * @primitive b.atomicFile.pathTimestamp
252
+ * @signature b.atomicFile.pathTimestamp(date)
253
+ * @since 0.7.0
254
+ * @status stable
255
+ * @related b.atomicFile.ensureDir, b.atomicFile.write
256
+ *
257
+ * Filesystem-safe ISO-8601 timestamp. Standard Date.toISOString()
258
+ * embeds ':' and '.' which Windows reserves for drive letters and
259
+ * extension separators; this helper substitutes both with '-' so the
260
+ * result is portable as a path segment. String sort still gives
261
+ * chronological order. Pass a Date to format a specific instant;
262
+ * omit it for `new Date()`.
263
+ *
264
+ * @example
265
+ * var stamp = b.atomicFile.pathTimestamp(new Date(0));
266
+ * // → "1970-01-01T00-00-00-000Z"
267
+ *
268
+ * var fixed = b.atomicFile.pathTimestamp(new Date(Date.UTC(2026, 0, 1)));
269
+ * // → "2026-01-01T00-00-00-000Z"
270
+ */
183
271
  function pathTimestamp(date) {
184
272
  var d = (date instanceof Date) ? date : new Date();
185
273
  return d.toISOString().replace(/[:.]/g, "-");
186
274
  }
187
275
 
188
- // ---- writeSync ----
189
- // Synchronous atomic write — same temp+fsync+rename+dirfsync flow as
190
- // async write(), but without the retry loop (which requires awaits).
191
- // Use this from sync code paths (process exit handlers, module-load-time
192
- // bootstraps). For everything else, prefer the async write().
193
- //
194
- // Transactional guarantee: either the rename completes (new contents fully
195
- // visible) or the tmp file is removed (no state change). The caller never
196
- // sees a half-written file at `filepath` and never leaves a tmp orphan
197
- // from the current call's failure path.
276
+ /**
277
+ * @primitive b.atomicFile.writeSync
278
+ * @signature b.atomicFile.writeSync(filepath, data, opts)
279
+ * @since 0.7.0
280
+ * @status stable
281
+ * @related b.atomicFile.write, b.atomicFile.cleanOrphans
282
+ *
283
+ * Synchronous atomic write same temp + fsync + rename + dirfsync
284
+ * sequence as the async `write`, but without the retry loop (which
285
+ * requires awaits). Use from sync-only code paths: process exit
286
+ * handlers, module-load-time bootstraps, signal handlers. For
287
+ * everything else, prefer the async form. Either the rename
288
+ * completes (new contents fully visible) or the tmp file is removed —
289
+ * no half-written file ever appears at `filepath`.
290
+ *
291
+ * @opts
292
+ * fileMode: 0o600, // mode applied to the temp file (and thus the renamed final)
293
+ * computeHash: false, // when true, return SHA3-512 of the written bytes
294
+ *
295
+ * @example
296
+ * var result = b.atomicFile.writeSync(
297
+ * "/var/lib/blamejs/state.bin",
298
+ * Buffer.from("payload"),
299
+ * { fileMode: 0o600, computeHash: true }
300
+ * );
301
+ * // → { bytesWritten: 7, hash: "<sha3-512 hex>" }
302
+ */
198
303
  function writeSync(filepath, data, opts) {
199
304
  opts = Object.assign({}, DEFAULTS, opts || {});
200
305
  var buf = safeBuffer.toBuffer(data, {
@@ -236,14 +341,31 @@ function writeSync(filepath, data, opts) {
236
341
  };
237
342
  }
238
343
 
239
- // Clean up orphan tmp files left behind by a previously-crashed process.
240
- // Atomic writes use random tmp names (filepath + ".tmp-" + token), so a
241
- // crash leaves a file with a name we can't predict on next boot — only
242
- // glob and prune by age. Default: prune anything older than 5 minutes.
243
- //
244
- // Operators should call this at boot for every "important" filepath
245
- // (vault.key.sealed, audit-sign.key.sealed, db.enc, etc.) BEFORE they
246
- // start their first atomic write to that path.
344
+ /**
345
+ * @primitive b.atomicFile.cleanOrphans
346
+ * @signature b.atomicFile.cleanOrphans(filepath, opts)
347
+ * @since 0.7.0
348
+ * @status stable
349
+ * @related b.atomicFile.write, b.atomicFile.writeSync
350
+ *
351
+ * Sweep orphan temp files left behind by a previously-crashed
352
+ * process. Atomic writes use random temp names (`<filepath>.tmp-<token>`),
353
+ * so a crashed run leaves a file with a name the next boot can't
354
+ * predict — only glob-by-prefix and prune by age. Operators should
355
+ * call this at boot for every "important" filepath (vault.key.sealed,
356
+ * audit-sign.key.sealed, db.enc, ...) BEFORE the first atomic write
357
+ * to that path. Returns the number of orphans removed.
358
+ *
359
+ * @opts
360
+ * olderThanMs: 300000, // only prune temp files older than this many ms (default 5 minutes)
361
+ *
362
+ * @example
363
+ * var removed = b.atomicFile.cleanOrphans(
364
+ * "/var/lib/blamejs/vault.key.sealed",
365
+ * { olderThanMs: 300000 }
366
+ * );
367
+ * // → 0 (no orphans found, or the count of files unlinked)
368
+ */
247
369
  function cleanOrphans(filepath, opts) {
248
370
  opts = opts || {};
249
371
  var olderThanMs = opts.olderThanMs != null ? opts.olderThanMs : C.TIME.minutes(5);
@@ -268,8 +390,39 @@ function cleanOrphans(filepath, opts) {
268
390
  return removed;
269
391
  }
270
392
 
271
- // ---- write ----
272
-
393
+ /**
394
+ * @primitive b.atomicFile.write
395
+ * @signature b.atomicFile.write(filepath, data, opts)
396
+ * @since 0.7.0
397
+ * @status stable
398
+ * @related b.atomicFile.writeSync, b.atomicFile.read, b.atomicFile.lock
399
+ *
400
+ * Crash-safe write-replace. Writes `data` to a sibling temp file,
401
+ * fsyncs the fd, atomically renames over `filepath`, then fsyncs the
402
+ * parent directory. On any failure path the temp is unlinked, so the
403
+ * destination is never seen as half-written. Transient FS errors
404
+ * (EBUSY / EAGAIN / ENFILE / EMFILE / EPERM) retry with exponential
405
+ * backoff. Returns `{ bytesWritten, hash }` where `hash` is null
406
+ * unless `computeHash: true`.
407
+ *
408
+ * @opts
409
+ * fileMode: 0o600, // mode applied to the renamed file
410
+ * computeHash: false, // SHA3-512 the written bytes; included in result
411
+ * retryAttempts: 5, // attempts before giving up on transient FS errors
412
+ * retryBaseMs: 50, // base backoff
413
+ * retryMaxMs: 2000, // backoff ceiling
414
+ * signal: AbortSignal | undefined, // abort the retry loop early
415
+ *
416
+ * @example
417
+ * async function persist() {
418
+ * var result = await b.atomicFile.write(
419
+ * "/var/lib/blamejs/state.bin",
420
+ * Buffer.from("payload"),
421
+ * { fileMode: 0o600, computeHash: true }
422
+ * );
423
+ * return result; // → { bytesWritten: 7, hash: "<sha3-512 hex>" }
424
+ * }
425
+ */
273
426
  async function write(filepath, data, opts) {
274
427
  opts = Object.assign({}, DEFAULTS, opts || {});
275
428
  var buf = safeBuffer.toBuffer(data, {
@@ -313,8 +466,47 @@ async function write(filepath, data, opts) {
313
466
  }, opts);
314
467
  }
315
468
 
316
- // ---- read ----
317
-
469
+ /**
470
+ * @primitive b.atomicFile.read
471
+ * @signature b.atomicFile.read(filepath, opts)
472
+ * @since 0.7.0
473
+ * @status stable
474
+ * @related b.atomicFile.readSync, b.atomicFile.write
475
+ *
476
+ * Read a file with size cap and optional integrity verification.
477
+ * `maxBytes` defaults to 64 MiB; values larger than the file's stat
478
+ * size throw `atomic-file/too-large` BEFORE the read happens (no
479
+ * memory-blow up on hostile inputs). When `expectedHash` is provided,
480
+ * the SHA3-512 of the bytes is compared and a mismatch throws
481
+ * `atomic-file/integrity`. Pass `encoding` to receive a decoded
482
+ * string instead of a Buffer. Retries on transient FS errors.
483
+ *
484
+ * @opts
485
+ * maxBytes: 67108864, // ceiling on file size; reject anything larger
486
+ * encoding: undefined, // when set (e.g. "utf8"), return a decoded string
487
+ * expectedHash: undefined, // SHA3-512 hex; when set, integrity-check the bytes
488
+ * retryAttempts: 5, // transient-error retry count
489
+ * retryBaseMs: 50,
490
+ * retryMaxMs: 2000,
491
+ * signal: AbortSignal | undefined,
492
+ *
493
+ * @example
494
+ * async function load() {
495
+ * var buf = await b.atomicFile.read(
496
+ * "/var/lib/blamejs/state.bin",
497
+ * { maxBytes: 1048576 }
498
+ * );
499
+ * return buf; // → <Buffer ...> (≤ 1 MiB)
500
+ * }
501
+ *
502
+ * // Integrity-checked read — pass the digest computed at write time:
503
+ * async function loadVerified(digestHex) {
504
+ * return await b.atomicFile.read(
505
+ * "/var/lib/blamejs/state.bin",
506
+ * { expectedHash: digestHex, encoding: "utf8" }
507
+ * );
508
+ * }
509
+ */
318
510
  async function read(filepath, opts) {
319
511
  opts = Object.assign({}, DEFAULTS, opts || {});
320
512
  return await _withRetry(function () {
@@ -325,11 +517,32 @@ async function read(filepath, opts) {
325
517
  }, opts);
326
518
  }
327
519
 
328
- // Sync variant for callers in module-init / boot paths that can't
329
- // `await` (vault.initPlaintext, audit-sign._initPlaintext,
330
- // db._checkRollback, db.loadOrCreateDbKey). Same semantics as
331
- // async read: size cap, optional integrity-hash verification, ENOENT
332
- // translation. No retry loop — sync paths can't usefully back off.
520
+ /**
521
+ * @primitive b.atomicFile.readSync
522
+ * @signature b.atomicFile.readSync(filepath, opts)
523
+ * @since 0.7.0
524
+ * @status stable
525
+ * @related b.atomicFile.read, b.atomicFile.writeSync
526
+ *
527
+ * Synchronous variant for callers in module-init / boot paths that
528
+ * can't await — vault unsealing, audit-sign init, DB rollback check.
529
+ * Same semantics as the async `read`: size cap via `maxBytes`,
530
+ * optional `expectedHash` integrity check, ENOENT translated to an
531
+ * AtomicFileError with `code === "ENOENT"`. No retry loop — sync
532
+ * paths can't usefully back off.
533
+ *
534
+ * @opts
535
+ * maxBytes: 67108864,
536
+ * encoding: undefined,
537
+ * expectedHash: undefined,
538
+ *
539
+ * @example
540
+ * var buf = b.atomicFile.readSync(
541
+ * "/var/lib/blamejs/vault.key.sealed",
542
+ * { maxBytes: 65536 }
543
+ * );
544
+ * // → <Buffer ...> (≤ 64 KiB)
545
+ */
333
546
  function readSync(filepath, opts) {
334
547
  opts = Object.assign({}, DEFAULTS, opts || {});
335
548
  return _readSyncCore(filepath, opts);
@@ -373,8 +586,37 @@ function _readSyncCore(filepath, opts) {
373
586
  return opts.encoding ? buf.toString(opts.encoding) : buf;
374
587
  }
375
588
 
376
- // ---- writeJson / readJson ----
377
-
589
+ /**
590
+ * @primitive b.atomicFile.writeJson
591
+ * @signature b.atomicFile.writeJson(filepath, value, opts)
592
+ * @since 0.7.0
593
+ * @status stable
594
+ * @related b.atomicFile.readJson, b.atomicFile.write
595
+ *
596
+ * Atomic JSON write. Serializes via `b.safeJson` (RFC 8785 canonical
597
+ * form when `canonical: true`, otherwise standard stringify with
598
+ * configurable indent) and routes through `b.atomicFile.write` for
599
+ * the same crash-safe semantics. Returns the same shape as `write`.
600
+ *
601
+ * @opts
602
+ * canonical: false, // when true, emit RFC 8785 JCS canonical bytes (suitable for signing)
603
+ * indent: 0, // pretty-print indent for the non-canonical path
604
+ * fileMode: 0o600,
605
+ * computeHash: false,
606
+ * retryAttempts: 5,
607
+ * retryBaseMs: 50,
608
+ * retryMaxMs: 2000,
609
+ *
610
+ * @example
611
+ * async function persist() {
612
+ * var result = await b.atomicFile.writeJson(
613
+ * "/var/lib/blamejs/manifest.json",
614
+ * { schema: 1, items: [] },
615
+ * { canonical: true, computeHash: true }
616
+ * );
617
+ * return result; // → { bytesWritten: 24, hash: "<sha3-512 hex>" }
618
+ * }
619
+ */
378
620
  async function writeJson(filepath, value, opts) {
379
621
  opts = opts || {};
380
622
  var serialized = opts.canonical
@@ -383,6 +625,33 @@ async function writeJson(filepath, value, opts) {
383
625
  return await write(filepath, serialized, opts);
384
626
  }
385
627
 
628
+ /**
629
+ * @primitive b.atomicFile.readJson
630
+ * @signature b.atomicFile.readJson(filepath, opts)
631
+ * @since 0.7.0
632
+ * @status stable
633
+ * @related b.atomicFile.writeJson, b.atomicFile.read
634
+ *
635
+ * Atomic JSON read. Routes through `b.atomicFile.read` (size cap +
636
+ * optional integrity hash) then parses via `b.safeJson.parse`, which
637
+ * applies the framework's prototype-pollution / __proto__-key
638
+ * defenses. Throws `atomic-file/too-large`, `atomic-file/integrity`,
639
+ * or a JSON parse error from safeJson — never returns a partial
640
+ * object.
641
+ *
642
+ * @opts
643
+ * maxBytes: 67108864,
644
+ * expectedHash: undefined,
645
+ *
646
+ * @example
647
+ * async function load() {
648
+ * var doc = await b.atomicFile.readJson(
649
+ * "/var/lib/blamejs/manifest.json",
650
+ * { maxBytes: 1048576 }
651
+ * );
652
+ * return doc; // → { schema: 1, items: [] }
653
+ * }
654
+ */
386
655
  async function readJson(filepath, opts) {
387
656
  opts = opts || {};
388
657
  var buf = await read(filepath, opts);
@@ -390,8 +659,37 @@ async function readJson(filepath, opts) {
390
659
  return safeJson.parse(input, opts);
391
660
  }
392
661
 
393
- // ---- copy ----
394
-
662
+ /**
663
+ * @primitive b.atomicFile.copy
664
+ * @signature b.atomicFile.copy(src, dst, opts)
665
+ * @since 0.7.0
666
+ * @status stable
667
+ * @related b.atomicFile.copyDirRecursive, b.atomicFile.write
668
+ *
669
+ * Atomic file copy. Reads the source via `b.atomicFile.read` (so
670
+ * `maxBytes` and retry semantics apply), then writes the bytes
671
+ * through `b.atomicFile.write` (temp + fsync + rename). When
672
+ * `expectedHash` is set, the digest is checked against the WRITTEN
673
+ * bytes at `dst` — the source is not gated on it. Returns
674
+ * `{ bytesWritten, hash }`.
675
+ *
676
+ * @opts
677
+ * maxBytes: 67108864,
678
+ * fileMode: 0o600,
679
+ * computeHash: false,
680
+ * expectedHash: undefined,
681
+ * retryAttempts: 5,
682
+ *
683
+ * @example
684
+ * async function snapshot() {
685
+ * var result = await b.atomicFile.copy(
686
+ * "/var/lib/blamejs/state.bin",
687
+ * "/var/lib/blamejs/state.bin.bak",
688
+ * { computeHash: true }
689
+ * );
690
+ * return result; // → { bytesWritten: 4096, hash: "<sha3-512 hex>" }
691
+ * }
692
+ */
395
693
  async function copy(src, dst, opts) {
396
694
  opts = Object.assign({}, DEFAULTS, opts || {});
397
695
  var srcOpts = Object.assign({}, opts);
@@ -401,14 +699,65 @@ async function copy(src, dst, opts) {
401
699
  return await write(dst, buf, opts);
402
700
  }
403
701
 
404
- // ---- exists ----
405
-
702
+ /**
703
+ * @primitive b.atomicFile.exists
704
+ * @signature b.atomicFile.exists(filepath)
705
+ * @since 0.7.0
706
+ * @status stable
707
+ * @related b.atomicFile.read, b.atomicFile.readSync
708
+ *
709
+ * Synchronous existence check. Thin wrapper over `fs.existsSync` that
710
+ * normalises the answer for callers that already require this module
711
+ * — saves an additional `require("fs")` in modules that otherwise
712
+ * only need atomicFile.
713
+ *
714
+ * @example
715
+ * if (b.atomicFile.exists("/var/lib/blamejs/state.bin")) {
716
+ * // → safe to read
717
+ * }
718
+ */
406
719
  function exists(filepath) {
407
720
  return fs.existsSync(filepath);
408
721
  }
409
722
 
410
- // ---- lock (cross-process file mutex) ----
411
-
723
+ /**
724
+ * @primitive b.atomicFile.lock
725
+ * @signature b.atomicFile.lock(filepath, fn, opts)
726
+ * @since 0.7.0
727
+ * @status stable
728
+ * @related b.atomicFile.write, b.atomicFile.read
729
+ *
730
+ * Cross-process file mutex around a read-modify-write sequence.
731
+ * Acquires `<filepath>.lock` via `O_CREAT | O_EXCL` (the POSIX atomic
732
+ * "create-or-fail" primitive — Node's "wx" flag), writes
733
+ * `{ pid, acquiredAt }` into the lock for diagnostics, runs `fn()`,
734
+ * then unlinks the lock in a `finally` so a thrown handler still
735
+ * releases. Stale-lock detection: lock files older than 5 minutes
736
+ * are assumed crashed-holder and reclaimed. Returns whatever `fn`
737
+ * returns (or rejects with whatever it throws). Throws
738
+ * `atomic-file/lock-timeout` if the lock can't be acquired before
739
+ * `lockTimeoutMs`.
740
+ *
741
+ * @opts
742
+ * lockTimeoutMs: 30000, // total time to wait before timing out
743
+ * lockPollMs: 50, // sleep between lock acquisition attempts
744
+ * fileMode: 0o600, // mode applied to the lock file
745
+ * signal: AbortSignal | undefined, // abort the wait early
746
+ *
747
+ * @example
748
+ * async function bumpCounter() {
749
+ * return await b.atomicFile.lock(
750
+ * "/var/lib/blamejs/counter.txt",
751
+ * async function () {
752
+ * var buf = await b.atomicFile.read("/var/lib/blamejs/counter.txt", { encoding: "utf8" });
753
+ * var next = (parseInt(buf, 10) || 0) + 1;
754
+ * await b.atomicFile.write("/var/lib/blamejs/counter.txt", String(next));
755
+ * return next;
756
+ * },
757
+ * { lockTimeoutMs: 5000 }
758
+ * );
759
+ * }
760
+ */
412
761
  async function lock(filepath, fn, opts) {
413
762
  opts = Object.assign({}, DEFAULTS, opts || {});
414
763
  var lockPath = filepath + ".lock";
@@ -459,26 +808,38 @@ async function lock(filepath, fn, opts) {
459
808
  }
460
809
  }
461
810
 
462
- // Single-directory listing primitive. Wraps fs.readdirSync with the
463
- // optional-stat pattern + missing-dir tolerance + filter that callers
464
- // across the framework were re-implementing.
465
- //
466
- // opts:
467
- // filter: function(name) => boolean name-only predicate
468
- // includeStat: bool — adds mtimeMs / sizeBytes / isDirectory /
469
- // isFile per entry (one fs.statSync call each).
470
- // Skip when the caller only needs names saves a
471
- // syscall per entry.
472
- // missingOk: bool default true. Returns [] when the dir
473
- // doesn't exist (ENOENT). Other errors throw.
474
- //
475
- // Returns: array of { name, fullPath } (plus stat fields when
476
- // includeStat is true). Entries that vanish between readdir and
477
- // stat (concurrent cleanup) are silently dropped.
478
- //
479
- // For recursive directory walks, callers compose listDir per
480
- // subdirectory the primitive doesn't recurse, so callers can apply
481
- // per-iteration limits / filters / stop conditions cleanly.
811
+ /**
812
+ * @primitive b.atomicFile.listDir
813
+ * @signature b.atomicFile.listDir(dir, opts)
814
+ * @since 0.7.0
815
+ * @status stable
816
+ * @related b.atomicFile.cleanOrphans, b.atomicFile.copyDirRecursive
817
+ *
818
+ * Single-directory listing with optional stat enrichment, name-only
819
+ * filter, and missing-dir tolerance. Returns an array of
820
+ * `{ name, fullPath }` objects (plus `mtimeMs`, `sizeBytes`,
821
+ * `isDirectory`, `isFile` when `includeStat: true`). Entries that
822
+ * vanish between readdir and stat — concurrent cleanup by another
823
+ * process — are silently dropped. For recursive walks, callers
824
+ * compose per subdirectory so per-iteration limits, filters, and
825
+ * stop conditions stay explicit.
826
+ *
827
+ * @opts
828
+ * filter: function (name) { return true; }, // name-only predicate; falsey skips entry
829
+ * includeStat: false, // when true, statSync each entry; one extra syscall per entry
830
+ * missingOk: true, // when true (default), ENOENT returns []; when false, ENOENT throws
831
+ *
832
+ * @example
833
+ * var entries = b.atomicFile.listDir(
834
+ * "/var/lib/blamejs/audit",
835
+ * {
836
+ * filter: function (n) { return n.endsWith(".log"); },
837
+ * includeStat: true,
838
+ * }
839
+ * );
840
+ * // → [{ name: "audit-1.log", fullPath: "/var/lib/blamejs/audit/audit-1.log",
841
+ * // mtimeMs: 1700000000000, sizeBytes: 2048, isDirectory: false, isFile: true }, ...]
842
+ */
482
843
  function listDir(dir, opts) {
483
844
  opts = opts || {};
484
845
  var missingOk = opts.missingOk !== false;