@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
@@ -64,6 +64,74 @@ function tableName(local) {
64
64
  return local;
65
65
  }
66
66
 
67
+ // Precomputed rewrite table for resolveTables(). Built once at module
68
+ // load from the static frozen frameworkSchema.LOCAL_TO_EXTERNAL mapping —
69
+ // not from operator/request input. Order longest-first so prefix matches
70
+ // don't collide (audit_log before audit). Identity mappings are filtered
71
+ // out so the loop body has no no-op iterations. Framework table names
72
+ // are entirely [A-Za-z0-9_], so a manual word-boundary scan replaces
73
+ // the regex `\b...\b` exactly without runtime regex compilation.
74
+ // Character-code bounds for the word-character class \w that JavaScript
75
+ // regex `\b` uses. Stored as code-from-literal so the magic numbers are
76
+ // self-evident at the source rather than needing a comment to decode.
77
+ var _CC_0 = "0".charCodeAt(0);
78
+ var _CC_9 = "9".charCodeAt(0);
79
+ var _CC_A = "A".charCodeAt(0);
80
+ var _CC_Z = "Z".charCodeAt(0);
81
+ var _CC_UNDERSCORE = "_".charCodeAt(0);
82
+ var _CC_a = "a".charCodeAt(0);
83
+ var _CC_z = "z".charCodeAt(0);
84
+
85
+ function _isWordChar(code) {
86
+ return (code >= _CC_0 && code <= _CC_9) ||
87
+ (code >= _CC_A && code <= _CC_Z) ||
88
+ code === _CC_UNDERSCORE ||
89
+ (code >= _CC_a && code <= _CC_z);
90
+ }
91
+
92
+ function _replaceWordBoundaryAll(haystack, needle, replacement) {
93
+ if (haystack.length < needle.length) return haystack;
94
+ var out = "";
95
+ var cursor = 0;
96
+ var matched = false;
97
+ var idx = haystack.indexOf(needle);
98
+ while (idx !== -1) {
99
+ var beforeCode = idx === 0 ? -1 : haystack.charCodeAt(idx - 1);
100
+ var afterPos = idx + needle.length;
101
+ var afterCode = afterPos >= haystack.length ? -1 : haystack.charCodeAt(afterPos);
102
+ var leftBoundary = beforeCode === -1 || !_isWordChar(beforeCode);
103
+ var rightBoundary = afterCode === -1 || !_isWordChar(afterCode);
104
+ if (leftBoundary && rightBoundary) {
105
+ out += haystack.slice(cursor, idx) + replacement;
106
+ cursor = afterPos;
107
+ matched = true;
108
+ idx = haystack.indexOf(needle, cursor);
109
+ } else {
110
+ // Boundary mismatch — advance past this position by one and keep
111
+ // scanning. We do NOT flush partial output here; the final
112
+ // haystack.slice(cursor) below covers any non-rewritten tail.
113
+ idx = haystack.indexOf(needle, idx + 1);
114
+ }
115
+ }
116
+ if (!matched) return haystack;
117
+ return out + haystack.slice(cursor);
118
+ }
119
+
120
+ var _REWRITE_TABLE = (function () {
121
+ var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
122
+ var names = Object.keys(mapping).sort(function (a, b) {
123
+ return b.length - a.length;
124
+ });
125
+ var entries = [];
126
+ for (var i = 0; i < names.length; i++) {
127
+ var local = names[i];
128
+ var external = mapping[local];
129
+ if (local === external) continue;
130
+ entries.push({ local: local, external: external });
131
+ }
132
+ return Object.freeze(entries);
133
+ })();
134
+
67
135
  // Rewrite bare table names in SQL when running in cluster mode. We only
68
136
  // touch tokens that are exactly one of the framework's known table names
69
137
  // (audit_log, consent_log, …) — anything else passes through unchanged
