@blamejs/core 0.6.69 → 0.7.0
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 +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +27 -20
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +17 -4
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +42 -17
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/db.js
CHANGED
|
@@ -52,20 +52,45 @@ var consent = require("./consent");
|
|
|
52
52
|
var C = require("./constants");
|
|
53
53
|
var { generateToken, generateBytes, encryptPacked, decryptPacked } = require("./crypto");
|
|
54
54
|
var cryptoField = require("./crypto-field");
|
|
55
|
+
var dbDeclareRowPolicy = require("./db-declare-row-policy");
|
|
56
|
+
var dbDeclareView = require("./db-declare-view");
|
|
55
57
|
var { Query } = require("./db-query");
|
|
56
58
|
var dbSchema = require("./db-schema");
|
|
59
|
+
var { defineClass } = require("./framework-error");
|
|
57
60
|
var { boot } = require("./log");
|
|
58
61
|
var lazyRequire = require("./lazy-require");
|
|
62
|
+
var ntpCheck = lazyRequire(function () { return require("./ntp-check"); });
|
|
59
63
|
var safeAsync = require("./safe-async");
|
|
60
64
|
var safeEnv = require("./parsers/safe-env");
|
|
61
65
|
var safeJson = require("./safe-json");
|
|
62
66
|
var vault = require("./vault");
|
|
63
67
|
|
|
68
|
+
var DbError = defineClass("DbError", { alwaysPermanent: true });
|
|
69
|
+
var _dbErr = DbError.factory;
|
|
70
|
+
|
|
64
71
|
// Lazy: cluster-storage's _localDb pulls db back in, so eager require
|
|
65
72
|
// would deadlock the load order. cluster-storage is only used on the
|
|
66
73
|
// purge-audit-chain external-db path, which always runs after init.
|
|
67
74
|
var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
|
|
68
75
|
|
|
76
|
+
// Lazy refs for the test-reset cascade. Each module requires db.js
|
|
77
|
+
// directly or transitively (audit/consent/subject/session/etc. all
|
|
78
|
+
// own a sealed-column slice that depends on db.from), so eager
|
|
79
|
+
// requires here would cycle on load. The cascade runs only when a
|
|
80
|
+
// test explicitly resets db, so paying the resolve cost lazily is
|
|
81
|
+
// the correct tradeoff.
|
|
82
|
+
var _resetAudit = lazyRequire(function () { return require("./audit"); });
|
|
83
|
+
var _resetConsent = lazyRequire(function () { return require("./consent"); });
|
|
84
|
+
var _resetSubject = lazyRequire(function () { return require("./subject"); });
|
|
85
|
+
var _resetSession = lazyRequire(function () { return require("./session"); });
|
|
86
|
+
var _resetStorage = lazyRequire(function () { return require("./storage"); });
|
|
87
|
+
var _resetAuditSign = lazyRequire(function () { return require("./audit-sign"); });
|
|
88
|
+
var _resetQueue = lazyRequire(function () { return require("./queue"); });
|
|
89
|
+
var _resetBreakGlass = lazyRequire(function () { return require("./break-glass"); });
|
|
90
|
+
var _resetLogStream = lazyRequire(function () { return require("./log-stream"); });
|
|
91
|
+
var _resetRedact = lazyRequire(function () { return require("./redact"); });
|
|
92
|
+
var _resetExternalDb = lazyRequire(function () { return require("./external-db"); });
|
|
93
|
+
|
|
69
94
|
var AUDIT_TIP_SCHEMA = {
|
|
70
95
|
type: "object",
|
|
71
96
|
required: ["atMonotonicCounter"],
|
|
@@ -82,7 +107,7 @@ var runSql = dbSchema.runSql;
|
|
|
82
107
|
var database = null; // the SQLite handle
|
|
83
108
|
var dbPath = null; // plaintext DB file path (tmpfs in encrypted mode, dataDir/db in plain mode)
|
|
84
109
|
var encPath = null; // encrypted-at-rest path (null in plain mode)
|
|
85
|
-
var encKey = null; //
|
|
110
|
+
var encKey = null; // DB encryption key buffer (null in plain mode)
|
|
86
111
|
var encTimer = null; // periodic encrypt interval handle
|
|
87
112
|
var atRest = null; // 'encrypted' or 'plain'
|
|
88
113
|
var dataDir = null;
|
|
@@ -532,13 +557,13 @@ function loadOrCreateDbKey(dataDirPath) {
|
|
|
532
557
|
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
533
558
|
var b64 = vault.unseal(sealed);
|
|
534
559
|
if (!b64) {
|
|
535
|
-
|
|
536
|
-
|
|
560
|
+
throw _dbErr("db/key-unseal-empty",
|
|
561
|
+
"FATAL: db.key.enc unseal returned empty — vault may not be initialized or key file corrupted");
|
|
537
562
|
}
|
|
538
563
|
return Buffer.from(b64, "base64");
|
|
539
564
|
}
|
|
540
565
|
// First run — generate, seal, persist (atomic)
|
|
541
|
-
var raw = generateBytes(32);
|
|
566
|
+
var raw = generateBytes(C.BYTES.bytes(32));
|
|
542
567
|
var sealedKey = vault.seal(raw.toString("base64"));
|
|
543
568
|
atomicFile.writeSync(keyPath, sealedKey, { fileMode: 0o600 });
|
|
544
569
|
log("generated DB encryption key at " + keyPath);
|
|
@@ -601,15 +626,17 @@ function cleanStaleTmpDbs(tmpDir) {
|
|
|
601
626
|
async function init(opts) {
|
|
602
627
|
if (initialized) return;
|
|
603
628
|
if (!opts || !opts.dataDir) {
|
|
604
|
-
throw new
|
|
629
|
+
throw new DbError("db/bad-init", "db.init({ dataDir }) is required");
|
|
605
630
|
}
|
|
606
631
|
if (!Array.isArray(opts.schema)) {
|
|
607
|
-
throw new
|
|
632
|
+
throw new DbError("db/bad-init",
|
|
633
|
+
"db.init({ schema }) must be an array of table definitions");
|
|
608
634
|
}
|
|
609
635
|
|
|
610
636
|
atRest = (opts.atRest || "encrypted").toLowerCase();
|
|
611
637
|
if (atRest !== "encrypted" && atRest !== "plain") {
|
|
612
|
-
throw new
|
|
638
|
+
throw new DbError("db/bad-at-rest",
|
|
639
|
+
"db.init: atRest must be 'encrypted' or 'plain', got: " + opts.atRest);
|
|
613
640
|
}
|
|
614
641
|
dataDir = opts.dataDir;
|
|
615
642
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
@@ -617,14 +644,14 @@ async function init(opts) {
|
|
|
617
644
|
if (atRest === "encrypted") {
|
|
618
645
|
var tmpDir = resolveTmpDir(opts.tmpDir);
|
|
619
646
|
if (!tmpDir) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
647
|
+
throw _dbErr("db/no-tmpfs",
|
|
648
|
+
"FATAL: atRest: 'encrypted' (default) requires tmpfs but none was found. " +
|
|
649
|
+
"Provide opts.tmpDir or set BLAMEJS_TMPDIR, or pass atRest: 'plain' (with warning).");
|
|
623
650
|
}
|
|
624
651
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
625
652
|
|
|
626
653
|
encPath = path.join(dataDir, "db.enc");
|
|
627
|
-
dbPath = path.join(tmpDir, "blamejs-" + generateToken(16) + ".db");
|
|
654
|
+
dbPath = path.join(tmpDir, "blamejs-" + generateToken(C.BYTES.bytes(16)) + ".db");
|
|
628
655
|
encKey = loadOrCreateDbKey(dataDir);
|
|
629
656
|
|
|
630
657
|
cleanStaleTmpDbs(tmpDir);
|
|
@@ -657,11 +684,10 @@ async function init(opts) {
|
|
|
657
684
|
// Refuse app schema entries that collide with framework-reserved names
|
|
658
685
|
for (var ri = 0; ri < opts.schema.length; ri++) {
|
|
659
686
|
if (RESERVED_TABLE_NAMES.has(opts.schema[ri].name)) {
|
|
660
|
-
throw new
|
|
687
|
+
throw new DbError("db/reserved-table-name",
|
|
661
688
|
"table name '" + opts.schema[ri].name + "' is reserved by the framework. " +
|
|
662
689
|
"Pick a different name (the framework provisions audit_log, consent_log, " +
|
|
663
|
-
"and _blamejs_* tables automatically)."
|
|
664
|
-
);
|
|
690
|
+
"and _blamejs_* tables automatically).");
|
|
665
691
|
}
|
|
666
692
|
}
|
|
667
693
|
|
|
@@ -743,25 +769,24 @@ async function init(opts) {
|
|
|
743
769
|
// detects-and-fails.
|
|
744
770
|
var auditResult = await audit.verify();
|
|
745
771
|
if (!auditResult.ok) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
log.error(" actual: " + auditResult.actual);
|
|
750
|
-
log.error("Refusing to boot. Compliance requires that any tamper-detection signal halt service.");
|
|
751
|
-
log.error("Recovery is manual: restore from backup, or rebuild the audit chain from a verified earlier snapshot.");
|
|
752
|
-
// Fire the breach event BEFORE exit so operator listeners get one
|
|
753
|
-
// last chance at sync I/O (file flag, console alert) before the
|
|
754
|
-
// process is gone.
|
|
772
|
+
// Fire the breach event BEFORE throwing so operator listeners get
|
|
773
|
+
// a chance at sync I/O (file flag, console alert) before init
|
|
774
|
+
// unwinds.
|
|
755
775
|
events.emit(events.EVENTS.AUDIT_CHAIN_BREAK, { table: "audit_log", result: auditResult });
|
|
756
|
-
|
|
776
|
+
throw _dbErr("db/audit-chain-break",
|
|
777
|
+
"FATAL: audit_log chain integrity broken at row " + auditResult.breakAt +
|
|
778
|
+
" (" + auditResult.reason + "); break row _id: " + auditResult.breakRowId +
|
|
779
|
+
"; expected: " + auditResult.expected + "; actual: " + auditResult.actual +
|
|
780
|
+
". Refusing to boot. Compliance requires that any tamper-detection signal halt service. " +
|
|
781
|
+
"Recovery is manual: restore from backup, or rebuild the audit chain from a verified earlier snapshot.");
|
|
757
782
|
}
|
|
758
783
|
var consentResult = await consent.verify();
|
|
759
784
|
if (!consentResult.ok) {
|
|
760
|
-
log.error("FATAL: consent_log chain integrity broken at row " + consentResult.breakAt + " (" + consentResult.reason + ")");
|
|
761
|
-
log.error(" break row _id: " + consentResult.breakRowId);
|
|
762
|
-
log.error("Refusing to boot.");
|
|
763
785
|
events.emit(events.EVENTS.AUDIT_CHAIN_BREAK, { table: "consent_log", result: consentResult });
|
|
764
|
-
|
|
786
|
+
throw _dbErr("db/consent-chain-break",
|
|
787
|
+
"FATAL: consent_log chain integrity broken at row " + consentResult.breakAt +
|
|
788
|
+
" (" + consentResult.reason + "); break row _id: " + consentResult.breakRowId +
|
|
789
|
+
". Refusing to boot.");
|
|
765
790
|
}
|
|
766
791
|
log("audit chain ok (" + auditResult.rowsVerified + " rows), consent chain ok (" + consentResult.rowsVerified + " rows)");
|
|
767
792
|
|
|
@@ -799,13 +824,12 @@ async function init(opts) {
|
|
|
799
824
|
// forgery attempt + key-rotation gone wrong). Refuse to boot on failure.
|
|
800
825
|
var ckptResult = await audit.verifyCheckpoints();
|
|
801
826
|
if (!ckptResult.ok) {
|
|
802
|
-
log.error("FATAL: audit checkpoint verification failed at row " +
|
|
803
|
-
ckptResult.breakAt + " (" + ckptResult.reason + ")");
|
|
804
|
-
log.error(" checkpoint _id: " + ckptResult.checkpointId);
|
|
805
|
-
log.error("Refusing to boot. Either the audit-signing key was rotated " +
|
|
806
|
-
"without retaining the prior pubkey, or a forged checkpoint was inserted.");
|
|
807
827
|
events.emit(events.EVENTS.AUDIT_CHECKPOINT_BREAK, { result: ckptResult });
|
|
808
|
-
|
|
828
|
+
throw _dbErr("db/audit-checkpoint-break",
|
|
829
|
+
"FATAL: audit checkpoint verification failed at row " +
|
|
830
|
+
ckptResult.breakAt + " (" + ckptResult.reason + "); checkpoint _id: " +
|
|
831
|
+
ckptResult.checkpointId + ". Refusing to boot. Either the audit-signing key " +
|
|
832
|
+
"was rotated without retaining the prior pubkey, or a forged checkpoint was inserted.");
|
|
809
833
|
}
|
|
810
834
|
log("audit checkpoints ok (" + ckptResult.checkpointsVerified + " signed)");
|
|
811
835
|
|
|
@@ -915,7 +939,9 @@ function execRaw(sql) {
|
|
|
915
939
|
|
|
916
940
|
function transaction(fn) {
|
|
917
941
|
_requireInit();
|
|
918
|
-
if (typeof fn !== "function")
|
|
942
|
+
if (typeof fn !== "function") {
|
|
943
|
+
throw new DbError("db/bad-transaction-fn", "transaction requires a function");
|
|
944
|
+
}
|
|
919
945
|
runSql(database, "BEGIN");
|
|
920
946
|
try {
|
|
921
947
|
var result = fn(module.exports);
|
|
@@ -965,7 +991,8 @@ function close() {
|
|
|
965
991
|
|
|
966
992
|
function _requireInit() {
|
|
967
993
|
if (!initialized) {
|
|
968
|
-
throw new
|
|
994
|
+
throw new DbError("db/not-initialized",
|
|
995
|
+
"db.init() must be awaited before using db API");
|
|
969
996
|
}
|
|
970
997
|
}
|
|
971
998
|
|
|
@@ -1018,25 +1045,25 @@ function _checkRollback(dataDirPath) {
|
|
|
1018
1045
|
try {
|
|
1019
1046
|
tip = safeJson.parse(atomicFile.readSync(tipPath), { schema: AUDIT_TIP_SCHEMA });
|
|
1020
1047
|
} catch (e) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1048
|
+
throw _dbErr("db/audit-tip-unreadable",
|
|
1049
|
+
"FATAL: audit.tip unreadable or schema-invalid at " + tipPath + " — " + e.message +
|
|
1050
|
+
". Either delete it (forfeits rollback protection until next checkpoint) " +
|
|
1023
1051
|
"or restore from operator backup.");
|
|
1024
|
-
process.exit(1);
|
|
1025
1052
|
}
|
|
1026
1053
|
var current = database.prepare("SELECT MAX(monotonicCounter) AS m FROM audit_log").get();
|
|
1027
1054
|
var currentMax = current && current.m ? current.m : 0;
|
|
1028
1055
|
if (currentMax < tip.atMonotonicCounter) {
|
|
1029
|
-
log.error("FATAL: audit-log rollback detected.");
|
|
1030
|
-
log.error(" audit.tip recorded counter: " + tip.atMonotonicCounter);
|
|
1031
|
-
log.error(" current DB max counter: " + currentMax);
|
|
1032
|
-
log.error("Either the DB was restored from an older snapshot, or audit_log " +
|
|
1033
|
-
"rows have been deleted. Investigate before continuing.");
|
|
1034
1056
|
events.emit(events.EVENTS.AUDIT_ROLLBACK_DETECTED, {
|
|
1035
1057
|
tipCounter: tip.atMonotonicCounter,
|
|
1036
1058
|
currentMax: currentMax,
|
|
1037
1059
|
tipPath: tipPath,
|
|
1038
1060
|
});
|
|
1039
|
-
|
|
1061
|
+
throw _dbErr("db/audit-rollback-detected",
|
|
1062
|
+
"FATAL: audit-log rollback detected. " +
|
|
1063
|
+
"audit.tip recorded counter: " + tip.atMonotonicCounter +
|
|
1064
|
+
"; current DB max counter: " + currentMax +
|
|
1065
|
+
". Either the DB was restored from an older snapshot, or audit_log rows " +
|
|
1066
|
+
"have been deleted. Investigate before continuing.");
|
|
1040
1067
|
}
|
|
1041
1068
|
log("rollback check ok (tip counter " + tip.atMonotonicCounter +
|
|
1042
1069
|
", current " + currentMax + ")");
|
|
@@ -1049,9 +1076,12 @@ function _checkRollback(dataDirPath) {
|
|
|
1049
1076
|
// fatal → log fatal, exit(1) — audit-log the attempt before exit
|
|
1050
1077
|
async function _runNtpBootCheck(opts) {
|
|
1051
1078
|
if (safeEnv.readVar("BLAMEJS_SKIP_NTP_CHECK", { default: "" }) === "1") return;
|
|
1052
|
-
var
|
|
1053
|
-
try {
|
|
1054
|
-
catch (
|
|
1079
|
+
var ntp;
|
|
1080
|
+
try { ntp = ntpCheck(); }
|
|
1081
|
+
catch (e) {
|
|
1082
|
+
log.debug("ntp-check module unavailable", { error: e.message });
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1055
1085
|
|
|
1056
1086
|
var envServersRaw = safeEnv.readVar("BLAMEJS_NTP_SERVERS", { default: "" });
|
|
1057
1087
|
var envTimeout = safeEnv.readVar("BLAMEJS_NTP_TIMEOUT_MS", { default: "" });
|
|
@@ -1065,12 +1095,13 @@ async function _runNtpBootCheck(opts) {
|
|
|
1065
1095
|
var thr = {};
|
|
1066
1096
|
if (envWarn) thr.warnMs = parseInt(envWarn, 10);
|
|
1067
1097
|
if (envFatal) thr.fatalMs = parseInt(envFatal, 10);
|
|
1068
|
-
try {
|
|
1098
|
+
try { ntp.setThresholds(thr); }
|
|
1099
|
+
catch (e) { log.debug("ntp setThresholds failed", { error: e.message }); }
|
|
1069
1100
|
}
|
|
1070
1101
|
|
|
1071
1102
|
var result;
|
|
1072
1103
|
try {
|
|
1073
|
-
result = await
|
|
1104
|
+
result = await ntp.bootCheck({
|
|
1074
1105
|
servers: resolvedServers,
|
|
1075
1106
|
timeoutMs: resolvedTimeout,
|
|
1076
1107
|
});
|
|
@@ -1098,17 +1129,27 @@ async function _runNtpBootCheck(opts) {
|
|
|
1098
1129
|
message: result.message,
|
|
1099
1130
|
});
|
|
1100
1131
|
if (safeEnv.readVar("BLAMEJS_NTP_STRICT", { default: "1" }) !== "0") {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1132
|
+
throw _dbErr("db/ntp-drift-fatal",
|
|
1133
|
+
"FATAL: ntp clock drift exceeds threshold: " + result.message +
|
|
1134
|
+
". Refuse to boot. Investigate NTP / RTC / container time sync. " +
|
|
1135
|
+
"Override: BLAMEJS_NTP_STRICT=0 to continue (NOT recommended for production).");
|
|
1104
1136
|
}
|
|
1105
1137
|
}
|
|
1106
1138
|
}
|
|
1107
1139
|
|
|
1140
|
+
// _cascadeStep — invoke ._resetForTest on a single lazy-required module
|
|
1141
|
+
// ref, logging at debug on any failure. Used by _resetForTest's cascade
|
|
1142
|
+
// over the framework's stateful subsystems.
|
|
1143
|
+
function _cascadeStep(name, ref) {
|
|
1144
|
+
try { ref()._resetForTest(); }
|
|
1145
|
+
catch (e) { log.debug("cascade-reset failed", { module: name, error: e.message }); }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1108
1148
|
// Test helpers — not part of public contract
|
|
1109
1149
|
function _resetForTest() {
|
|
1110
1150
|
if (encTimer) { encTimer.stop(); encTimer = null; }
|
|
1111
|
-
try { if (database) database.close(); }
|
|
1151
|
+
try { if (database) database.close(); }
|
|
1152
|
+
catch (e) { log.debug("test-reset close failed", { error: e.message }); }
|
|
1112
1153
|
database = null;
|
|
1113
1154
|
dbPath = null;
|
|
1114
1155
|
encPath = null;
|
|
@@ -1119,6 +1160,7 @@ function _resetForTest() {
|
|
|
1119
1160
|
cryptoField.clearForTest();
|
|
1120
1161
|
}
|
|
1121
1162
|
|
|
1163
|
+
|
|
1122
1164
|
module.exports = {
|
|
1123
1165
|
init: init,
|
|
1124
1166
|
from: from,
|
|
@@ -1154,7 +1196,8 @@ module.exports = {
|
|
|
1154
1196
|
purgeAuditChain: async function (args) {
|
|
1155
1197
|
var lastPurgedCounter = Number(args && args.lastPurgedCounter);
|
|
1156
1198
|
if (!Number.isFinite(lastPurgedCounter) || lastPurgedCounter < 0) {
|
|
1157
|
-
throw new
|
|
1199
|
+
throw new DbError("db/bad-purge-counter",
|
|
1200
|
+
"purgeAuditChain: lastPurgedCounter must be a non-negative number");
|
|
1158
1201
|
}
|
|
1159
1202
|
if (cluster.isClusterMode()) {
|
|
1160
1203
|
// External-db has no append-only triggers; ordinary DELETE works.
|
|
@@ -1195,20 +1238,20 @@ module.exports = {
|
|
|
1195
1238
|
// subject mapping. Useful for tooling, RoPA generation, and admin dashboards.
|
|
1196
1239
|
// Returns a deep-copied snapshot; mutations don't affect framework state.
|
|
1197
1240
|
getTableMetadata: function (name) {
|
|
1198
|
-
if (!name) return
|
|
1241
|
+
if (!name) return structuredClone(tableMetadata);
|
|
1199
1242
|
var m = tableMetadata[name];
|
|
1200
|
-
return m ?
|
|
1243
|
+
return m ? structuredClone(m) : null;
|
|
1201
1244
|
},
|
|
1202
1245
|
// declareView — declarative CREATE VIEW + GRANT migration spec for an
|
|
1203
1246
|
// externalDb backend. Returns a migration-shape object for use with
|
|
1204
1247
|
// b.externalDb.migrate. Postgres-only; fail-fast at apply time on other
|
|
1205
1248
|
// dialects. See lib/db-declare-view.js.
|
|
1206
|
-
declareView:
|
|
1249
|
+
declareView: dbDeclareView.declareView,
|
|
1207
1250
|
// declareRowPolicy — declarative Postgres ROW LEVEL SECURITY migration
|
|
1208
1251
|
// spec. Pairs with externalDb.transaction({ sessionGucs }) for the
|
|
1209
1252
|
// per-request `SET LOCAL` plumbing. Postgres-only; fail-fast on other
|
|
1210
1253
|
// dialects. See lib/db-declare-row-policy.js.
|
|
1211
|
-
declareRowPolicy:
|
|
1254
|
+
declareRowPolicy: dbDeclareRowPolicy.declareRowPolicy,
|
|
1212
1255
|
// Internal accessors used by audit / subject / consent modules.
|
|
1213
1256
|
// Not part of the public contract — apps should not depend on them.
|
|
1214
1257
|
_getSubjectTables: function () { return subjectTables.slice(); },
|
|
@@ -1221,17 +1264,20 @@ module.exports = {
|
|
|
1221
1264
|
dataResidency = null;
|
|
1222
1265
|
tableMetadata = {};
|
|
1223
1266
|
// Cascade reset to stateful modules so a fresh init() works.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1267
|
+
// Each ref is a lazyRequire (top-of-file) so module-load cycles
|
|
1268
|
+
// don't trip; failures (missing optional dep, partial smoke
|
|
1269
|
+
// suites that skip a module entirely) get logged at debug.
|
|
1270
|
+
_cascadeStep("audit", _resetAudit);
|
|
1271
|
+
_cascadeStep("consent", _resetConsent);
|
|
1272
|
+
_cascadeStep("subject", _resetSubject);
|
|
1273
|
+
_cascadeStep("session", _resetSession);
|
|
1274
|
+
_cascadeStep("storage", _resetStorage);
|
|
1275
|
+
_cascadeStep("audit-sign", _resetAuditSign);
|
|
1276
|
+
_cascadeStep("queue", _resetQueue);
|
|
1277
|
+
_cascadeStep("break-glass", _resetBreakGlass);
|
|
1278
|
+
_cascadeStep("log-stream", _resetLogStream);
|
|
1279
|
+
_cascadeStep("redact", _resetRedact);
|
|
1280
|
+
_cascadeStep("external-db", _resetExternalDb);
|
|
1235
1281
|
},
|
|
1236
1282
|
// Helper for audit.checkpoint to write the rollback-detection sidecar
|
|
1237
1283
|
_writeAuditTip: function (tip) {
|
package/lib/deprecate.js
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
var safeEnv = require("./parsers/safe-env");
|
|
54
|
+
var validateOpts = require("./validate-opts");
|
|
54
55
|
var { FrameworkError } = require("./framework-error");
|
|
55
56
|
|
|
56
57
|
class DeprecateError extends FrameworkError {
|
|
@@ -90,14 +91,8 @@ function _validateOpts(opts, fnName) {
|
|
|
90
91
|
throw new DeprecateError("deprecate/bad-opts",
|
|
91
92
|
fnName + ": opts is required (with at least 'since' and 'removeIn')");
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fnName + ": opts.since (version string) is required");
|
|
96
|
-
}
|
|
97
|
-
if (typeof opts.removeIn !== "string" || opts.removeIn.length === 0) {
|
|
98
|
-
throw new DeprecateError("deprecate/bad-opts",
|
|
99
|
-
fnName + ": opts.removeIn (version string) is required");
|
|
100
|
-
}
|
|
94
|
+
validateOpts.requireNonEmptyString(opts.since, fnName + ": opts.since (version string)", DeprecateError, "deprecate/bad-opts");
|
|
95
|
+
validateOpts.requireNonEmptyString(opts.removeIn, fnName + ": opts.removeIn (version string)", DeprecateError, "deprecate/bad-opts");
|
|
101
96
|
}
|
|
102
97
|
|
|
103
98
|
function warn(name, opts) {
|
package/lib/dev.js
CHANGED
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
var path = require("path");
|
|
51
|
+
var childProcess = require("child_process");
|
|
52
|
+
var fs = require("fs");
|
|
53
|
+
var logModule = require("./log");
|
|
54
|
+
var nb = require("./numeric-bounds");
|
|
55
|
+
var validateOpts = require("./validate-opts");
|
|
51
56
|
var { FrameworkError } = require("./framework-error");
|
|
52
57
|
|
|
53
58
|
class DevError extends FrameworkError {
|
|
@@ -59,8 +64,13 @@ class DevError extends FrameworkError {
|
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
var bootLog = logModule.boot("dev");
|
|
68
|
+
|
|
62
69
|
var DEFAULT_GRACE_MS = 250;
|
|
63
|
-
|
|
70
|
+
// Default SIGKILL escalation budget: 4s — long enough for graceful
|
|
71
|
+
// shutdown handlers to flush, short enough that an ignored SIGTERM
|
|
72
|
+
// doesn't keep the dev loop hanging.
|
|
73
|
+
var DEFAULT_KILL_TIMEOUT_MS = 0xFA0;
|
|
64
74
|
var DEFAULT_KILL_SIGNAL = "SIGTERM";
|
|
65
75
|
var DEFAULT_IGNORE = [
|
|
66
76
|
/node_modules/,
|
|
@@ -86,39 +96,39 @@ function _logVia(log, level, message, fields) {
|
|
|
86
96
|
catch (_e) { /* logger best-effort */ }
|
|
87
97
|
return;
|
|
88
98
|
}
|
|
89
|
-
// Fallback:
|
|
90
|
-
var line =
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
// Fallback: framework boot channel (TTY-aware, structured-JSON when piped).
|
|
100
|
+
var line = message + (fields ? " " + JSON.stringify(fields) : "");
|
|
101
|
+
var emit = (level === "error" || level === "fatal") ? bootLog.error
|
|
102
|
+
: (level === "warn" ? bootLog.warn : bootLog.info);
|
|
103
|
+
emit(line);
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
function create(opts) {
|
|
96
107
|
opts = opts || {};
|
|
97
|
-
|
|
98
|
-
throw new DevError("dev/no-command",
|
|
99
|
-
"dev.create requires opts.command (the program to spawn)");
|
|
100
|
-
}
|
|
108
|
+
validateOpts.requireNonEmptyString(opts.command, "dev.create: opts.command (the program to spawn)", DevError, "dev/no-command");
|
|
101
109
|
var args = Array.isArray(opts.args) ? opts.args.slice() : [];
|
|
102
110
|
var watch = Array.isArray(opts.watch) && opts.watch.length > 0
|
|
103
111
|
? opts.watch.slice() : ["."];
|
|
104
112
|
var ignore = Array.isArray(opts.ignore)
|
|
105
113
|
? DEFAULT_IGNORE.concat(opts.ignore)
|
|
106
114
|
: DEFAULT_IGNORE.slice();
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.graceMs,
|
|
116
|
+
"dev.create: opts.graceMs", DevError, "dev/bad-grace-ms");
|
|
117
|
+
var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
|
|
109
118
|
var killSignal = typeof opts.killSignal === "string" ? opts.killSignal : DEFAULT_KILL_SIGNAL;
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.killTimeoutMs,
|
|
120
|
+
"dev.create: opts.killTimeoutMs", DevError, "dev/bad-kill-timeout-ms");
|
|
121
|
+
var killTimeoutMs = opts.killTimeoutMs !== undefined ? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
|
|
112
122
|
var log = opts.log || null;
|
|
113
123
|
var env = opts.env || process.env;
|
|
114
124
|
var cwd = opts.cwd || process.cwd();
|
|
115
125
|
|
|
116
126
|
// Test seams
|
|
117
127
|
var spawnFn = opts._spawn || function (cmd, sargs, sopts) {
|
|
118
|
-
return
|
|
128
|
+
return childProcess.spawn(cmd, sargs, sopts);
|
|
119
129
|
};
|
|
120
130
|
var watchFn = opts._watch || function (dir, wopts, listener) {
|
|
121
|
-
return
|
|
131
|
+
return fs.watch(dir, wopts, listener);
|
|
122
132
|
};
|
|
123
133
|
var setTimeoutFn = opts._setTimeout || setTimeout;
|
|
124
134
|
var clearTimeoutFn = opts._clearTimeout || clearTimeout;
|
|
@@ -160,13 +170,13 @@ function create(opts) {
|
|
|
160
170
|
|
|
161
171
|
function _killChild() {
|
|
162
172
|
return new Promise(function (resolve) {
|
|
163
|
-
if (!child)
|
|
173
|
+
if (!child) { resolve(); return; }
|
|
164
174
|
var c = child;
|
|
165
175
|
var settled = false;
|
|
166
176
|
function done() {
|
|
167
177
|
if (settled) return;
|
|
168
178
|
settled = true;
|
|
169
|
-
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) {} killTimer = null; }
|
|
179
|
+
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) { /* timer already cleared */ } killTimer = null; }
|
|
170
180
|
resolve();
|
|
171
181
|
}
|
|
172
182
|
c.once("exit", done);
|
|
@@ -174,7 +184,8 @@ function create(opts) {
|
|
|
174
184
|
catch (e) {
|
|
175
185
|
_logVia(log, "warn", "kill threw, child may already be gone",
|
|
176
186
|
{ error: (e && e.message) || String(e) });
|
|
177
|
-
|
|
187
|
+
done();
|
|
188
|
+
return;
|
|
178
189
|
}
|
|
179
190
|
// Hard-kill if the child ignores SIGTERM
|
|
180
191
|
killTimer = setTimeoutFn(function () {
|
|
@@ -285,7 +296,7 @@ function create(opts) {
|
|
|
285
296
|
async function stop() {
|
|
286
297
|
if (!started) return;
|
|
287
298
|
stopping = true;
|
|
288
|
-
if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) {} debounceTimer = null; }
|
|
299
|
+
if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ } debounceTimer = null; }
|
|
289
300
|
_disarmWatchers();
|
|
290
301
|
await _killChild();
|
|
291
302
|
child = null;
|
package/lib/dual-control.js
CHANGED
|
@@ -113,9 +113,7 @@ function create(opts) {
|
|
|
113
113
|
"namespace", "cache", "audit", "ttlMs", "minApprovers", "forbidSelfApprove",
|
|
114
114
|
"consumeLockMs", "minReasonLength", "approverRoles", "notify",
|
|
115
115
|
], "dualControl");
|
|
116
|
-
|
|
117
|
-
throw _err("BAD_OPT", "create: opts.namespace is required");
|
|
118
|
-
}
|
|
116
|
+
validateOpts.requireNonEmptyString(opts.namespace, "create: opts.namespace", DualControlError, "BAD_OPT");
|
|
119
117
|
if (!opts.cache || typeof opts.cache.get !== "function" || typeof opts.cache.set !== "function") {
|
|
120
118
|
throw _err("BAD_OPT", "create: opts.cache is required (a b.cache instance)");
|
|
121
119
|
}
|
|
@@ -210,7 +208,7 @@ function create(opts) {
|
|
|
210
208
|
if (reasonProblem) {
|
|
211
209
|
return Object.assign({ grantId: null }, reasonProblem);
|
|
212
210
|
}
|
|
213
|
-
var grantId = "dc-" + crypto.generateToken(8);
|
|
211
|
+
var grantId = "dc-" + crypto.generateToken(C.BYTES.bytes(8));
|
|
214
212
|
var nowMs = Date.now();
|
|
215
213
|
var record = {
|
|
216
214
|
grantId: grantId,
|