@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +27 -20
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +17 -4
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +42 -17
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. 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; // 32-byte DB encryption key (null in plain mode)
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
- log.error("FATAL: db.key.enc unseal returned empty — vault may not be initialized or key file corrupted");
536
- process.exit(1);
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 Error("db.init({ dataDir }) is required");
629
+ throw new DbError("db/bad-init", "db.init({ dataDir }) is required");
605
630
  }
606
631
  if (!Array.isArray(opts.schema)) {
607
- throw new Error("db.init({ schema }) must be an array of table definitions");
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 Error("db.init: atRest must be 'encrypted' or 'plain', got: " + opts.atRest);
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
- log.error("FATAL: atRest: 'encrypted' (default) requires tmpfs but none was found.");
621
- log.error(" Provide opts.tmpDir or set BLAMEJS_TMPDIR, or pass atRest: 'plain' (with warning).");
622
- process.exit(1);
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 Error(
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
- log.error("FATAL: audit_log chain integrity broken at row " + auditResult.breakAt + " (" + auditResult.reason + ")");
747
- log.error(" break row _id: " + auditResult.breakRowId);
748
- log.error(" expected: " + auditResult.expected);
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
- process.exit(1);
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
- process.exit(1);
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
- process.exit(1);
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") throw new Error("transaction requires a 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 Error("db.init() must be awaited before using db API");
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
- log.error("FATAL: audit.tip unreadable or schema-invalid at " + tipPath + " — " + e.message);
1022
- log.error("Either delete it (forfeits rollback protection until next checkpoint) " +
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
- process.exit(1);
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 ntpCheck;
1053
- try { ntpCheck = require("./ntp-check"); }
1054
- catch (_e) { return; /* module not present — skip silently */ }
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 { ntpCheck.setThresholds(thr); } catch (_e) {}
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 ntpCheck.bootCheck({
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
- log.error("Refuse to boot. Investigate NTP / RTC / container time sync.");
1102
- log.error("Override: BLAMEJS_NTP_STRICT=0 to continue (NOT recommended for production).");
1103
- process.exit(1);
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(); } catch (_e) {}
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 Error("purgeAuditChain: lastPurgedCounter must be a non-negative number");
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 JSON.parse(JSON.stringify(tableMetadata));
1241
+ if (!name) return structuredClone(tableMetadata);
1199
1242
  var m = tableMetadata[name];
1200
- return m ? JSON.parse(JSON.stringify(m)) : null;
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: require("./db-declare-view").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: require("./db-declare-row-policy").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
- try { require("./audit")._resetForTest(); } catch (_e) {}
1225
- try { require("./consent")._resetForTest(); } catch (_e) {}
1226
- try { require("./subject")._resetForTest(); } catch (_e) {}
1227
- try { require("./session")._resetForTest(); } catch (_e) {}
1228
- try { require("./storage")._resetForTest(); } catch (_e) {}
1229
- try { require("./audit-sign")._resetForTest(); } catch (_e) {}
1230
- try { require("./queue")._resetForTest(); } catch (_e) {}
1231
- try { require("./break-glass")._resetForTest(); } catch (_e) {}
1232
- try { require("./log-stream")._resetForTest(); } catch (_e) {}
1233
- try { require("./redact")._resetForTest(); } catch (_e) {}
1234
- try { require("./external-db")._resetForTest(); } catch (_e) {}
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
- if (typeof opts.since !== "string" || opts.since.length === 0) {
94
- throw new DeprecateError("deprecate/bad-opts",
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
- var DEFAULT_KILL_TIMEOUT_MS = 4000;
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: prefixed line on stdout/stderr
90
- var line = "[blamejs:dev] " + message + (fields ? " " + JSON.stringify(fields) : "");
91
- if (level === "error" || level === "warn" || level === "fatal") console.error(line);
92
- else console.log(line);
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
- if (typeof opts.command !== "string" || opts.command.length === 0) {
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
- var graceMs = typeof opts.graceMs === "number" && opts.graceMs >= 0
108
- ? opts.graceMs : DEFAULT_GRACE_MS;
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
- var killTimeoutMs = typeof opts.killTimeoutMs === "number" && opts.killTimeoutMs >= 0
111
- ? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
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 require("child_process").spawn(cmd, sargs, sopts);
128
+ return childProcess.spawn(cmd, sargs, sopts);
119
129
  };
120
130
  var watchFn = opts._watch || function (dir, wopts, listener) {
121
- return require("fs").watch(dir, wopts, listener);
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) return resolve();
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
- return done();
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;
@@ -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
- if (typeof opts.namespace !== "string" || opts.namespace.length === 0) {
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,