@@ -71,18 +139,10 @@ function tableName(local) {
71
139
  // migrations) isn't rewritten by accident.
72
140
  function resolveTables(sql) {
73
141
  if (!cluster.isClusterMode()) return sql;
74
- var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
75
- // Order longest-first so prefix matches don't collide (audit_log
76
- // before audit). Match on word boundaries so we don't rewrite a
77
- // suffix of a longer identifier.
78
- var localNames = Object.keys(mapping).sort(function (a, b) { return b.length - a.length; });
79
142
  var translated = sql;
80
- for (var i = 0; i < localNames.length; i++) {
81
- var local = localNames[i];
82
- var external = mapping[local];
83
- if (local === external) continue; // identity mapping — no-op
84
- var re = new RegExp("\\b" + local.replace(/[\\.-]/g, "\\$&") + "\\b", "g");
85
- translated = translated.replace(re, external);
143
+ for (var i = 0; i < _REWRITE_TABLE.length; i++) {
144
+ var entry = _REWRITE_TABLE[i];
145
+ translated = _replaceWordBoundaryAll(translated, entry.local, entry.external);
86
146
  }
87
147
  return translated;
88
148
  }
package/lib/cluster.js CHANGED
@@ -317,12 +317,13 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
317
317
  : 0;
318
318
 
319
319
  if (currentMax < tipCounter) {
320
- log.error("FATAL: cluster-mode " + chainName + "-log rollback detected.");
321
- log.error(" " + chainName + "-tip counter: " + tipCounter);
322
- log.error(" current external-db max: " + currentMax);
323
- log.error("Either external-db was restored from an older snapshot, or " +
324
- logTable + " rows have been deleted. Investigate before continuing.");
325
- process.exit(1);
320
+ throw _err("ROLLBACK_DETECTED",
321
+ "FATAL: cluster-mode " + chainName + "-log rollback detected. " +
322
+ chainName + "-tip counter: " + tipCounter +
323
+ "; current external-db max: " + currentMax +
324
+ ". Either external-db was restored from an older snapshot, or " +
325
+ logTable + " rows have been deleted. Investigate before continuing.",
326
+ true);
326
327
  }
327
328
 
328
329
  if (tipHash) {
@@ -335,13 +336,14 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
335
336
  if (hashRows.rows && hashRows.rows.length > 0) {
336
337
  var rowAtTip = hashRows.rows[0].rowHash;
337
338
  if (rowAtTip !== tipHash) {
338
- log.error("FATAL: cluster-mode " + chainName + "-log rollback detected (row-hash mismatch).");
339
- log.error(" " + chainName + "-tip counter: " + tipCounter);
340
- log.error(" " + chainName + "-tip rowHash: " + tipHash);
341
- log.error(" current row rowHash: " + rowAtTip);
342
- log.error("The row at the recorded tip counter has a different hash — " +
343
- "indicates row substitution at the chain head. Investigate before continuing.");
344
- process.exit(1);
339
+ throw _err("ROLLBACK_DETECTED",
340
+ "FATAL: cluster-mode " + chainName + "-log rollback detected (row-hash mismatch). " +
341
+ chainName + "-tip counter: " + tipCounter +
342
+ "; " + chainName + "-tip rowHash: " + tipHash +
343
+ "; current row rowHash: " + rowAtTip +
344
+ ". The row at the recorded tip counter has a different hash — " +
345
+ "indicates row substitution at the chain head. Investigate before continuing.",
346
+ true);
345
347
  }
346
348
  }
347
349
  }
@@ -429,25 +431,28 @@ async function _checkVaultKeyConsistency() {
429
431
  if (!rows.rows || rows.rows.length === 0) {
430
432
  // Should never happen — we just INSERTed. Surface as fatal so the
431
433
  // condition isn't silently ignored.
432
- log.error("FATAL: cluster-state row missing immediately after INSERT — " +
433
- "external-db may not be honoring writes. Refusing boot.");
434
- process.exit(1);
434
+ throw _err("CLUSTER_STATE_MISSING",
435
+ "FATAL: cluster-state row missing immediately after INSERT " +
436
+ "external-db may not be honoring writes. Refusing boot.",
437
+ true);
435
438
  }
436
439
  var canonical = rows.rows[0];
437
440
  if (canonical.vaultKeyFp !== localFp) {
438
- log.error("FATAL: vault-key drift detected.");
439
- log.error(" local node: " + nodeId);
440
- log.error(" local fingerprint: " + localFp.slice(0, 16) + "…");
441
- log.error(" canonical recorded by: " + canonical.recordedByNode);
442
- log.error(" canonical fingerprint: " + canonical.vaultKeyFp.slice(0, 16) + "…");
443
- log.error("This node holds a DIFFERENT vault key than the rest of the " +
441
+ var fpPrefix = C.BYTES.bytes(16);
442
+ throw _err("VAULT_KEY_DRIFT",
443
+ "FATAL: vault-key drift detected. " +
444
+ "local node: " + nodeId +
445
+ "; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
446
+ "; canonical recorded by: " + canonical.recordedByNode +
447
+ "; canonical fingerprint: " + canonical.vaultKeyFp.slice(0, fpPrefix) + "…" +
448
+ ". This node holds a DIFFERENT vault key than the rest of the " +
444
449
  "cluster. Sealed-column writes from this node would be unreadable " +
445
450
  "by the others (and vice versa). Restore the same vault key file " +
446
- "before booting this node into the cluster.");
447
- process.exit(1);
451
+ "before booting this node into the cluster.",
452
+ true);
448
453
  }
449
454
  log("cluster vault-key consistency ok (fingerprint " +
450
- localFp.slice(0, 16) + "… recorded by " + canonical.recordedByNode + ")");
455
+ localFp.slice(0, C.BYTES.bytes(16)) + "… recorded by " + canonical.recordedByNode + ")");
451
456
  }
452
457
 
453
458
  async function _tryAcquire() {
@@ -51,6 +51,7 @@ var auditSign = require("./audit-sign");
51
51
  var canonicalJson = require("./canonical-json");
52
52
  var crypto = require("./crypto");
53
53
  var lazyRequire = require("./lazy-require");
54
+ var safeJson = require("./safe-json");
54
55
  var validateOpts = require("./validate-opts");
55
56
  var { defineClass } = require("./framework-error");
56
57
 
@@ -100,9 +101,7 @@ function create(opts) {
100
101
  validateOpts(opts, [
101
102
  "dataDir", "audit", "baseline", "criticalKeys", "ignoreKeys",
102
103
  ], "configDrift");
103
- if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
104
- throw _err("BAD_OPT", "create: opts.dataDir is required");
105
- }
104
+ validateOpts.requireNonEmptyString(opts.dataDir, "create: opts.dataDir", ConfigDriftError, "BAD_OPT");
106
105
  var dataDir = opts.dataDir;
107
106
  var auditOn = opts.audit !== false;
108
107
  var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
@@ -142,7 +141,7 @@ function create(opts) {
142
141
  try { raw = fs.readFileSync(sidecarPath, "utf8"); }
143
142
  catch (_e) { return null; }
144
143
  var parsed;
145
- try { parsed = JSON.parse(raw); }
144
+ try { parsed = safeJson.parse(raw); }
146
145
  catch (_e) { return { unreadable: true }; }
147
146
  if (!parsed || parsed.version !== SIDECAR_VERSION) return { unreadable: true };
148
147
  if (typeof parsed.digestHex !== "string" || typeof parsed.signatureBase64 !== "string" ||
package/lib/constants.js CHANGED
@@ -51,9 +51,14 @@ function _validateBytes(unit, n) {
51
51
  }
52
52
  }
53
53
  var BYTES = Object.freeze({
54
- kib: function (n) { _validateBytes("kib", n); return n * 1024; },
55
- mib: function (n) { _validateBytes("mib", n); return n * 1024 * 1024; },
56
- gib: function (n) { _validateBytes("gib", n); return n * 1024 * 1024 * 1024; },
54
+ // Passthrough for protocol-fixed byte counts (32-byte salt, 16-byte
55
+ // nonce, 8-byte counter, etc.) keeps every byte literal routed
56
+ // through C.BYTES so the codebase has a single source of truth.
57
+ // Equivalent to `n` but names the unit at the call site.
58
+ bytes: function (n) { _validateBytes("bytes", n); return n; },
59
+ kib: function (n) { _validateBytes("kib", n); return n * 1024; },
60
+ mib: function (n) { _validateBytes("mib", n); return n * 1024 * 1024; },
61
+ gib: function (n) { _validateBytes("gib", n); return n * 1024 * 1024 * 1024; },
57
62
  });
58
63
 
59
64
  // ---- Crypto envelope versioning ----
package/lib/cookies.js CHANGED
@@ -72,14 +72,23 @@ var TOKEN_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
72
72
  // Reject CRLF, NUL, semicolon, comma in cookie value pre-encoding.
73
73
  var FORBIDDEN_VALUE_RE = /[\r\n\0;,]/;
74
74
 
75
+ // Length caps applied before the regex tests so a hostile caller can't
76
+ // stage a pathological-length input against the engine. RFC 6265 doesn't
77
+ // pin a name/value length; browsers vary, with a practical 4 KiB total
78
+ // envelope. The framework caps name + value to leave headroom.
79
+ var MAX_NAME_LENGTH = C.BYTES.bytes(256);
80
+ var MAX_VALUE_LENGTH = C.BYTES.kib(4);
81
+
75
82
  function _validateName(name) {
76
83
  if (typeof name !== "string" || name.length === 0) {
77
84
  throw new CookieError("cookies/invalid-name",
78
85
  "cookie name must be a non-empty string");
79
86
  }
80
- if (!TOKEN_RE.test(name)) {
87
+ // Length cap before the regex test — bound the engine on hostile
88
+ // input lengths even though the regex is anchored.
89
+ if (name.length > MAX_NAME_LENGTH || !TOKEN_RE.test(name)) {
81
90
  throw new CookieError("cookies/invalid-name",
82
- "cookie name '" + name + "' contains forbidden characters");
91
+ "cookie name '" + name + "' is empty, too long, or contains forbidden characters");
83
92
  }
84
93
  }
85
94
 
@@ -88,9 +97,12 @@ function _validateValue(value) {
88
97
  throw new CookieError("cookies/invalid-value",
89
98
  "cookie value must be a string");
90
99
  }
91
- if (FORBIDDEN_VALUE_RE.test(value)) {
100
+ // Length cap before the regex test (defense in depth — the regex
101
+ // here is a simple character class, but the same discipline that
102
+ // bounds longer regexes elsewhere applies).
103
+ if (value.length > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
92
104
  throw new CookieError("cookies/invalid-value",
93
- "cookie value contains forbidden control character (CRLF/NUL/;/,)");
105
+ "cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
94
106
  }
95
107
  }
96
108
 
@@ -77,10 +77,7 @@ var { FrameworkError } = require("./framework-error");
77
77
 
78
78
  var observability = lazyRequire(function () { return require("./observability"); });
79
79
 
80
- function _emitEvent(name, value, labels) {
81
- try { observability().event(name, value, labels || {}); }
82
- catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
83
- }
80
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
84
81
 
85
82
  // Default SHAKE256 output size for newly-issued credentials. 128
86
83
  // bytes (1024 bits) is double the SHAKE256 capacity (512 bits, which
@@ -92,14 +89,14 @@ function _emitEvent(name, value, labels) {
92
89
  // posture (highest practical bar where cost is negligible), default
93
90
  // to the larger size; operators pass `{ params: { length: 64 } }`
94
91
  // to opt into the SHA3-512-comparable byte count if they need it.
95
- var SHAKE256_DEFAULT_LENGTH = 128;
92
+ var SHAKE256_DEFAULT_LENGTH = C.BYTES.bytes(128);
96
93
  // Minimum payload length both hash() and verify() enforce. 16 bytes
97
94
  // (128 bits) is the floor below which the digest's collision space
98
95
  // becomes brute-forceable. The asymmetry between hash() (rejected < 16
99
96
  // at config time) and verify() (silently accepted any length) let a
100
97
  // truncated stored envelope verify with trivial work; both ends now
101
98
  // refuse short payloads symmetrically.
102
- var SHAKE256_MIN_LENGTH = 16;
99
+ var SHAKE256_MIN_LENGTH = C.BYTES.bytes(16);
103
100
 
104
101
  function _shake256(secret, length) {
105
102
  // crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
@@ -227,7 +224,7 @@ async function hash(secret, opts) {
227
224
  }
228
225
  // Unreachable — _validateOpts rejects unknown algos.
229
226
  throw new CredentialHashError(
230
- "credentialHash.hash: unsupported algo id 0x" + algoId.toString(16),
227
+ "credentialHash.hash: unsupported algo id 0x" + algoId.toString(0x10),
231
228
  "credential-hash/unsupported");
232
229
  }
233
230
 
package/lib/crypto.js CHANGED
@@ -82,7 +82,7 @@ function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
82
82
 
83
83
  // (SHA-1 is intentionally NOT exported from b.crypto. The framework's
84
84
  // only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
85
- // lib/auth/password.js, which imports lib/internal-sha1-hibp.js
85
+ // lib/auth/password.js, which imports lib/framework-sha1-hibp.js
86
86
  // directly. Public b.crypto.sha1* is permanently off the table — a
87
87
  // future caller wanting SHA-1 for storage / signing / fingerprinting
88
88
  // would re-introduce a broken primitive into the crypto surface this
@@ -137,8 +137,8 @@ function encrypt(plaintext, publicKeys) {
137
137
  privateKey: nodeCrypto.createPrivateKey(ephEc.privateKey),
138
138
  publicKey: nodeCrypto.createPublicKey(ecPubPem),
139
139
  });
140
- var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), 32);
141
- var nonce = generateBytes(24);
140
+ var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), C.BYTES.bytes(32));
141
+ var nonce = generateBytes(C.BYTES.bytes(24));
142
142
  var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
