@blamejs/core 0.9.14 → 0.9.15
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 +1 -0
- package/lib/a2a.js +11 -11
- package/lib/acme.js +5 -5
- package/lib/ai-input.js +2 -2
- package/lib/api-key.js +4 -4
- package/lib/api-snapshot.js +6 -6
- package/lib/app-shutdown.js +2 -2
- package/lib/app.js +5 -5
- package/lib/archive.js +8 -8
- package/lib/argon2-builtin.js +2 -2
- package/lib/atomic-file.js +53 -53
- package/lib/audit-sign.js +8 -8
- package/lib/audit-tools.js +22 -22
- package/lib/auth/dpop.js +3 -3
- package/lib/auth/sd-jwt-vc.js +2 -2
- package/lib/backup/bundle.js +17 -17
- package/lib/backup/index.js +36 -36
- package/lib/budr.js +3 -3
- package/lib/bundler.js +20 -20
- package/lib/circuit-breaker.js +4 -4
- package/lib/cli.js +25 -26
- package/lib/cluster.js +2 -2
- package/lib/compliance-sanctions.js +2 -2
- package/lib/config-drift.js +15 -15
- package/lib/content-credentials.js +4 -4
- package/lib/credential-hash.js +3 -3
- package/lib/daemon.js +19 -19
- package/lib/db-file-lifecycle.js +24 -24
- package/lib/db-schema.js +2 -2
- package/lib/db.js +35 -35
- package/lib/dev.js +10 -10
- package/lib/dr-runbook.js +5 -5
- package/lib/dual-control.js +2 -2
- package/lib/external-db-migrate.js +2 -2
- package/lib/external-db.js +2 -2
- package/lib/fdx.js +2 -2
- package/lib/file-upload.js +30 -30
- package/lib/flag-providers.js +4 -4
- package/lib/gate-contract.js +5 -5
- package/lib/graphql-federation.js +4 -7
- package/lib/honeytoken.js +6 -6
- package/lib/http-client-cookie-jar.js +6 -6
- package/lib/http-client.js +18 -18
- package/lib/i18n.js +5 -5
- package/lib/keychain.js +9 -9
- package/lib/legal-hold.js +2 -2
- package/lib/local-db-thin.js +9 -9
- package/lib/log-stream-local.js +17 -17
- package/lib/log-stream-syslog.js +2 -2
- package/lib/log-stream.js +3 -3
- package/lib/mail-bounce.js +2 -2
- package/lib/mail-mdn.js +2 -2
- package/lib/mail-srs.js +2 -2
- package/lib/mail.js +4 -4
- package/lib/mcp.js +2 -2
- package/lib/metrics.js +2 -2
- package/lib/middleware/api-encrypt.js +16 -16
- package/lib/middleware/body-parser.js +16 -16
- package/lib/middleware/compression.js +3 -3
- package/lib/middleware/csp-nonce.js +4 -4
- package/lib/middleware/health.js +7 -7
- package/lib/middleware/idempotency-key.js +163 -63
- package/lib/migrations.js +3 -3
- package/lib/mtls-ca.js +26 -26
- package/lib/mtls-engine-default.js +5 -5
- package/lib/network-dns.js +2 -2
- package/lib/network-nts.js +2 -2
- package/lib/network-proxy.js +3 -3
- package/lib/network-smtp-policy.js +2 -2
- package/lib/network-tls.js +17 -17
- package/lib/network.js +13 -13
- package/lib/notify.js +3 -3
- package/lib/object-store/gcs-bucket-ops.js +2 -2
- package/lib/object-store/gcs.js +5 -5
- package/lib/object-store/index.js +6 -6
- package/lib/object-store/local.js +19 -19
- package/lib/object-store/sigv4.js +3 -3
- package/lib/observability-tracer.js +4 -4
- package/lib/otel-export.js +3 -3
- package/lib/pagination.js +5 -5
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/pqc-gate.js +5 -5
- package/lib/pubsub-redis.js +2 -2
- package/lib/queue-local.js +3 -3
- package/lib/queue.js +2 -2
- package/lib/redis-client.js +4 -4
- package/lib/restore-bundle.js +18 -18
- package/lib/restore-rollback.js +34 -34
- package/lib/restore.js +16 -16
- package/lib/router.js +13 -13
- package/lib/sandbox.js +8 -8
- package/lib/sec-cyber.js +3 -3
- package/lib/security-assert.js +2 -2
- package/lib/seeders.js +4 -4
- package/lib/self-update.js +18 -18
- package/lib/session-device-binding.js +2 -2
- package/lib/static.js +22 -22
- package/lib/template.js +19 -19
- package/lib/testing.js +7 -7
- package/lib/tls-exporter.js +5 -5
- package/lib/tracing.js +3 -3
- package/lib/vault/index.js +11 -11
- package/lib/vault/passphrase-ops.js +37 -37
- package/lib/vault/passphrase-source.js +2 -2
- package/lib/vault/rotate.js +64 -64
- package/lib/vault/seal-pem-file.js +26 -26
- package/lib/watcher.js +23 -23
- package/lib/webhook.js +10 -10
- package/lib/worker-pool.js +6 -6
- package/lib/ws-client.js +4 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/config-drift.js
CHANGED
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
* @card
|
|
36
36
|
* Monitor + alert when runtime config diverges from a declared baseline.
|
|
37
37
|
*/
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var nodeFs = require("node:fs");
|
|
39
|
+
var nodePath = require("node:path");
|
|
40
40
|
var auditSign = require("./audit-sign");
|
|
41
41
|
var canonicalJson = require("./canonical-json");
|
|
42
|
-
var
|
|
42
|
+
var bCrypto = require("./crypto");
|
|
43
43
|
var lazyRequire = require("./lazy-require");
|
|
44
44
|
var safeJson = require("./safe-json");
|
|
45
45
|
var validateOpts = require("./validate-opts");
|
|
@@ -57,12 +57,12 @@ var SIDECAR_VERSION = 1;
|
|
|
57
57
|
// Deterministic key order so the same snapshot always hashes to the same
|
|
58
58
|
// digest. Pre-v0.6.67 the in-line implementation silently lost Date /
|
|
59
59
|
// Map / Set / Buffer / BigInt content; the shared walker handles all
|
|
60
|
-
// of those + circular
|
|
60
|
+
// of those + circular renodeFs. Same bytes as audit-chain / audit-tools /
|
|
61
61
|
// pagination would produce for the same input.
|
|
62
62
|
function _stableStringify(value) { return canonicalJson.stringify(value); }
|
|
63
63
|
|
|
64
64
|
function _hashSnapshot(snapshot) {
|
|
65
|
-
return
|
|
65
|
+
return bCrypto.sha3Hash(_stableStringify(snapshot));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function _diffShallow(prev, next) {
|
|
@@ -155,7 +155,7 @@ function create(opts) {
|
|
|
155
155
|
// (e.g. operator-tracked metadata that legitimately changes per
|
|
156
156
|
// boot). Captured in the snapshot but never flagged.
|
|
157
157
|
var ignoreKeys = Array.isArray(opts.ignoreKeys) ? opts.ignoreKeys.slice() : [];
|
|
158
|
-
var sidecarPath =
|
|
158
|
+
var sidecarPath = nodePath.join(dataDir,
|
|
159
159
|
baselineName === "default" ? SIDECAR_NAME : ("config-baseline-" + baselineName + ".sig"));
|
|
160
160
|
|
|
161
161
|
function _emit(action, info, outcome) {
|
|
@@ -172,9 +172,9 @@ function create(opts) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function _readSidecar() {
|
|
175
|
-
if (!
|
|
175
|
+
if (!nodeFs.existsSync(sidecarPath)) return null;
|
|
176
176
|
var raw;
|
|
177
|
-
try { raw =
|
|
177
|
+
try { raw = nodeFs.readFileSync(sidecarPath, "utf8"); }
|
|
178
178
|
catch (_e) { return null; }
|
|
179
179
|
var parsed;
|
|
180
180
|
try { parsed = safeJson.parse(raw); }
|
|
@@ -200,8 +200,8 @@ function create(opts) {
|
|
|
200
200
|
snapshot: snapshot,
|
|
201
201
|
};
|
|
202
202
|
var tmp = sidecarPath + ".tmp";
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
nodeFs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
204
|
+
nodeFs.renameSync(tmp, sidecarPath);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function _verifySidecar(parsed) {
|
|
@@ -366,10 +366,10 @@ function create(opts) {
|
|
|
366
366
|
*/
|
|
367
367
|
function verifyVendorIntegrity(opts) {
|
|
368
368
|
opts = opts || {};
|
|
369
|
-
var libVendorDir = opts.libVendorDir ||
|
|
370
|
-
var manifestPath = opts.manifestPath ||
|
|
369
|
+
var libVendorDir = opts.libVendorDir || nodePath.join(process.cwd(), "lib", "vendor");
|
|
370
|
+
var manifestPath = opts.manifestPath || nodePath.join(libVendorDir, "MANIFEST.json");
|
|
371
371
|
var raw;
|
|
372
|
-
try { raw =
|
|
372
|
+
try { raw = nodeFs.readFileSync(manifestPath, "utf8"); }
|
|
373
373
|
catch (_e) {
|
|
374
374
|
throw _err("VENDOR_MANIFEST_MISSING",
|
|
375
375
|
"vendor MANIFEST.json missing at " + manifestPath, true);
|
|
@@ -389,10 +389,10 @@ function verifyVendorIntegrity(opts) {
|
|
|
389
389
|
var rel = files[kind];
|
|
390
390
|
var expected = hashes[kind];
|
|
391
391
|
if (typeof rel !== "string" || typeof expected !== "string") return;
|
|
392
|
-
var abs =
|
|
392
|
+
var abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(process.cwd(), rel);
|
|
393
393
|
var actual;
|
|
394
394
|
try {
|
|
395
|
-
var bytes =
|
|
395
|
+
var bytes = nodeFs.readFileSync(abs);
|
|
396
396
|
actual = "sha256:" + require("node:crypto")
|
|
397
397
|
.createHash("sha256").update(bytes).digest("hex");
|
|
398
398
|
} catch (_e) {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* C2PA 2.1 content provenance — sign assets with a manifest declaring origin, edits, AI involvement.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
var
|
|
31
|
+
var bCrypto = require("./crypto");
|
|
32
32
|
var canonicalJson = require("./canonical-json");
|
|
33
33
|
var validateOpts = require("./validate-opts");
|
|
34
34
|
var audit = require("./audit");
|
|
@@ -234,7 +234,7 @@ function sign(manifest, opts) {
|
|
|
234
234
|
validateOpts.requireNonEmptyString(opts.privateKeyPem,
|
|
235
235
|
"contentCredentials.sign: privateKeyPem", ContentCredentialsError, "BAD_KEY");
|
|
236
236
|
var canonical = canonicalJson.stringify(manifest);
|
|
237
|
-
var signature =
|
|
237
|
+
var signature = bCrypto.sign(Buffer.from(canonical, "utf8"), opts.privateKeyPem);
|
|
238
238
|
var auditOn = opts.audit !== false;
|
|
239
239
|
if (auditOn) {
|
|
240
240
|
audit.safeEmit({
|
|
@@ -299,7 +299,7 @@ function verify(envelope, publicKeyPem, opts) {
|
|
|
299
299
|
catch (_e) {
|
|
300
300
|
return { valid: false, claims: null, reason: "signature-base64-bad" };
|
|
301
301
|
}
|
|
302
|
-
var ok =
|
|
302
|
+
var ok = bCrypto.verify(Buffer.from(canonical, "utf8"), sigBuf, publicKeyPem);
|
|
303
303
|
if (!ok) {
|
|
304
304
|
return { valid: false, claims: null, reason: "signature-mismatch" };
|
|
305
305
|
}
|
|
@@ -519,7 +519,7 @@ function signCose(manifest, opts) {
|
|
|
519
519
|
var toBeSigned = Buffer.concat(sigStructureBufs);
|
|
520
520
|
|
|
521
521
|
// Sign with framework's b.crypto.sign — algorithm picked from the PEM.
|
|
522
|
-
var signature =
|
|
522
|
+
var signature = bCrypto.sign(toBeSigned, opts.privateKeyPem);
|
|
523
523
|
|
|
524
524
|
// COSE_Sign1 = tagged-18 array [protected, unprotected, payload, signature]
|
|
525
525
|
var coseSign1 = Buffer.concat([
|
package/lib/credential-hash.js
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* Derive a deterministic, verifiable hash for credential lookup (API-key secret, shared bearer token, webhook signing key) without storing the credential itself.
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
var
|
|
44
|
+
var bCrypto = require("./crypto");
|
|
45
45
|
var C = require("./constants");
|
|
46
46
|
var lazyRequire = require("./lazy-require");
|
|
47
47
|
var { FrameworkError } = require("./framework-error");
|
|
@@ -73,7 +73,7 @@ function _shake256(secret, length) {
|
|
|
73
73
|
// crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
|
|
74
74
|
// exact primitive we need — the framework's KDF and credential-hash
|
|
75
75
|
// share one underlying XOF.
|
|
76
|
-
return
|
|
76
|
+
return bCrypto.kdf(secret, length);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
|
|
@@ -291,7 +291,7 @@ async function verify(secret, envelope) {
|
|
|
291
291
|
return false;
|
|
292
292
|
}
|
|
293
293
|
var expected = _shake256(secret, decoded.payload.length);
|
|
294
|
-
var ok =
|
|
294
|
+
var ok = bCrypto.timingSafeEqual(expected, decoded.payload);
|
|
295
295
|
_emitEvent("credentialHash.verify", 1,
|
|
296
296
|
{ outcome: ok ? "success" : "failure", algo: algoName });
|
|
297
297
|
return ok;
|
package/lib/daemon.js
CHANGED
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
* Long-running process orchestration — supervisor wiring around `b.appShutdown`, foreground signal handling, detached-fork spawn via `b.processSpawn`, PID-file health probes, and a SIGTERM-then-SIGKILL restart policy on stop.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
var
|
|
40
|
+
var nodeFs = require("fs");
|
|
41
|
+
var nodePath = require("path");
|
|
42
|
+
var numericBounds = require("./numeric-bounds");
|
|
43
43
|
var appShutdown = require("./app-shutdown");
|
|
44
44
|
var processSpawn = require("./process-spawn");
|
|
45
45
|
var lazyRequire = require("./lazy-require");
|
|
@@ -80,7 +80,7 @@ function _isLivePid(pid) {
|
|
|
80
80
|
|
|
81
81
|
function _readPidFile(pidFile) {
|
|
82
82
|
try {
|
|
83
|
-
var raw =
|
|
83
|
+
var raw = nodeFs.readFileSync(pidFile, "utf8");
|
|
84
84
|
var pid = parseInt(String(raw).trim(), 10);
|
|
85
85
|
return isFinite(pid) && pid > 0 ? pid : null;
|
|
86
86
|
} catch (_e) { return null; }
|
|
@@ -118,9 +118,9 @@ function _validateStopOpts(opts) {
|
|
|
118
118
|
"daemon.stop: opts.pidFile", DaemonError, "daemon/bad-pid-file");
|
|
119
119
|
validateOpts.optionalNonEmptyString(opts.signal,
|
|
120
120
|
"daemon.stop: opts.signal", DaemonError, "daemon/bad-signal");
|
|
121
|
-
|
|
121
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
|
|
122
122
|
"daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
|
|
123
|
-
|
|
123
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.pollMs,
|
|
124
124
|
"daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -133,7 +133,7 @@ function _maybeReapStale(pidFile) {
|
|
|
133
133
|
}
|
|
134
134
|
if (existing === process.pid) return false;
|
|
135
135
|
// Stale: PID is gone (or signal-0 returned ESRCH). Reap + audit.
|
|
136
|
-
try {
|
|
136
|
+
try { nodeFs.unlinkSync(pidFile); } catch (_e) { /* race: another reaper */ }
|
|
137
137
|
_safeAuditEmit("daemon.stale_pid_cleaned", "success", {
|
|
138
138
|
pidFile: pidFile,
|
|
139
139
|
stalePid: existing,
|
|
@@ -146,13 +146,13 @@ function _maybeReapStale(pidFile) {
|
|
|
146
146
|
// redirect of the current process' stdout/stderr.
|
|
147
147
|
function _openLogFd(logFile) {
|
|
148
148
|
if (typeof logFile !== "string" || logFile.length === 0) return null;
|
|
149
|
-
atomicFile.ensureDir(
|
|
150
|
-
var fd =
|
|
149
|
+
atomicFile.ensureDir(nodePath.dirname(logFile));
|
|
150
|
+
var fd = nodeFs.openSync(logFile, "a", DEFAULT_LOG_FILE_MODE);
|
|
151
151
|
return fd;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// Redirect the current process's stdout/stderr file descriptors at the
|
|
155
|
-
// given fd. Implemented via
|
|
155
|
+
// given fd. Implemented via nodeFs.writeSync streams: Node doesn't expose a
|
|
156
156
|
// portable dup2, so we replace process.stdout.write / process.stderr.write
|
|
157
157
|
// with a writer that pushes to the log fd. This is the standard
|
|
158
158
|
// pattern for foreground daemons that don't want to lose output when
|
|
@@ -163,7 +163,7 @@ function _redirectStdio(fd) {
|
|
|
163
163
|
var enc = typeof encOrCb === "string" ? encOrCb : "utf8";
|
|
164
164
|
var cb = typeof encOrCb === "function" ? encOrCb : maybeCb;
|
|
165
165
|
var buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), enc);
|
|
166
|
-
try {
|
|
166
|
+
try { nodeFs.writeSync(fd, buf); }
|
|
167
167
|
catch (_e) { /* log fd closed underneath us — drop */ }
|
|
168
168
|
if (typeof cb === "function") cb();
|
|
169
169
|
return true;
|
|
@@ -246,21 +246,21 @@ function start(opts) {
|
|
|
246
246
|
cwd: typeof opts.cwd === "string" ? opts.cwd : undefined,
|
|
247
247
|
});
|
|
248
248
|
} catch (e) {
|
|
249
|
-
try { if (typeof logFd === "number")
|
|
249
|
+
try { if (typeof logFd === "number") nodeFs.closeSync(logFd); }
|
|
250
250
|
catch (_c) { /* best-effort */ }
|
|
251
251
|
throw new DaemonError("daemon/spawn-failed",
|
|
252
252
|
"daemon.start: spawn failed: " + ((e && e.message) || String(e)));
|
|
253
253
|
}
|
|
254
254
|
// Write the child's PID via atomic temp+rename so a concurrent
|
|
255
255
|
// observer never sees a half-written pidFile.
|
|
256
|
-
atomicFile.ensureDir(
|
|
256
|
+
atomicFile.ensureDir(nodePath.dirname(pidFile));
|
|
257
257
|
var pidStr = String(child.pid) + "\n";
|
|
258
258
|
atomicFile.writeSync(pidFile, pidStr, { fileMode: 0o600 });
|
|
259
259
|
// Detach so the child survives parent exit.
|
|
260
260
|
try { child.unref(); } catch (_u) { /* best-effort */ }
|
|
261
261
|
if (typeof logFd === "number") {
|
|
262
262
|
// Parent doesn't need its handle to the log; child inherited it.
|
|
263
|
-
try {
|
|
263
|
+
try { nodeFs.closeSync(logFd); } catch (_c) { /* best-effort */ }
|
|
264
264
|
}
|
|
265
265
|
_safeAuditEmit("daemon.started", "success", {
|
|
266
266
|
pidFile: pidFile,
|
|
@@ -307,7 +307,7 @@ function start(opts) {
|
|
|
307
307
|
run: function () {
|
|
308
308
|
try { lock.release(); } catch (_e) { /* best-effort */ }
|
|
309
309
|
if (logFdForeground !== null) {
|
|
310
|
-
try {
|
|
310
|
+
try { nodeFs.closeSync(logFdForeground); } catch (_c) { /* best-effort */ }
|
|
311
311
|
}
|
|
312
312
|
},
|
|
313
313
|
timeoutMs: C.TIME.seconds(2),
|
|
@@ -381,7 +381,7 @@ async function stop(opts) {
|
|
|
381
381
|
}
|
|
382
382
|
if (!_isLivePid(pid)) {
|
|
383
383
|
// Stale — clean up and report.
|
|
384
|
-
try {
|
|
384
|
+
try { nodeFs.unlinkSync(pidFile); } catch (_e) { /* best-effort */ }
|
|
385
385
|
_safeAuditEmit("daemon.stale_pid_cleaned", "success", { pidFile: pidFile, stalePid: pid });
|
|
386
386
|
return { stopped: false, pid: pid, reason: "stale" };
|
|
387
387
|
}
|
|
@@ -392,7 +392,7 @@ async function stop(opts) {
|
|
|
392
392
|
catch (e) {
|
|
393
393
|
if (e && e.code === "ESRCH") {
|
|
394
394
|
// Died between read and kill — cleanup + report.
|
|
395
|
-
try {
|
|
395
|
+
try { nodeFs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
396
396
|
_safeAuditEmit("daemon.stopped", "success", {
|
|
397
397
|
pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
|
|
398
398
|
});
|
|
@@ -405,7 +405,7 @@ async function stop(opts) {
|
|
|
405
405
|
var deadline = t0 + timeoutMs;
|
|
406
406
|
while (Date.now() < deadline) {
|
|
407
407
|
if (!_isLivePid(pid)) {
|
|
408
|
-
try {
|
|
408
|
+
try { nodeFs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
409
409
|
_safeAuditEmit("daemon.stopped", "success", {
|
|
410
410
|
pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
|
|
411
411
|
});
|
|
@@ -428,7 +428,7 @@ async function stop(opts) {
|
|
|
428
428
|
if (!_isLivePid(pid)) break;
|
|
429
429
|
await safeAsync.sleep(pollMs, { signal: opts.abortSignal });
|
|
430
430
|
}
|
|
431
|
-
try {
|
|
431
|
+
try { nodeFs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
432
432
|
_safeAuditEmit("daemon.stopped", "success", {
|
|
433
433
|
pidFile: pidFile, signal: "SIGKILL", waitMs: Date.now() - t0, escalated: true,
|
|
434
434
|
});
|
package/lib/db-file-lifecycle.js
CHANGED
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
* bun:sqlite).
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
-
var
|
|
60
|
+
var nodeFs = require("fs");
|
|
61
61
|
var os = require("os");
|
|
62
|
-
var
|
|
62
|
+
var nodePath = require("path");
|
|
63
63
|
var atomicFile = require("./atomic-file");
|
|
64
64
|
var C = require("./constants");
|
|
65
65
|
var { generateBytes, generateToken, encryptPacked, decryptPacked } = require("./crypto");
|
|
@@ -92,7 +92,7 @@ function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
|
|
|
92
92
|
// Linux: /dev/shm is the standard tmpfs mount.
|
|
93
93
|
if (process.platform === "linux") {
|
|
94
94
|
try {
|
|
95
|
-
var st =
|
|
95
|
+
var st = nodeFs.statSync("/dev/shm");
|
|
96
96
|
if (st && st.isDirectory()) return "/dev/shm";
|
|
97
97
|
} catch (_e) { /* fall through */ }
|
|
98
98
|
}
|
|
@@ -115,8 +115,8 @@ function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
|
|
|
115
115
|
* Returns an encrypted-DB-file lifecycle handle. Methods:
|
|
116
116
|
*
|
|
117
117
|
* - `decryptToTmp()` — decrypt the encrypted DB file to a fresh
|
|
118
|
-
* tmpfs path and return the
|
|
119
|
-
* return the existing
|
|
118
|
+
* tmpfs path and return the nodePath. Idempotent: subsequent calls
|
|
119
|
+
* return the existing nodePath.
|
|
120
120
|
* - `dbPath` — the resolved plaintext-tmpfs path (set after
|
|
121
121
|
* `decryptToTmp()` runs).
|
|
122
122
|
* - `startFlushTimer(db, opts?)` — start a periodic flush timer
|
|
@@ -164,15 +164,15 @@ function fileLifecycle(opts) {
|
|
|
164
164
|
var label = opts.label || "default";
|
|
165
165
|
var encName = opts.encryptedDbName || "db.enc";
|
|
166
166
|
var encPath = opts.encryptedDbPath
|
|
167
|
-
?
|
|
168
|
-
:
|
|
167
|
+
? nodePath.resolve(opts.encryptedDbPath)
|
|
168
|
+
: nodePath.join(opts.dataDir, encName);
|
|
169
169
|
var keyPath = opts.dbKeyPath
|
|
170
|
-
?
|
|
171
|
-
:
|
|
170
|
+
? nodePath.resolve(opts.dbKeyPath)
|
|
171
|
+
: nodePath.join(opts.dataDir, "db.key.enc");
|
|
172
172
|
var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
173
173
|
var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
|
|
174
|
-
if (!
|
|
175
|
-
if (!
|
|
174
|
+
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
175
|
+
if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true });
|
|
176
176
|
|
|
177
177
|
var dbPath = null;
|
|
178
178
|
var encKey = null;
|
|
@@ -181,8 +181,8 @@ function fileLifecycle(opts) {
|
|
|
181
181
|
|
|
182
182
|
function _loadOrGenerateKey() {
|
|
183
183
|
if (encKey) return encKey;
|
|
184
|
-
if (
|
|
185
|
-
var sealedKey =
|
|
184
|
+
if (nodeFs.existsSync(keyPath)) {
|
|
185
|
+
var sealedKey = nodeFs.readFileSync(keyPath, "utf8");
|
|
186
186
|
var keyB64;
|
|
187
187
|
try { keyB64 = opts.vault.unseal(sealedKey); }
|
|
188
188
|
catch (e) {
|
|
@@ -208,10 +208,10 @@ function fileLifecycle(opts) {
|
|
|
208
208
|
function decryptToTmp() {
|
|
209
209
|
if (decrypted) return dbPath;
|
|
210
210
|
_loadOrGenerateKey();
|
|
211
|
-
dbPath =
|
|
211
|
+
dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
|
|
212
212
|
generateToken(TMP_NAME_BYTES) + ".db");
|
|
213
|
-
if (
|
|
214
|
-
var packed =
|
|
213
|
+
if (nodeFs.existsSync(encPath)) {
|
|
214
|
+
var packed = nodeFs.readFileSync(encPath);
|
|
215
215
|
if (packed.length < 26) { // allow:raw-byte-literal — minimum envelope length
|
|
216
216
|
throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
|
|
217
217
|
"fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
|
|
@@ -231,7 +231,7 @@ function fileLifecycle(opts) {
|
|
|
231
231
|
label: label,
|
|
232
232
|
encPath: encPath,
|
|
233
233
|
dbPath: dbPath,
|
|
234
|
-
isEmpty: !
|
|
234
|
+
isEmpty: !nodeFs.existsSync(encPath),
|
|
235
235
|
});
|
|
236
236
|
_emitMetric("decrypted");
|
|
237
237
|
return dbPath;
|
|
@@ -246,8 +246,8 @@ function fileLifecycle(opts) {
|
|
|
246
246
|
try { db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run(); }
|
|
247
247
|
catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
|
|
248
248
|
}
|
|
249
|
-
if (!
|
|
250
|
-
var plain =
|
|
249
|
+
if (!nodeFs.existsSync(dbPath)) return null;
|
|
250
|
+
var plain = nodeFs.readFileSync(dbPath);
|
|
251
251
|
var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
252
252
|
atomicFile.writeSync(encPath, packed);
|
|
253
253
|
_emitAudit("flushed", "success", { label: label, bytes: plain.length });
|
|
@@ -264,11 +264,11 @@ function fileLifecycle(opts) {
|
|
|
264
264
|
try { db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run(); }
|
|
265
265
|
catch (_e) { /* best-effort */ }
|
|
266
266
|
}
|
|
267
|
-
if (!
|
|
267
|
+
if (!nodeFs.existsSync(dbPath)) {
|
|
268
268
|
throw new DbFileLifecycleError("db-file-lifecycle/no-source",
|
|
269
269
|
"fileLifecycle.snapshot: " + dbPath + " is missing");
|
|
270
270
|
}
|
|
271
|
-
var plain =
|
|
271
|
+
var plain = nodeFs.readFileSync(dbPath);
|
|
272
272
|
return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -308,9 +308,9 @@ function fileLifecycle(opts) {
|
|
|
308
308
|
try { db.close(); } catch (_e) { /* best-effort */ }
|
|
309
309
|
}
|
|
310
310
|
if (fopts.removePlaintext === true && dbPath) {
|
|
311
|
-
try {
|
|
312
|
-
try {
|
|
313
|
-
try {
|
|
311
|
+
try { nodeFs.unlinkSync(dbPath); } catch (_e) { /* best-effort */ }
|
|
312
|
+
try { nodeFs.unlinkSync(dbPath + "-wal"); } catch (_e) { /* best-effort */ }
|
|
313
|
+
try { nodeFs.unlinkSync(dbPath + "-shm"); } catch (_e) { /* best-effort */ }
|
|
314
314
|
}
|
|
315
315
|
_emitAudit("shutdown", "success", { label: label });
|
|
316
316
|
}
|
package/lib/db-schema.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* surfaces a rollback throw without
|
|
37
37
|
* swallowing the original error.
|
|
38
38
|
*/
|
|
39
|
-
var
|
|
39
|
+
var nodePath = require("path");
|
|
40
40
|
var atomicFile = require("./atomic-file");
|
|
41
41
|
var safeSql = require("./safe-sql");
|
|
42
42
|
|
|
@@ -280,7 +280,7 @@ function runMigrations(database, migrationDir) {
|
|
|
280
280
|
skipped.push(file);
|
|
281
281
|
continue;
|
|
282
282
|
}
|
|
283
|
-
var fullPath =
|
|
283
|
+
var fullPath = nodePath.join(migrationDir, file);
|
|
284
284
|
var mig;
|
|
285
285
|
try {
|
|
286
286
|
// Operator-supplied migration file — by definition not statically
|
package/lib/db.js
CHANGED
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
* @card
|
|
42
42
|
* Database core — SQLite (node:sqlite) wrapped in encrypted-at-rest storage, sealed-column field-level crypto, append-only audit-chain integration, declarative schema reconcile, and run-once migrations.
|
|
43
43
|
*/
|
|
44
|
-
var
|
|
45
|
-
var
|
|
44
|
+
var nodeFs = require("fs");
|
|
45
|
+
var nodePath = require("path");
|
|
46
46
|
var { DatabaseSync } = require("node:sqlite");
|
|
47
47
|
var { Readable } = require("node:stream");
|
|
48
48
|
var atomicFile = require("./atomic-file");
|
|
@@ -92,7 +92,7 @@ var _dbErr = DbError.factory;
|
|
|
92
92
|
|
|
93
93
|
// Lazy: cluster-storage's _localDb pulls db back in, so eager require
|
|
94
94
|
// would deadlock the load order. cluster-storage is only used on the
|
|
95
|
-
// purge-audit-chain external-db
|
|
95
|
+
// purge-audit-chain external-db nodePath, which always runs after init.
|
|
96
96
|
var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
|
|
97
97
|
|
|
98
98
|
// Lazy refs for the test-reset cascade. Each module requires db.js
|
|
@@ -639,7 +639,7 @@ function resolveTmpDir(optsTmpDir) {
|
|
|
639
639
|
if (optsTmpDir) return optsTmpDir;
|
|
640
640
|
var envTmp = safeEnv.readVar("BLAMEJS_TMPDIR");
|
|
641
641
|
if (envTmp) return envTmp;
|
|
642
|
-
if (
|
|
642
|
+
if (nodeFs.existsSync("/dev/shm")) return "/dev/shm";
|
|
643
643
|
return null;
|
|
644
644
|
}
|
|
645
645
|
|
|
@@ -650,8 +650,8 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
|
650
650
|
// needs to live outside `dataDir` (e.g. a separate volume mounted
|
|
651
651
|
// from a KMS-fronted secret store). Default places it next to the
|
|
652
652
|
// encrypted DB so backup capture is one-tarball.
|
|
653
|
-
var keyPath = keyPathOverride ||
|
|
654
|
-
if (
|
|
653
|
+
var keyPath = keyPathOverride || nodePath.join(dataDirPath, "db.key.enc");
|
|
654
|
+
if (nodeFs.existsSync(keyPath)) {
|
|
655
655
|
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
656
656
|
var b64 = vault.unseal(sealed);
|
|
657
657
|
if (!b64) {
|
|
@@ -669,18 +669,18 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
|
669
669
|
}
|
|
670
670
|
|
|
671
671
|
function decryptToTmp() {
|
|
672
|
-
if (!encPath || !
|
|
672
|
+
if (!encPath || !nodeFs.existsSync(encPath)) return;
|
|
673
673
|
// If a plaintext file already exists in tmpfs from a prior process, prefer
|
|
674
674
|
// the newer mtime (crash recovery — operator's most recent state wins).
|
|
675
|
-
if (
|
|
676
|
-
var plainStat =
|
|
677
|
-
var encStat =
|
|
675
|
+
if (nodeFs.existsSync(dbPath)) {
|
|
676
|
+
var plainStat = nodeFs.statSync(dbPath);
|
|
677
|
+
var encStat = nodeFs.statSync(encPath);
|
|
678
678
|
if (plainStat.mtimeMs > encStat.mtimeMs && plainStat.size > 0) {
|
|
679
679
|
log("plaintext is newer than encrypted — keeping plaintext (crash recovery)");
|
|
680
680
|
return;
|
|
681
681
|
}
|
|
682
682
|
}
|
|
683
|
-
var packed =
|
|
683
|
+
var packed = nodeFs.readFileSync(encPath);
|
|
684
684
|
if (packed.length < 26) return; // too short to be a valid envelope
|
|
685
685
|
// AAD binds the envelope to this deployment's data dir so two
|
|
686
686
|
// installs sharing the same operator passphrase can't swap each
|
|
@@ -703,8 +703,8 @@ function encryptToDisk() {
|
|
|
703
703
|
if (!encPath) return;
|
|
704
704
|
// Force WAL checkpoint so the .db file holds all committed transactions.
|
|
705
705
|
try { runSql(database, "PRAGMA wal_checkpoint(TRUNCATE)"); } catch (_e) { /* best effort */ }
|
|
706
|
-
if (!
|
|
707
|
-
atomicFile.writeSync(encPath, encryptPacked(
|
|
706
|
+
if (!nodeFs.existsSync(dbPath)) return;
|
|
707
|
+
atomicFile.writeSync(encPath, encryptPacked(nodeFs.readFileSync(dbPath), encKey, _dbEncAad(dataDir)));
|
|
708
708
|
}
|
|
709
709
|
|
|
710
710
|
/**
|
|
@@ -737,11 +737,11 @@ function snapshot() {
|
|
|
737
737
|
// so the snapshot reflects the current logical state, not just the
|
|
738
738
|
// pre-WAL pages.
|
|
739
739
|
try { runSql(database, "PRAGMA wal_checkpoint(TRUNCATE)"); } catch (_e) { /* best effort */ }
|
|
740
|
-
if (!
|
|
740
|
+
if (!nodeFs.existsSync(dbPath)) {
|
|
741
741
|
throw _dbErr("db/snapshot-no-source",
|
|
742
742
|
"snapshot: plaintext DB at " + dbPath + " is missing — did init complete?");
|
|
743
743
|
}
|
|
744
|
-
var plain =
|
|
744
|
+
var plain = nodeFs.readFileSync(dbPath);
|
|
745
745
|
if (!encPath || !encKey) {
|
|
746
746
|
// atRest: 'plain' — return the raw bytes. Operators wanting an
|
|
747
747
|
// encrypted snapshot under plain mode wrap with their own
|
|
@@ -756,9 +756,9 @@ function snapshot() {
|
|
|
756
756
|
// database.close().
|
|
757
757
|
function removePlaintextFiles() {
|
|
758
758
|
if (!dbPath) return;
|
|
759
|
-
try {
|
|
760
|
-
try {
|
|
761
|
-
try {
|
|
759
|
+
try { nodeFs.unlinkSync(dbPath); } catch (_e) { /* cleanup */ }
|
|
760
|
+
try { nodeFs.unlinkSync(dbPath + "-wal"); } catch (_e) { /* cleanup */ }
|
|
761
|
+
try { nodeFs.unlinkSync(dbPath + "-shm"); } catch (_e) { /* cleanup */ }
|
|
762
762
|
}
|
|
763
763
|
|
|
764
764
|
// Clean up stale plaintext DB files left by previously-crashed processes.
|
|
@@ -771,9 +771,9 @@ function cleanStaleTmpDbs(tmpDir) {
|
|
|
771
771
|
for (var i = 0; i < entries.length; i++) {
|
|
772
772
|
var full = entries[i].fullPath;
|
|
773
773
|
if (full === dbPath) continue;
|
|
774
|
-
try {
|
|
775
|
-
try {
|
|
776
|
-
try {
|
|
774
|
+
try { nodeFs.unlinkSync(full); } catch (_e) { /* concurrent cleanup */ }
|
|
775
|
+
try { nodeFs.unlinkSync(full + "-wal"); } catch (_e) { /* may not exist */ }
|
|
776
|
+
try { nodeFs.unlinkSync(full + "-shm"); } catch (_e) { /* may not exist */ }
|
|
777
777
|
}
|
|
778
778
|
}
|
|
779
779
|
|
|
@@ -865,7 +865,7 @@ async function init(opts) {
|
|
|
865
865
|
streamLimit = opts.streamLimit;
|
|
866
866
|
}
|
|
867
867
|
dataDir = opts.dataDir;
|
|
868
|
-
if (!
|
|
868
|
+
if (!nodeFs.existsSync(dataDir)) nodeFs.mkdirSync(dataDir, { recursive: true });
|
|
869
869
|
|
|
870
870
|
if (atRest === "encrypted") {
|
|
871
871
|
var tmpDir = resolveTmpDir(opts.tmpDir);
|
|
@@ -874,7 +874,7 @@ async function init(opts) {
|
|
|
874
874
|
"FATAL: atRest: 'encrypted' (default) requires tmpfs but none was found. " +
|
|
875
875
|
"Provide opts.tmpDir or set BLAMEJS_TMPDIR, or pass atRest: 'plain' (with warning).");
|
|
876
876
|
}
|
|
877
|
-
if (!
|
|
877
|
+
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
878
878
|
|
|
879
879
|
// D-H7 — if the resolved tmpDir is NOT actually tmpfs, the
|
|
880
880
|
// plaintext DB file lives on persistent storage. statvfs/statfs
|
|
@@ -884,7 +884,7 @@ async function init(opts) {
|
|
|
884
884
|
// out-of-band.
|
|
885
885
|
if (process.platform === "linux") {
|
|
886
886
|
var realTmp = "";
|
|
887
|
-
try { realTmp =
|
|
887
|
+
try { realTmp = nodeFs.realpathSync(tmpDir); } catch (_e) { /* stat best-effort */ }
|
|
888
888
|
if (realTmp.indexOf("/dev/shm") !== 0 && realTmp.indexOf("/run/shm") !== 0 &&
|
|
889
889
|
realTmp.indexOf("/run/user/") !== 0 && realTmp.indexOf("/tmp") !== 0) {
|
|
890
890
|
log.warn("WARNING: db.init: tmpDir '" + tmpDir + "' (real: '" + realTmp +
|
|
@@ -894,13 +894,13 @@ async function init(opts) {
|
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
-
// Operator overrides for the encrypted-DB on-disk
|
|
898
|
-
// takes a fully-qualified
|
|
897
|
+
// Operator overrides for the encrypted-DB on-disk nodePath. `opts.encryptedDbPath`
|
|
898
|
+
// takes a fully-qualified nodePath; `opts.encryptedDbName` overrides
|
|
899
899
|
// just the basename under `dataDir` (default "db.enc"). Helps when
|
|
900
900
|
// multiple framework-shaped instances share a dataDir.
|
|
901
901
|
encPath = opts.encryptedDbPath ||
|
|
902
|
-
|
|
903
|
-
dbPath =
|
|
902
|
+
nodePath.join(dataDir, opts.encryptedDbName || "db.enc");
|
|
903
|
+
dbPath = nodePath.join(tmpDir, "blamejs-" + generateToken(C.BYTES.bytes(16)) + ".db");
|
|
904
904
|
encKey = loadOrCreateDbKey(dataDir, opts.dbKeyPath);
|
|
905
905
|
|
|
906
906
|
cleanStaleTmpDbs(tmpDir);
|
|
@@ -910,7 +910,7 @@ async function init(opts) {
|
|
|
910
910
|
log.warn("WARNING: atRest: 'plain' — DB structure and row counts visible on disk.");
|
|
911
911
|
log.warn(" Field-level encryption (sealedFields) still protects sealed columns,");
|
|
912
912
|
log.warn(" but the simpler at-rest model is opt-out only. Default is 'encrypted'.");
|
|
913
|
-
dbPath =
|
|
913
|
+
dbPath = nodePath.join(dataDir, "blamejs.db");
|
|
914
914
|
encPath = null;
|
|
915
915
|
encKey = null;
|
|
916
916
|
}
|
|
@@ -1479,7 +1479,7 @@ function stream(sql) {
|
|
|
1479
1479
|
this.destroy(new DbError("db/stream-limit-exceeded",
|
|
1480
1480
|
"db.stream: emitted " + emitted + " rows, exceeding streamLimit " +
|
|
1481
1481
|
perCallLimit + ". Pass opts.streamLimit higher OR raise via " +
|
|
1482
|
-
"db.init({ streamLimit }) after auditing the export
|
|
1482
|
+
"db.init({ streamLimit }) after auditing the export nodePath."));
|
|
1483
1483
|
return;
|
|
1484
1484
|
}
|
|
1485
1485
|
var step = iter.next();
|
|
@@ -1499,7 +1499,7 @@ function stream(sql) {
|
|
|
1499
1499
|
// review can reconstruct schema evolution from the chain alone (D-M1).
|
|
1500
1500
|
var DDL_RE = /^\s*(CREATE|DROP|ALTER|TRUNCATE|RENAME|ATTACH|DETACH|REINDEX)\b/i;
|
|
1501
1501
|
|
|
1502
|
-
// D-L7 — slow-query observability buckets for the local SQLite
|
|
1502
|
+
// D-L7 — slow-query observability buckets for the local SQLite nodePath.
|
|
1503
1503
|
// Highest matched bucket wins so the per-query emit is single-shot;
|
|
1504
1504
|
// operators dashboard on the `bucket` label.
|
|
1505
1505
|
var _SLOW_QUERY_BUCKETS_LOCAL = Object.freeze([
|
|
@@ -1907,7 +1907,7 @@ function exportCsv(opts) {
|
|
|
1907
1907
|
* cluster leader, re-encrypts the live tmpfs database back to
|
|
1908
1908
|
* `<dataDir>/db.enc`, closes the SQLite handle (releasing the file
|
|
1909
1909
|
* lock on Windows), then unlinks the plaintext sidecar files in
|
|
1910
|
-
*
|
|
1910
|
+
* tmpnodeFs. Safe to call multiple times — no-ops after the first
|
|
1911
1911
|
* successful close.
|
|
1912
1912
|
*
|
|
1913
1913
|
* @example
|
|
@@ -2327,8 +2327,8 @@ function eraseHard(tableName, rowId, opts) {
|
|
|
2327
2327
|
// Read the audit.tip sidecar file in dataDir and compare to the current
|
|
2328
2328
|
// audit_log MAX(monotonicCounter). Refuse boot on rollback (current < tip).
|
|
2329
2329
|
function _checkRollback(dataDirPath) {
|
|
2330
|
-
var tipPath =
|
|
2331
|
-
if (!
|
|
2330
|
+
var tipPath = nodePath.join(dataDirPath, "audit.tip");
|
|
2331
|
+
if (!nodeFs.existsSync(tipPath)) {
|
|
2332
2332
|
log("no audit.tip sidecar — skipping rollback check (first boot or operator-cleared)");
|
|
2333
2333
|
return;
|
|
2334
2334
|
}
|
|
@@ -3104,7 +3104,7 @@ module.exports = {
|
|
|
3104
3104
|
// Helper for audit.checkpoint to write the rollback-detection sidecar
|
|
3105
3105
|
_writeAuditTip: function (tip) {
|
|
3106
3106
|
if (!dataDir) return;
|
|
3107
|
-
var tipPath =
|
|
3107
|
+
var tipPath = nodePath.join(dataDir, "audit.tip");
|
|
3108
3108
|
atomicFile.writeSync(tipPath, JSON.stringify(tip, null, 2), { fileMode: 0o600 });
|
|
3109
3109
|
},
|
|
3110
3110
|
};
|