@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.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/atomic-file.js
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
//
|
|
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;
|