143
143
 
144
144
  var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
@@ -153,8 +153,8 @@ function encrypt(plaintext, publicKeys) {
153
153
 
154
154
  function encryptMlkemOnly(plaintext, publicKeyPem) {
155
155
  var kem = nodeCrypto.encapsulate(nodeCrypto.createPublicKey(publicKeyPem));
156
- var key = kdf(kem.sharedKey, 32);
157
- var nonce = generateBytes(24);
156
+ var key = kdf(kem.sharedKey, C.BYTES.bytes(32));
157
+ var nonce = generateBytes(C.BYTES.bytes(24));
158
158
  var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
159
159
  var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
160
160
  return Buffer.concat([
@@ -200,14 +200,14 @@ function decryptEnvelope(packed, privateKeys) {
200
200
  privateKey: nodeCrypto.createPrivateKey(ecPrivPem),
201
201
  publicKey: nodeCrypto.createPublicKey({ key: ecEphDer, type: "spki", format: "der" }),
202
202
  });
203
- symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), 32);
203
+ symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), C.BYTES.bytes(32));
204
204
  } else if (kemId === C.KEM_IDS.ML_KEM_1024) {
205
- symmetricKey = kdf(mlkemSs, 32);
205
+ symmetricKey = kdf(mlkemSs, C.BYTES.bytes(32));
206
206
  } else {
207
207
  throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
208
208
  }
