@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/vault/rotate.js
CHANGED
|
@@ -48,18 +48,18 @@
|
|
|
48
48
|
* sampler skips them.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
var
|
|
52
|
-
var
|
|
51
|
+
var nodeFs = require("fs");
|
|
52
|
+
var nodePath = require("path");
|
|
53
53
|
var { DatabaseSync } = require("node:sqlite");
|
|
54
54
|
var atomicFile = require("../atomic-file");
|
|
55
55
|
var safeSql = require("../safe-sql");
|
|
56
56
|
var C = require("../constants");
|
|
57
57
|
var cryptoField = require("../crypto-field");
|
|
58
|
-
var
|
|
58
|
+
var bCrypto = require("../crypto");
|
|
59
59
|
var dbSchema = require("../db-schema");
|
|
60
60
|
var lazyRequire = require("../lazy-require");
|
|
61
61
|
var { boot } = require("../log");
|
|
62
|
-
var
|
|
62
|
+
var numericBounds = require("../numeric-bounds");
|
|
63
63
|
var safeJson = require("../safe-json");
|
|
64
64
|
var validateOpts = require("../validate-opts");
|
|
65
65
|
var vaultWrap = lazyRequire(function () { return require("./wrap"); });
|
|
@@ -109,7 +109,7 @@ function _knownColumnsFor(schema, infraColumns) {
|
|
|
109
109
|
|
|
110
110
|
function validateSchemaMatch(db, opts) {
|
|
111
111
|
opts = opts || {};
|
|
112
|
-
|
|
112
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.driftSampleLimit,
|
|
113
113
|
"validateSchemaMatch: driftSampleLimit", VaultRotateError, "vault-rotate/bad-opt");
|
|
114
114
|
var sampleLimit = opts.driftSampleLimit !== undefined
|
|
115
115
|
? opts.driftSampleLimit : DEFAULT_DRIFT_SAMPLE_LIMIT;
|
|
@@ -254,7 +254,7 @@ function verify(opts) {
|
|
|
254
254
|
var keys = opts.keys;
|
|
255
255
|
var db = opts.db;
|
|
256
256
|
var oldKeys = opts.oldKeys || null;
|
|
257
|
-
|
|
257
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.sampleMin,
|
|
258
258
|
"verify: sampleMin", VaultRotateError, "vault-rotate/bad-opt");
|
|
259
259
|
var sampleMin = opts.sampleMin !== undefined
|
|
260
260
|
? opts.sampleMin : DEFAULT_VERIFY_SAMPLE_MIN;
|
|
@@ -263,7 +263,7 @@ function verify(opts) {
|
|
|
263
263
|
opts.samplePercent <= 0)) {
|
|
264
264
|
throw new VaultRotateError("vault-rotate/bad-opt",
|
|
265
265
|
"verify: samplePercent must be a positive finite fraction; got " +
|
|
266
|
-
|
|
266
|
+
numericBounds.shape(opts.samplePercent));
|
|
267
267
|
}
|
|
268
268
|
var samplePct = opts.samplePercent !== undefined
|
|
269
269
|
? opts.samplePercent : DEFAULT_VERIFY_SAMPLE_FRAC;
|
|
@@ -311,7 +311,7 @@ function verify(opts) {
|
|
|
311
311
|
if (typeof v !== "string" || v.indexOf(VAULT_PREFIX) !== 0) continue;
|
|
312
312
|
var payload = v.substring(VAULT_PREFIX.length);
|
|
313
313
|
|
|
314
|
-
try {
|
|
314
|
+
try { bCrypto.decrypt(payload, keys); }
|
|
315
315
|
catch (e) {
|
|
316
316
|
rowFailed = true;
|
|
317
317
|
failures.push({
|
|
@@ -324,7 +324,7 @@ function verify(opts) {
|
|
|
324
324
|
|
|
325
325
|
if (oldKeys && !foundOldFail) {
|
|
326
326
|
try {
|
|
327
|
-
|
|
327
|
+
bCrypto.decrypt(payload, oldKeys);
|
|
328
328
|
regressions.push({
|
|
329
329
|
table: table,
|
|
330
330
|
column: col,
|
|
@@ -391,8 +391,8 @@ function _emit(cb, ev) {
|
|
|
391
391
|
// the original write fd around.
|
|
392
392
|
function _fsyncFileByPath(p) {
|
|
393
393
|
try {
|
|
394
|
-
var fd =
|
|
395
|
-
try {
|
|
394
|
+
var fd = nodeFs.openSync(p, "r+");
|
|
395
|
+
try { nodeFs.fsyncSync(fd); } finally { nodeFs.closeSync(fd); }
|
|
396
396
|
} catch (_e) { /* best-effort across platforms */ }
|
|
397
397
|
}
|
|
398
398
|
|
|
@@ -400,8 +400,8 @@ function _reSealValue(sealedValue, oldKeys, newKeys) {
|
|
|
400
400
|
if (typeof sealedValue !== "string") return sealedValue;
|
|
401
401
|
if (sealedValue.indexOf(C.VAULT_PREFIX) !== 0) return sealedValue;
|
|
402
402
|
var payload = sealedValue.substring(VAULT_PREFIX_LEN);
|
|
403
|
-
var plain =
|
|
404
|
-
return C.VAULT_PREFIX +
|
|
403
|
+
var plain = bCrypto.decrypt(payload, oldKeys);
|
|
404
|
+
return C.VAULT_PREFIX + bCrypto.encrypt(plain, newKeys);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
// Walk a JSON-decoded value, re-sealing every vault-prefixed string.
|
|
@@ -530,12 +530,12 @@ async function rotate(opts) {
|
|
|
530
530
|
throw new VaultRotateError("vault-rotate/no-keys",
|
|
531
531
|
"rotate: opts.oldKeys and opts.newKeys are required");
|
|
532
532
|
}
|
|
533
|
-
if (typeof opts.dataDir !== "string" || !
|
|
533
|
+
if (typeof opts.dataDir !== "string" || !nodeFs.existsSync(opts.dataDir)) {
|
|
534
534
|
throw new VaultRotateError("vault-rotate/no-datadir",
|
|
535
535
|
"rotate: opts.dataDir is required and must exist");
|
|
536
536
|
}
|
|
537
537
|
validateOpts.requireNonEmptyString(opts.stagingDir, "rotate: opts.stagingDir", VaultRotateError, "vault-rotate/no-staging");
|
|
538
|
-
if (
|
|
538
|
+
if (nodeFs.existsSync(opts.stagingDir)) {
|
|
539
539
|
throw new VaultRotateError("vault-rotate/staging-exists",
|
|
540
540
|
"rotate: stagingDir already exists: " + opts.stagingDir);
|
|
541
541
|
}
|
|
@@ -571,30 +571,30 @@ async function rotate(opts) {
|
|
|
571
571
|
_emit(progress, { phase: "copy_verbatim" });
|
|
572
572
|
for (var vf = 0; vf < paths.verbatimFiles.length; vf++) {
|
|
573
573
|
var entry = paths.verbatimFiles[vf];
|
|
574
|
-
var src =
|
|
575
|
-
if (!
|
|
574
|
+
var src = nodePath.join(dataDir, entry.relativePath);
|
|
575
|
+
if (!nodeFs.existsSync(src)) {
|
|
576
576
|
if (entry.required) {
|
|
577
577
|
throw new VaultRotateError("vault-rotate/missing-verbatim",
|
|
578
578
|
"rotate: required verbatim file missing: " + entry.relativePath);
|
|
579
579
|
}
|
|
580
580
|
continue;
|
|
581
581
|
}
|
|
582
|
-
var dest =
|
|
583
|
-
atomicFile.ensureDir(
|
|
584
|
-
|
|
582
|
+
var dest = nodePath.join(stagingDir, entry.relativePath);
|
|
583
|
+
atomicFile.ensureDir(nodePath.dirname(dest));
|
|
584
|
+
nodeFs.copyFileSync(src, dest);
|
|
585
585
|
}
|
|
586
586
|
for (var vd = 0; vd < paths.verbatimDirs.length; vd++) {
|
|
587
587
|
var dent = paths.verbatimDirs[vd];
|
|
588
|
-
var sdir =
|
|
589
|
-
if (!
|
|
588
|
+
var sdir = nodePath.join(dataDir, dent.relativePath);
|
|
589
|
+
if (!nodeFs.existsSync(sdir)) {
|
|
590
590
|
if (dent.required) {
|
|
591
591
|
throw new VaultRotateError("vault-rotate/missing-verbatim-dir",
|
|
592
592
|
"rotate: required verbatim dir missing: " + dent.relativePath);
|
|
593
593
|
}
|
|
594
594
|
continue;
|
|
595
595
|
}
|
|
596
|
-
if (
|
|
597
|
-
atomicFile.copyDirRecursive(sdir,
|
|
596
|
+
if (nodeFs.existsSync(sdir)) {
|
|
597
|
+
atomicFile.copyDirRecursive(sdir, nodePath.join(stagingDir, dent.relativePath));
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
|
|
@@ -603,59 +603,59 @@ async function rotate(opts) {
|
|
|
603
603
|
var keysJson = JSON.stringify(newKeys, null, 2);
|
|
604
604
|
if (mode === "wrapped") {
|
|
605
605
|
var sealed = await vaultWrap().wrap(keysJson, opts.newPassphrase);
|
|
606
|
-
|
|
606
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.vaultKeySealed), sealed, { mode: 0o600 });
|
|
607
607
|
} else {
|
|
608
|
-
|
|
608
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.vaultKeyPlain), keysJson, { mode: 0o600 });
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
// 3. re-seal db.key.enc + any operator-supplied additionalSealed files
|
|
612
612
|
_emit(progress, { phase: "reseal_files" });
|
|
613
|
-
var dbKeySealedPath =
|
|
613
|
+
var dbKeySealedPath = nodePath.join(dataDir, paths.dbKeySealed);
|
|
614
614
|
var dbKey = null;
|
|
615
|
-
if (
|
|
616
|
-
var sealedKey =
|
|
615
|
+
if (nodeFs.existsSync(dbKeySealedPath)) {
|
|
616
|
+
var sealedKey = nodeFs.readFileSync(dbKeySealedPath, "utf8").trim();
|
|
617
617
|
if (sealedKey.indexOf(C.VAULT_PREFIX) !== 0) {
|
|
618
618
|
throw new VaultRotateError("vault-rotate/bad-dbkey",
|
|
619
619
|
"rotate: db.key.enc does not start with the vault prefix");
|
|
620
620
|
}
|
|
621
|
-
var dbKeyB64 =
|
|
621
|
+
var dbKeyB64 = bCrypto.decrypt(sealedKey.substring(VAULT_PREFIX_LEN), oldKeys);
|
|
622
622
|
dbKey = Buffer.from(dbKeyB64, "base64");
|
|
623
|
-
var resealedKey = C.VAULT_PREFIX +
|
|
624
|
-
|
|
623
|
+
var resealedKey = C.VAULT_PREFIX + bCrypto.encrypt(dbKeyB64, newKeys);
|
|
624
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.dbKeySealed), resealedKey, { mode: 0o600 });
|
|
625
625
|
}
|
|
626
626
|
for (var as = 0; as < paths.additionalSealed.length; as++) {
|
|
627
627
|
var ase = paths.additionalSealed[as];
|
|
628
|
-
var asSrc =
|
|
629
|
-
if (!
|
|
628
|
+
var asSrc = nodePath.join(dataDir, ase.relativePath);
|
|
629
|
+
if (!nodeFs.existsSync(asSrc)) {
|
|
630
630
|
if (ase.required) {
|
|
631
631
|
throw new VaultRotateError("vault-rotate/missing-sealed",
|
|
632
632
|
"rotate: required sealed file missing: " + ase.relativePath);
|
|
633
633
|
}
|
|
634
634
|
continue;
|
|
635
635
|
}
|
|
636
|
-
var current =
|
|
636
|
+
var current = nodeFs.readFileSync(asSrc, "utf8").trim();
|
|
637
637
|
if (current.indexOf(C.VAULT_PREFIX) !== 0) {
|
|
638
638
|
throw new VaultRotateError("vault-rotate/bad-sealed",
|
|
639
639
|
"rotate: sealed file does not start with the vault prefix: " + ase.relativePath);
|
|
640
640
|
}
|
|
641
|
-
var asDestDir =
|
|
642
|
-
if (!
|
|
643
|
-
|
|
641
|
+
var asDestDir = nodePath.join(stagingDir, nodePath.dirname(ase.relativePath));
|
|
642
|
+
if (!nodeFs.existsSync(asDestDir)) atomicFile.ensureDir(asDestDir);
|
|
643
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, ase.relativePath),
|
|
644
644
|
_reSealValue(current, oldKeys, newKeys), { mode: 0o600 });
|
|
645
645
|
}
|
|
646
646
|
|
|
647
647
|
// 4. decrypt + rotate + re-encrypt db.enc
|
|
648
648
|
_emit(progress, { phase: "rotate_db" });
|
|
649
|
-
var encDbPath =
|
|
649
|
+
var encDbPath = nodePath.join(dataDir, paths.encryptedDb);
|
|
650
650
|
var tablesProcessed = 0;
|
|
651
651
|
var totalRowsProcessed = 0;
|
|
652
652
|
var verifyResult = null;
|
|
653
653
|
|
|
654
|
-
if (
|
|
655
|
-
var packed =
|
|
656
|
-
var plainBytes =
|
|
657
|
-
var tmpDbPath =
|
|
658
|
-
|
|
654
|
+
if (nodeFs.existsSync(encDbPath) && dbKey) {
|
|
655
|
+
var packed = nodeFs.readFileSync(encDbPath);
|
|
656
|
+
var plainBytes = bCrypto.decryptPacked(packed, dbKey);
|
|
657
|
+
var tmpDbPath = nodePath.join(stagingDir, "_blamejs_rotate.tmp.db");
|
|
658
|
+
nodeFs.writeFileSync(tmpDbPath, plainBytes, { mode: 0o600 });
|
|
659
659
|
|
|
660
660
|
var db = new DatabaseSync(tmpDbPath);
|
|
661
661
|
try {
|
|
@@ -708,32 +708,32 @@ async function rotate(opts) {
|
|
|
708
708
|
// sidecar may be absent (depending on whether journal_mode produced
|
|
709
709
|
// one for this run); log at debug so the cleanup attempt isn't
|
|
710
710
|
// silently swallowed when something genuinely unexpected fails.
|
|
711
|
-
try {
|
|
712
|
-
catch (e) { rotateLog.debug("cleanup-failed", { op: "
|
|
713
|
-
try {
|
|
714
|
-
catch (e) { rotateLog.debug("cleanup-failed", { op: "
|
|
711
|
+
try { nodeFs.unlinkSync(tmpDbPath + "-wal"); }
|
|
712
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: tmpDbPath + "-wal", error: e.message }); }
|
|
713
|
+
try { nodeFs.unlinkSync(tmpDbPath + "-shm"); }
|
|
714
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: tmpDbPath + "-shm", error: e.message }); }
|
|
715
715
|
|
|
716
|
-
var rotatedBytes =
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
716
|
+
var rotatedBytes = nodeFs.readFileSync(tmpDbPath);
|
|
717
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.encryptedDb),
|
|
718
|
+
bCrypto.encryptPacked(rotatedBytes, dbKey));
|
|
719
|
+
nodeFs.unlinkSync(tmpDbPath);
|
|
720
720
|
|
|
721
721
|
// Round-trip verify on the staged DB
|
|
722
722
|
_emit(progress, { phase: "verify" });
|
|
723
|
-
var verifyTmp =
|
|
724
|
-
|
|
725
|
-
|
|
723
|
+
var verifyTmp = nodePath.join(stagingDir, "_blamejs_verify.tmp.db");
|
|
724
|
+
nodeFs.writeFileSync(verifyTmp,
|
|
725
|
+
bCrypto.decryptPacked(nodeFs.readFileSync(nodePath.join(stagingDir, paths.encryptedDb)), dbKey));
|
|
726
726
|
var vdb = new DatabaseSync(verifyTmp);
|
|
727
727
|
try {
|
|
728
728
|
verifyResult = verify({ keys: newKeys, db: vdb, oldKeys: oldKeys });
|
|
729
729
|
} finally {
|
|
730
730
|
vdb.close();
|
|
731
|
-
try {
|
|
732
|
-
catch (e) { rotateLog.debug("cleanup-failed", { op: "
|
|
733
|
-
try {
|
|
734
|
-
catch (e) { rotateLog.debug("cleanup-failed", { op: "
|
|
735
|
-
try {
|
|
736
|
-
catch (e) { rotateLog.debug("cleanup-failed", { op: "
|
|
731
|
+
try { nodeFs.unlinkSync(verifyTmp); }
|
|
732
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: verifyTmp, error: e.message }); }
|
|
733
|
+
try { nodeFs.unlinkSync(verifyTmp + "-wal"); }
|
|
734
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: verifyTmp + "-wal", error: e.message }); }
|
|
735
|
+
try { nodeFs.unlinkSync(verifyTmp + "-shm"); }
|
|
736
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: verifyTmp + "-shm", error: e.message }); }
|
|
737
737
|
}
|
|
738
738
|
if (!verifyResult.ok) {
|
|
739
739
|
throw new VaultRotateError("vault-rotate/verify-failed",
|
|
@@ -747,10 +747,10 @@ async function rotate(opts) {
|
|
|
747
747
|
// 5. fsync staging for durability before caller does the swap
|
|
748
748
|
_emit(progress, { phase: "fsync" });
|
|
749
749
|
function fsyncTree(dir) {
|
|
750
|
-
var entries =
|
|
750
|
+
var entries = nodeFs.readdirSync(dir);
|
|
751
751
|
for (var i = 0; i < entries.length; i++) {
|
|
752
|
-
var p =
|
|
753
|
-
var st =
|
|
752
|
+
var p = nodePath.join(dir, entries[i]);
|
|
753
|
+
var st = nodeFs.statSync(p);
|
|
754
754
|
if (st.isFile()) _fsyncFileByPath(p);
|
|
755
755
|
else if (st.isDirectory()) fsyncTree(p);
|
|
756
756
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* source: "/etc/letsencrypt/live/example.com/privkey.pem",
|
|
20
20
|
* destination: "/var/lib/blamejs/server.key.sealed",
|
|
21
21
|
* audit: true, // default
|
|
22
|
-
* pollInterval: b.constants.TIME.seconds(2), //
|
|
22
|
+
* pollInterval: b.constants.TIME.seconds(2), // nodeFs.watchFile cadence
|
|
23
23
|
* onResealed: function (info) { ... }, // { srcPath, destPath, bytes,
|
|
24
24
|
* resealedAt, generation }
|
|
25
25
|
* onError: function (err) { ... }, // sealing failed
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
* (rename did not happen). The recovery routine re-runs the seal from
|
|
43
43
|
* source — idempotent because the source PEM is the source of truth.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
45
|
+
* nodeFs.watchFile semantics:
|
|
46
46
|
*
|
|
47
|
-
* Node's
|
|
48
|
-
* pollInterval. It fires on mtime / size change.
|
|
47
|
+
* Node's nodeFs.watchFile is a polling stat() loop with the configured
|
|
48
|
+
* pollInterval. It fires on mtime / size change. nodeFs.watch (the
|
|
49
49
|
* inotify / kqueue backend) is more efficient but inconsistent across
|
|
50
50
|
* platforms — single rename events surface as multiple change events
|
|
51
51
|
* on Linux (events fire on the directory entry, the file, and the
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
* pollInterval) is acceptable for renewal cadences measured in days.
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
var
|
|
58
|
-
var
|
|
57
|
+
var nodeFs = require("fs");
|
|
58
|
+
var nodePath = require("path");
|
|
59
59
|
var atomicFile = require("../atomic-file");
|
|
60
60
|
var C = require("../constants");
|
|
61
61
|
var lazyRequire = require("../lazy-require");
|
|
@@ -76,7 +76,7 @@ var SealPemFileError = defineClass("SealPemFileError", { alwaysPermanent: true }
|
|
|
76
76
|
// 2-second worst-case re-seal latency — negligible against the
|
|
77
77
|
// renewal cadence. Operators with sub-second-sensitive use cases
|
|
78
78
|
// override via opts.pollInterval.
|
|
79
|
-
// H6 #6 —
|
|
79
|
+
// H6 #6 — nodeFs.watchFile default cadence reduced from 2s to 500ms so a
|
|
80
80
|
// fast renewal-then-revert (mtime bump then second bump within ~2s)
|
|
81
81
|
// doesn't sneak past the watcher. Operators with extremely-quiet
|
|
82
82
|
// renewal cycles can override via opts.pollInterval; the cost of
|
|
@@ -126,7 +126,7 @@ var DEFAULT_MAX_SOURCE_BYTES = C.BYTES.mib(1);
|
|
|
126
126
|
* source: string, // plaintext PEM path (required)
|
|
127
127
|
* destination: string, // sealed-output path (required, must differ from source)
|
|
128
128
|
* audit: boolean, // emit b.audit events on every reseal (default true)
|
|
129
|
-
* pollInterval: number, //
|
|
129
|
+
* pollInterval: number, // nodeFs.watchFile cadence in ms (default 500)
|
|
130
130
|
* onResealed: function, // (info) => void — { srcPath, destPath, bytes, resealedAt, generation }
|
|
131
131
|
* onError: function, // (err) => void — sealing failed
|
|
132
132
|
* maxSourceBytes: number, // refuse source larger than this (default 1 MiB)
|
|
@@ -219,7 +219,7 @@ function sealPemFile(opts) {
|
|
|
219
219
|
// marker create and marker remove, the marker remains on disk
|
|
220
220
|
// and _recoverIfNeeded() detects it on the next start().
|
|
221
221
|
var markerPath = destination + ".rewriting";
|
|
222
|
-
var destDir =
|
|
222
|
+
var destDir = nodePath.dirname(destination);
|
|
223
223
|
atomicFile.ensureDir(destDir);
|
|
224
224
|
// H6 #4 — assert parent-dir mode. If the directory is world-
|
|
225
225
|
// writable, an attacker can swap the destination file or the
|
|
@@ -229,7 +229,7 @@ function sealPemFile(opts) {
|
|
|
229
229
|
// skip the check there.
|
|
230
230
|
if (process.platform !== "win32") {
|
|
231
231
|
try {
|
|
232
|
-
var dirStat =
|
|
232
|
+
var dirStat = nodeFs.statSync(destDir);
|
|
233
233
|
if ((dirStat.mode & 0o022) !== 0) { // allow:raw-byte-literal — POSIX mode mask
|
|
234
234
|
throw new SealPemFileError("seal-pem-file/parent-dir-writable",
|
|
235
235
|
"destination parent dir '" + destDir + "' is group/other-writable " +
|
|
@@ -242,23 +242,23 @@ function sealPemFile(opts) {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
var sealed = vault().seal(plaintextBytes);
|
|
245
|
-
|
|
245
|
+
nodeFs.writeFileSync(markerPath, String(Date.now()), { mode: 0o600 }); // allow:raw-byte-literal — POSIX file mode
|
|
246
246
|
try {
|
|
247
247
|
atomicFile.writeSync(destination, sealed, { fileMode: 0o600 }); // allow:raw-byte-literal — POSIX file mode
|
|
248
248
|
} catch (e) {
|
|
249
|
-
try {
|
|
249
|
+
try { nodeFs.unlinkSync(markerPath); } catch (_e) { /* best-effort */ }
|
|
250
250
|
throw e;
|
|
251
251
|
}
|
|
252
|
-
try {
|
|
252
|
+
try { nodeFs.unlinkSync(markerPath); } catch (_e) { /* marker cleanup best-effort */ }
|
|
253
253
|
// H6 #5 — fsync the destination directory so the rename + marker
|
|
254
254
|
// unlink survive a power loss. Crash + backup-snapshot edge case:
|
|
255
255
|
// without dir-fsync, a journaled fs may have the new file inode
|
|
256
256
|
// but not the directory entry update by the time the snapshot
|
|
257
257
|
// reads.
|
|
258
258
|
try {
|
|
259
|
-
var dirFd =
|
|
260
|
-
try {
|
|
261
|
-
finally {
|
|
259
|
+
var dirFd = nodeFs.openSync(destDir, "r");
|
|
260
|
+
try { nodeFs.fsyncSync(dirFd); }
|
|
261
|
+
finally { nodeFs.closeSync(dirFd); }
|
|
262
262
|
} catch (_e) { /* dir fsync best-effort — Windows / non-POSIX may refuse */ }
|
|
263
263
|
}
|
|
264
264
|
|
|
@@ -267,14 +267,14 @@ function sealPemFile(opts) {
|
|
|
267
267
|
resealing = true;
|
|
268
268
|
var plaintext = null;
|
|
269
269
|
try {
|
|
270
|
-
// H6 #1 — bounded read.
|
|
270
|
+
// H6 #1 — bounded read. nodeFs.readFileSync without a size cap on a
|
|
271
271
|
// file the operator's renewal process writes is an OOM vector.
|
|
272
|
-
// H6 #3 — symlink TOCTOU defense. Open the file via
|
|
272
|
+
// H6 #3 — symlink TOCTOU defense. Open the file via nodeFs.openSync
|
|
273
273
|
// with O_NOFOLLOW where possible; lstat first to verify the
|
|
274
274
|
// source isn't a symlink we don't expect, then read via fd so
|
|
275
275
|
// a swap-after-stat doesn't change which bytes we read.
|
|
276
276
|
try {
|
|
277
|
-
var lstat =
|
|
277
|
+
var lstat = nodeFs.lstatSync(source);
|
|
278
278
|
if (lstat.isSymbolicLink()) {
|
|
279
279
|
throw new SealPemFileError("seal-pem-file/symlink-refused",
|
|
280
280
|
"source is a symlink (refused; follow + re-stat opens TOCTOU)");
|
|
@@ -283,9 +283,9 @@ function sealPemFile(opts) {
|
|
|
283
283
|
throw new SealPemFileError("seal-pem-file/source-too-large",
|
|
284
284
|
"source size " + lstat.size + " exceeds maxSourceBytes " + maxSourceBytes);
|
|
285
285
|
}
|
|
286
|
-
var fd =
|
|
286
|
+
var fd = nodeFs.openSync(source, "r");
|
|
287
287
|
try {
|
|
288
|
-
var fstat =
|
|
288
|
+
var fstat = nodeFs.fstatSync(fd);
|
|
289
289
|
// H6 #3 — confirm the fd points at the same inode lstat saw.
|
|
290
290
|
if (fstat.ino !== lstat.ino || fstat.size > maxSourceBytes) {
|
|
291
291
|
throw new SealPemFileError("seal-pem-file/toctou-detected",
|
|
@@ -294,7 +294,7 @@ function sealPemFile(opts) {
|
|
|
294
294
|
plaintext = Buffer.alloc(fstat.size);
|
|
295
295
|
var read = 0;
|
|
296
296
|
while (read < fstat.size) {
|
|
297
|
-
var n =
|
|
297
|
+
var n = nodeFs.readSync(fd, plaintext, read, fstat.size - read, null);
|
|
298
298
|
if (n === 0) break;
|
|
299
299
|
read += n;
|
|
300
300
|
}
|
|
@@ -303,7 +303,7 @@ function sealPemFile(opts) {
|
|
|
303
303
|
"short read: " + read + " of " + fstat.size + " bytes");
|
|
304
304
|
}
|
|
305
305
|
} finally {
|
|
306
|
-
try {
|
|
306
|
+
try { nodeFs.closeSync(fd); } catch (_e) { /* close best-effort */ }
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
catch (e) {
|
|
@@ -390,7 +390,7 @@ function sealPemFile(opts) {
|
|
|
390
390
|
// reseal was interrupted. Re-seal from source idempotently.
|
|
391
391
|
function _recoverIfNeeded() {
|
|
392
392
|
var markerPath = destination + ".rewriting";
|
|
393
|
-
if (
|
|
393
|
+
if (nodeFs.existsSync(markerPath)) {
|
|
394
394
|
log.info("vault.sealPemFile: recovery — marker '" + markerPath +
|
|
395
395
|
"' present from prior crashed reseal; re-sealing from source");
|
|
396
396
|
_emitAudit("recovery_started", "success", {
|
|
@@ -414,7 +414,7 @@ function sealPemFile(opts) {
|
|
|
414
414
|
_resealNow();
|
|
415
415
|
}
|
|
416
416
|
};
|
|
417
|
-
|
|
417
|
+
nodeFs.watchFile(source, { persistent: false, interval: pollInterval }, listener);
|
|
418
418
|
watching = true;
|
|
419
419
|
_emitAudit("watch_started", "success", {
|
|
420
420
|
source: source,
|
|
@@ -425,7 +425,7 @@ function sealPemFile(opts) {
|
|
|
425
425
|
|
|
426
426
|
function stop() {
|
|
427
427
|
if (!watching) return;
|
|
428
|
-
|
|
428
|
+
nodeFs.unwatchFile(source, listener);
|
|
429
429
|
listener = null;
|
|
430
430
|
watching = false;
|
|
431
431
|
_emitAudit("watch_stopped", "success", {
|
package/lib/watcher.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* b.watcher — recursive filesystem-watch primitive with cross-platform
|
|
4
4
|
* event normalization.
|
|
5
5
|
*
|
|
6
|
-
* Wraps `
|
|
6
|
+
* Wraps `nodeFs.watch(root, { recursive: true })` and turns the per-platform
|
|
7
7
|
* event soup (Linux inotify "rename" + "change", macOS FSEvents
|
|
8
8
|
* coalesced "rename", Windows ReadDirectoryChangesW pure "rename" /
|
|
9
9
|
* "change") into a single shape:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `type` is one of "file" or "dir". The watcher is build-tool-shaped:
|
|
16
16
|
* use it to drive incremental rebuilds, hot-reload-on-change,
|
|
17
17
|
* config-file watching, or content-store cache busts. It is NOT a
|
|
18
|
-
* security primitive —
|
|
18
|
+
* security primitive — nodeFs.watch is best-effort across kernels and the
|
|
19
19
|
* caller must not rely on it for audit-grade change detection.
|
|
20
20
|
*
|
|
21
21
|
* Cross-platform notes baked in:
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
* watcher.WatcherError
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
var
|
|
49
|
-
var
|
|
48
|
+
var nodeFs = require("fs");
|
|
49
|
+
var nodePath = require("path");
|
|
50
50
|
var lazyRequire = require("./lazy-require");
|
|
51
51
|
var validateOpts = require("./validate-opts");
|
|
52
52
|
var { WatcherError } = require("./framework-error");
|
|
@@ -56,7 +56,7 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
56
56
|
|
|
57
57
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
58
58
|
// Polling-mode defaults. The polling backend exists for environments
|
|
59
|
-
// where
|
|
59
|
+
// where nodeFs.watch's native events don't reach userspace — most commonly
|
|
60
60
|
// Docker Desktop bind-mounts on Windows / macOS hosts (where the
|
|
61
61
|
// inotify events from the Linux container's mount don't propagate
|
|
62
62
|
// through the gRPC-FUSE / VirtioFS bridge to the host fs), or NFS /
|
|
@@ -166,8 +166,8 @@ function _compileIgnore(patterns) {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
return function (relPath) {
|
|
169
|
-
var base =
|
|
170
|
-
var normalized = relPath.split(
|
|
169
|
+
var base = nodePath.basename(relPath);
|
|
170
|
+
var normalized = relPath.split(nodePath.sep).join("/");
|
|
171
171
|
for (var j = 0; j < compiled.length; j += 1) {
|
|
172
172
|
var c = compiled[j];
|
|
173
173
|
if (c.kind === "exact" && (c.value === relPath || c.value === normalized)) return true;
|
|
@@ -212,7 +212,7 @@ function _validateOpts(opts) {
|
|
|
212
212
|
function create(opts) {
|
|
213
213
|
_validateOpts(opts);
|
|
214
214
|
|
|
215
|
-
var root =
|
|
215
|
+
var root = nodePath.resolve(opts.root);
|
|
216
216
|
var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
217
217
|
var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
|
|
218
218
|
var mode = opts.mode || "fs";
|
|
@@ -226,7 +226,7 @@ function create(opts) {
|
|
|
226
226
|
|
|
227
227
|
// Pre-flight: root must exist and be a directory.
|
|
228
228
|
var rootStat;
|
|
229
|
-
try { rootStat =
|
|
229
|
+
try { rootStat = nodeFs.statSync(root); }
|
|
230
230
|
catch (e) {
|
|
231
231
|
throw new WatcherError("watcher/root-missing",
|
|
232
232
|
"watcher.create: root '" + root + "' is not accessible: " + ((e && e.message) || String(e)));
|
|
@@ -260,10 +260,10 @@ function create(opts) {
|
|
|
260
260
|
function _normalizeAndDispatch(relPath) {
|
|
261
261
|
if (stopped) return;
|
|
262
262
|
if (isIgnored(relPath)) return;
|
|
263
|
-
var fullPath =
|
|
263
|
+
var fullPath = nodePath.join(root, relPath);
|
|
264
264
|
// lstat (NOT stat) — refuses to follow symlinks out of root.
|
|
265
265
|
var lst;
|
|
266
|
-
try { lst =
|
|
266
|
+
try { lst = nodeFs.lstatSync(fullPath); }
|
|
267
267
|
catch (e) {
|
|
268
268
|
if (e && e.code === "ENOENT") {
|
|
269
269
|
// Path is gone — delete event. Type unknown by the time we
|
|
@@ -335,9 +335,9 @@ function create(opts) {
|
|
|
335
335
|
var stack = [""];
|
|
336
336
|
while (stack.length > 0) {
|
|
337
337
|
var relDir = stack.pop();
|
|
338
|
-
var absDir = relDir === "" ? root :
|
|
338
|
+
var absDir = relDir === "" ? root : nodePath.join(root, relDir);
|
|
339
339
|
var entries;
|
|
340
|
-
try { entries =
|
|
340
|
+
try { entries = nodeFs.readdirSync(absDir, { withFileTypes: true }); }
|
|
341
341
|
catch (_e) {
|
|
342
342
|
// Root vanished mid-walk OR an inner dir got deleted between
|
|
343
343
|
// the parent listing and the descent. Skip — the next tick's
|
|
@@ -348,8 +348,8 @@ function create(opts) {
|
|
|
348
348
|
var entry = entries[i];
|
|
349
349
|
var relPath = relDir === "" ? entry.name : (relDir + "/" + entry.name);
|
|
350
350
|
// Normalize to forward-slash so glob ignore-matching is
|
|
351
|
-
// consistent with the
|
|
352
|
-
relPath = relPath.split(
|
|
351
|
+
// consistent with the nodeFs.watch path the operator's hooks see.
|
|
352
|
+
relPath = relPath.split(nodePath.sep).join("/");
|
|
353
353
|
if (isIgnored(relPath)) continue;
|
|
354
354
|
if (entry.isSymbolicLink()) continue; // never follow symlinks
|
|
355
355
|
fileCount += 1;
|
|
@@ -358,9 +358,9 @@ function create(opts) {
|
|
|
358
358
|
"watcher.poll: tree exceeds pollMaxFiles=" + pollMaxFiles +
|
|
359
359
|
" — narrow `ignore` patterns OR raise pollMaxFiles, OR switch to mode: \"fs\"");
|
|
360
360
|
}
|
|
361
|
-
var absPath =
|
|
361
|
+
var absPath = nodePath.join(absDir, entry.name);
|
|
362
362
|
var st;
|
|
363
|
-
try { st =
|
|
363
|
+
try { st = nodeFs.statSync(absPath); }
|
|
364
364
|
catch (_e) { continue; } // race — entry vanished
|
|
365
365
|
if (entry.isDirectory()) {
|
|
366
366
|
snapshot.set(relPath, { type: "dir", size: 0, mtimeMs: st.mtimeMs });
|
|
@@ -382,13 +382,13 @@ function create(opts) {
|
|
|
382
382
|
if (pollSnapshot === null) {
|
|
383
383
|
// First tick — establish the baseline without firing events.
|
|
384
384
|
// Operators get add events on file CREATION after start, not on
|
|
385
|
-
// pre-existing files (matches
|
|
385
|
+
// pre-existing files (matches nodeFs.watch semantics).
|
|
386
386
|
pollSnapshot = next;
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
389
|
// Diff: anything in `next` not in `pollSnapshot`, OR with size /
|
|
390
390
|
// mtimeMs different, fires onChange via the same _enqueue path the
|
|
391
|
-
//
|
|
391
|
+
// nodeFs.watch backend uses (so debounce + ignore + lstat dispatch
|
|
392
392
|
// stay uniform). Anything in `pollSnapshot` missing from `next`
|
|
393
393
|
// fires onDelete (via _normalizeAndDispatch's ENOENT branch).
|
|
394
394
|
next.forEach(function (info, relPath) {
|
|
@@ -416,12 +416,12 @@ function create(opts) {
|
|
|
416
416
|
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
417
417
|
} else {
|
|
418
418
|
try {
|
|
419
|
-
watcherHandle =
|
|
419
|
+
watcherHandle = nodeFs.watch(root, { recursive: true, persistent: true }, function (eventType, filename) {
|
|
420
420
|
if (stopped) return;
|
|
421
421
|
if (!filename) return;
|
|
422
422
|
var rel = filename;
|
|
423
|
-
if (
|
|
424
|
-
rel =
|
|
423
|
+
if (nodePath.isAbsolute(rel) && rel.indexOf(root) === 0) {
|
|
424
|
+
rel = nodePath.relative(root, rel);
|
|
425
425
|
}
|
|
426
426
|
if (rel === "" || rel === ".") return;
|
|
427
427
|
_enqueue(rel);
|
|
@@ -434,7 +434,7 @@ function create(opts) {
|
|
|
434
434
|
((e && e.message) || String(e)) + " — pass mode: \"poll\" to fall back to interval polling");
|
|
435
435
|
}
|
|
436
436
|
throw new WatcherError("watcher/start-failed",
|
|
437
|
-
"watcher.create:
|
|
437
|
+
"watcher.create: nodeFs.watch failed: " + ((e && e.message) || String(e)));
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|