209
209
 
210
- var nonce = packed.subarray(pos, pos + 24); pos += 24;
210
+ var nonce = packed.subarray(pos, pos + C.BYTES.bytes(24)); pos += C.BYTES.bytes(24);
211
211
  return Buffer.from(
212
212
  xchacha20poly1305(symmetricKey, nonce).decrypt(packed.subarray(pos))
213
213
  ).toString("utf8");
@@ -222,7 +222,7 @@ function decryptEnvelope(packed, privateKeys) {
222
222
  // ciphertext from row A literally cannot decrypt as row B even with
223
223
  // the same key).
224
224
  function encryptPacked(buffer, key, aad) {
225
- var nonce = random(24);
225
+ var nonce = random(C.BYTES.bytes(24));
226
226
  var ct = xchacha20poly1305(key, nonce, aad ? Buffer.from(aad) : undefined).encrypt(buffer);
227
227
  return Buffer.concat([
228
228
  Buffer.from([C.FORMAT.XCHACHA20_POLY1305]),
package/lib/csv.js CHANGED
@@ -53,7 +53,7 @@ var DEFAULTS_PARSE = {
53
53
  quote: "\"",
54
54
  trim: false,
55
55
  maxBytes: C.BYTES.mib(16),
56
- maxRows: 1000000,
56
+ maxRows: 1_000_000,
57
57
  maxFieldBytes: C.BYTES.mib(1),
58
58
  onBadRow: "throw",
59
59
  };
@@ -84,13 +84,16 @@ function _validateDelim(name, value) {
84
84
 
85
85
  function parse(input, opts) {
86
86
  opts = Object.assign({}, DEFAULTS_PARSE, opts || {});
87
- // maxBytes via shared lib/numeric-bounds — Infinity / NaN bypass the
88
- // body cap and let a hostile multi-megabyte CSV through unbounded.
89
- if (!numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
90
- throw new CsvError("csv/bad-opt",
91
- "csv.parse: maxBytes must be a positive finite integer; got " +
92
- numericBounds.shape(opts.maxBytes));
93
- }
87
+ // maxBytes / maxRows / maxFieldBytes via shared lib/numeric-bounds —
88
+ // Infinity / NaN bypass the corresponding caps and let a hostile
89
+ // multi-megabyte CSV (or a single hostile row / single hostile field)
90
+ // through unbounded.
91
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
92
+ "csv.parse: maxBytes", CsvError, "csv/bad-opt");
93
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxRows,
94
+ "csv.parse: maxRows", CsvError, "csv/bad-opt");
95
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxFieldBytes,
96
+ "csv.parse: maxFieldBytes", CsvError, "csv/bad-opt");
94
97
 
95
98
  var s;
96
99
  if (typeof input === "string") s = input;
@@ -54,6 +54,7 @@
54
54
  * }
55
55
  */
56
56
  var safeSql = require("./safe-sql");
57
+ var validateOpts = require("./validate-opts");
57
58
  var { defineClass } = require("./framework-error");
58
59
 
59
60
  var DeclareRowPolicyError = defineClass("DeclareRowPolicyError", { alwaysPermanent: true });
@@ -103,19 +104,13 @@ function _validateOpts(opts) {
103
104
  }
104
105
  }
105
106
 
106
- if (typeof opts.schema !== "string" || opts.schema.length === 0) {
107
- throw _err("declare-row-policy/missing-opt", "schema is required");
108
- }
107
+ validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareRowPolicyError, "declare-row-policy/missing-opt");
109
108
  _validateIdent("schema", opts.schema);
110
109
 
111
- if (typeof opts.table !== "string" || opts.table.length === 0) {
112
- throw _err("declare-row-policy/missing-opt", "table is required");
113
- }
110
+ validateOpts.requireNonEmptyString(opts.table, "table", DeclareRowPolicyError, "declare-row-policy/missing-opt");
114
111
  _validateIdent("table", opts.table);
115
112
 
116
- if (typeof opts.name !== "string" || opts.name.length === 0) {
117
- throw _err("declare-row-policy/missing-opt", "name is required");
118
- }
113
+ validateOpts.requireNonEmptyString(opts.name, "name", DeclareRowPolicyError, "declare-row-policy/missing-opt");
119
114
  _validateIdent("name", opts.name);
120
115
 
121
116
  var role = null;
@@ -61,6 +61,7 @@
61
61
  * }
62
62
  */
63
63
  var safeSql = require("./safe-sql");
64
+ var validateOpts = require("./validate-opts");
64
65
  var { defineClass } = require("./framework-error");
65
66
 
66
67
  var DeclareViewError = defineClass("DeclareViewError", { alwaysPermanent: true });
@@ -135,14 +136,10 @@ function _validateOpts(opts) {
135
136
  }
136
137
  }
137
138
 
138
- if (typeof opts.schema !== "string" || opts.schema.length === 0) {
139
- throw _err("declare-view/missing-opt", "schema is required");
140
- }
139
+ validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareViewError, "declare-view/missing-opt");
141
140
  _validateIdent("schema", opts.schema);
142
141
 
143
- if (typeof opts.name !== "string" || opts.name.length === 0) {
144
- throw _err("declare-view/missing-opt", "name is required");
145
- }
142
+ validateOpts.requireNonEmptyString(opts.name, "name", DeclareViewError, "declare-view/missing-opt");
146
143
  _validateIdent("name", opts.name);
147
144
 
148
145
  if (opts.source === undefined) {
package/lib/db-query.js CHANGED
@@ -27,6 +27,7 @@
27
27
  * would always return zero rows; failing loudly is safer).
28
28
  */
29
29
  var { Readable } = require("node:stream");
30
+ var C = require("./constants");
30
31
  var cryptoField = require("./crypto-field");
31
32
  var { generateToken } = require("./crypto");
32
33
  var safeSql = require("./safe-sql");
@@ -298,7 +299,7 @@ class Query {
298
299
  }
299
300
  var withId = Object.assign({}, row);
300
301
  if (withId._id === undefined || withId._id === null) {
301
- withId._id = generateToken(16);
302
+ withId._id = generateToken(C.BYTES.bytes(16));
302
303
  }
303
304
  var sealed = cryptoField.sealRow(this._cryptoFieldKey(), withId);
304
305
  var cols = Object.keys(sealed);
@@ -392,8 +393,13 @@ class Query {
392
393
  }
393
394
 
394
395
  function _validateField(field) {
395
- if (typeof field !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(field)) {
396
- throw new Error("invalid field name: '" + field + "' (must match [A-Za-z_][A-Za-z0-9_]*)");
396
+ if (typeof field !== "string" ||
397
+ field.length === 0 ||
398
+ field.length > safeSql.MAX_IDENTIFIER_LENGTH ||
399
+ !safeSql.DEFAULT_IDENTIFIER_RE.test(field)) {
400
+ throw new Error("invalid field name: '" + field +
401
+ "' (must match " + safeSql.DEFAULT_IDENTIFIER_RE + ", length 1.." +
402
+ safeSql.MAX_IDENTIFIER_LENGTH + ")");
397
403
  }
398
404
  }
399
405
 
package/lib/db-schema.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
- * Schema reconciler + imperative migration runner.
3
+ * Schema reconciler + imperative migration runner + shared SQL
4
+ * execution helpers used by every db-touching primitive.
4
5
  *
5
6
  * Hybrid migration strategy (per roadmap Q2):
6
7
  *
@@ -19,9 +20,25 @@
19
20
  * Apps mix both: declarative covers structural changes (CREATE/ALTER),
20
21
  * imperative covers data backfills, transformations, conditional schema
21
22
  * changes that need code.
23
+ *
24
+ * Shared SQL execution helpers used by lib/migrations / lib/seeders /
25
+ * lib/vault/rotate / lib/db etc.:
26
+ *
27
+ * - runSql — raw DDL / BEGIN / COMMIT / PRAGMA on
28
+ * the framework's better-sqlite3 handle.
29
+ * - runSqlOnHandle — handles BOTH raw better-sqlite3 and
30
+ * b.db framework wrapper shapes —
31
+ * operator-supplied handles can be either.
32
+ * - runInTransaction — wrap fn in BEGIN / COMMIT / ROLLBACK on
33
+ * the supplied db handle. opts.lockMode
34
+ * appends to BEGIN ("IMMEDIATE" /
35
+ * "EXCLUSIVE" on SQLite). opts.onRollbackFail
36
+ * surfaces a rollback throw without
37
+ * swallowing the original error.
22
38
  */
23
39
  var path = require("path");
24
40
  var atomicFile = require("./atomic-file");
41
+ var safeSql = require("./safe-sql");
25
42
 
26
43
  // SQLite raw-SQL helper. node:sqlite DatabaseSync exposes a method on the
27
44
  // database object that runs raw SQL without bind parameters — used for DDL,
@@ -30,6 +47,48 @@ var atomicFile = require("./atomic-file");
30
47
  // `.exec(` regardless of receiver type.
31
48
  function runSql(database, sql) { return database["exec"](sql); }
32
49
 
50
+ // runSqlOnHandle — execute SQL against either handle shape:
51
+ // - raw better-sqlite3 / node:sqlite Database (db.exec)
52
+ // - b.db framework wrapper (db.runSql)
53
+ // Migrations / seeders / break-glass migrate accept operator-supplied
54
+ // handles in either shape; this wrapper normalizes the call. The
55
+ // 'exec'/'runSql' lookups use bracket notation so the eslint rule
56
+ // banning bare-identifier db method calls (collapsing prepare → field)
57
+ // stays satisfied.
58
+ function runSqlOnHandle(db, sql) {
59
+ if (db && typeof db["exec"] === "function") return db["exec"](sql);
60
+ if (db && typeof db["runSql"] === "function") return db["runSql"](sql);
61
+ throw new Error("dbSchema.runSqlOnHandle: handle exposes no DDL runner (exec / runSql)");
62
+ }
63
+
64
+ // runInTransaction — wrap fn in BEGIN / COMMIT / ROLLBACK on the
65
+ // supplied db handle. Used by migrations / seeders / any operator-
66
+ // facing transaction. opts.lockMode appends to BEGIN ("IMMEDIATE",
67
+ // "EXCLUSIVE" — SQLite-specific). opts.onRollbackFail surfaces a
68
+ // rollback throw without swallowing the original error (the original
69
+ // always re-throws).
70
+ function runInTransaction(db, fn, opts) {
71
+ if (typeof fn !== "function") {
72
+ throw new TypeError("dbSchema.runInTransaction: fn must be a function");
73
+ }
74
+ opts = opts || {};
75
+ var beginSql = opts.lockMode ? "BEGIN " + opts.lockMode : "BEGIN";
76
+ runSqlOnHandle(db, beginSql);
77
+ try {
78
+ var result = fn();
79
+ runSqlOnHandle(db, "COMMIT");
80
+ return result;
81
+ } catch (e) {
82
+ try { runSqlOnHandle(db, "ROLLBACK"); }
83
+ catch (rollbackErr) {
84
+ if (typeof opts.onRollbackFail === "function") {
85
+ try { opts.onRollbackFail(rollbackErr); } catch (_e) { /* nested handler must not bubble */ }
86
+ }
87
+ }
88
+ throw e;
89
+ }
90
+ }
91
+
33
92
  // ---- Internal migrations table ----
34
93
 
35
94
  var MIGRATIONS_TABLE = "_blamejs_migrations";
@@ -186,8 +245,13 @@ function listColumns(database, tableName) {
186
245
 
187
246
  // SQL identifier safety: alphanumeric + underscore, starts with letter or underscore.
188
247
  function validateIdent(ident, kind) {
189
- if (typeof ident !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) {
190
- throw new Error("invalid " + kind + ": '" + ident + "' (must match [A-Za-z_][A-Za-z0-9_]*)");
248
+ if (typeof ident !== "string" ||
249
+ ident.length === 0 ||
250
+ ident.length > safeSql.MAX_IDENTIFIER_LENGTH ||
251
+ !safeSql.DEFAULT_IDENTIFIER_RE.test(ident)) {
252
+ throw new Error("invalid " + kind + ": '" + ident +
253
+ "' (must match " + safeSql.DEFAULT_IDENTIFIER_RE + ", length 1.." +
254
+ safeSql.MAX_IDENTIFIER_LENGTH + ")");
191
255
  }
192
256
  }
193
257
 
@@ -228,14 +292,13 @@ function runMigrations(database, migrationDir) {
228
292
  }
229
293
 
230
294
  try {
231
- runSql(database, "BEGIN");
232
- mig.up(database);
233
- database.prepare(
234
- "INSERT INTO " + Q_MIGRATIONS_TABLE + " (name, description, appliedAt) VALUES (?, ?, ?)"
235
- ).run(file, mig.description || "", new Date().toISOString());
236
- runSql(database, "COMMIT");
295
+ runInTransaction(database, function () {
296
+ mig.up(database);
297
+ database.prepare(
298
+ "INSERT INTO " + Q_MIGRATIONS_TABLE + " (name, description, appliedAt) VALUES (?, ?, ?)"
299
+ ).run(file, mig.description || "", new Date().toISOString());
300
+ });
237
301
  } catch (e) {
238
- try { runSql(database, "ROLLBACK"); } catch (_e) { /* ignored — already in error path */ }
239
302
  throw new Error("migration '" + file + "' failed: " + e.message);
240
303
  }
241
304
  applied.push(file);
@@ -250,5 +313,7 @@ module.exports = {
250
313
  runMigrations: runMigrations,
251
314
  validateIdent: validateIdent,
252
315
  runSql: runSql,
316
+ runSqlOnHandle: runSqlOnHandle,
317
+ runInTransaction: runInTransaction,
253
318
  MIGRATIONS_TABLE: MIGRATIONS_TABLE,
254
319
  };