@blamejs/core 0.6.70 → 0.7.1

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 +23 -26
  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 +4 -3
  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 +26 -15
  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 +98 -52
  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
@@ -69,6 +69,7 @@
69
69
  */
70
70
 
71
71
  var C = require("../constants");
72
+ var safeBuffer = require("../safe-buffer");
72
73
  var safeJson = require("../safe-json");
73
74
  var { FrameworkError } = require("../framework-error");
74
75
 
@@ -84,7 +85,10 @@ class BackupManifestError extends FrameworkError {
84
85
  var FORMAT_VERSION = 1;
85
86
  var FRAMEWORK_NAME = "blamejs";
86
87
  var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
87
- var HEX_RE = /^[0-9a-fA-F]+$/;
88
+ // SHA3-512 produces 64 raw bytes — each byte serializes as 2 hex chars
89
+ // in the manifest, so the hex string is 128 chars long.
90
+ var SHA3_512_HEX_LENGTH = 128;
91
+ var HEX_RE = safeBuffer.HEX_RE;
88
92
  var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
89
93
 
90
94
  function _isHex(s, evenLength) {
@@ -124,7 +128,7 @@ function _validateFileEntry(f, idx, errors) {
124
128
  if (typeof f.encryptedSize !== "number" || !Number.isInteger(f.encryptedSize) || f.encryptedSize < 0) {
125
129
  errors.push("files[" + idx + "].encryptedSize: required non-negative integer");
126
130
  }
127
- if (!_isHex(f.checksum, true) || f.checksum.length !== 128) {
131
+ if (!_isHex(f.checksum, true) || f.checksum.length !== SHA3_512_HEX_LENGTH) {
128
132
  errors.push("files[" + idx + "].checksum: required 128-char hex string (sha3-512)");
129
133
  }
130
134
  if (!_isHex(f.salt, true)) {
@@ -66,6 +66,14 @@ var BreakGlassError = defineClass("BreakGlassError", { alwaysPermanent: false })
66
66
 
67
67
  // ---- Defaults (matched to operator-locked decisions) ----
68
68
 
69
+ // Symmetric byte counts: 32-byte cell-key (kdf output, XChaCha20 key
70
+ // length); 32-byte DEK (AEAD key); 16-byte grant-id randomness (128-bit
71
+ // uniqueness for cross-table grant lookups). All routed through C.BYTES
72
+ // so the framework's byte math has a single source of truth.
73
+ var CELL_KEY_BYTES = C.BYTES.bytes(32);
74
+ var DEK_BYTES = C.BYTES.bytes(32);
75
+ var GRANT_ID_BYTES = C.BYTES.bytes(16);
76
+
69
77
  var DEFAULT_GRANT_TTL_MS = C.TIME.minutes(15);
70
78
  var DEFAULT_MAX_ROWS = 1; // operator-locked: row-by-row auth
71
79
  var DEFAULT_REASON_MIN_LEN = 12;
@@ -137,7 +145,7 @@ function _kCell(dek, table, rowId, column) {
137
145
  return kdf(Buffer.concat([
138
146
  Buffer.isBuffer(dek) ? dek : Buffer.from(dek, "base64"),
139
147
  Buffer.from("breakglass.cell|" + table + "|" + String(rowId) + "|" + column, "utf8"),
140
- ]), 32);
148
+ ]), CELL_KEY_BYTES);
141
149
  }
142
150
 
143
151
  async function _ensureDek(table) {
@@ -158,7 +166,7 @@ async function _ensureDek(table) {
158
166
  if (sealed) {
159
167
  dek = Buffer.from(vault().unseal(sealed), "base64");
160
168
  } else {
161
- dek = generateBytes(32);
169
+ dek = generateBytes(DEK_BYTES);
162
170
  var sealedDek = vault().seal(dek.toString("base64"));
163
171
  await clusterStorage.execute(
164
172
  "UPDATE _blamejs_break_glass_policies SET dekSealed = ? WHERE tableName = ?",
@@ -415,10 +423,7 @@ function _validatePolicySet(table, opts) {
415
423
  // CLI to convert existing rows). At unseal time, the row's
416
424
  // glass-locked columns are decrypted via decryptCell with encryption
417
425
  // context binding (table, rowId, column).
418
- if (opts.cryptographic !== undefined && typeof opts.cryptographic !== "boolean") {
419
- throw new BreakGlassError("breakglass/bad-policy",
420
- "policy.set: cryptographic must be a boolean");
421
- }
426
+ validateOpts.optionalBoolean(opts.cryptographic, "policy.set: cryptographic", BreakGlassError, "breakglass/bad-policy");
422
427
  var grantTtl = opts.grantTtl != null ? opts.grantTtl : DEFAULT_GRANT_TTL_MS;
423
428
  if (typeof grantTtl !== "number" || !isFinite(grantTtl) || grantTtl <= 0) {
424
429
  throw new BreakGlassError("breakglass/bad-policy",
@@ -733,7 +738,7 @@ async function grant(opts) {
733
738
 
734
739
  // Build + persist the grant row
735
740
  var nowMs = Date.now();
736
- var grantId = "bg-" + generateToken(16);
741
+ var grantId = "bg-" + generateToken(GRANT_ID_BYTES);
737
742
  var sessionId = (opts.req && opts.req.session && opts.req.session.id) || null;
738
743
  // Honor the framework-wide trustProxy setting from init() — same
739
744
  // boundary as middleware. Without trustProxy, X-Forwarded-For is
package/lib/bundler.js CHANGED
@@ -81,11 +81,22 @@ var path = require("path");
81
81
  var fs = require("fs");
82
82
  var crypto = require("./crypto");
83
83
  var atomicFile = require("./atomic-file");
84
- var { defineClass } = require("./framework-error");
84
+ var logModule = require("./log");
85
+ var nb = require("./numeric-bounds");
85
86
  var safeJson = require("./safe-json");
86
87
  var validateOpts = require("./validate-opts");
88
+ var { defineClass } = require("./framework-error");
87
89
 
88
90
  var BundlerError = defineClass("BundlerError", { alwaysPermanent: true });
91
+ var bootLog = logModule.boot("bundler");
92
+
93
+ // Default content-hash length: 16 hex chars (8 bytes of SHA3-512). Long
94
+ // enough that collision probability is negligible for asset cache busting,
95
+ // short enough to keep generated filenames readable.
96
+ var DEFAULT_HASH_LEN = 0x10;
97
+ var MIN_HASH_LEN = 0x4;
98
+ var MAX_HASH_LEN = 0x40;
99
+ var DEFAULT_GRACE_MS = 100;
89
100
 
90
101
  function _hashContent(buf, hexLen) {
91
102
  // SHA3-512 → take the first hexLen hex chars. Same family as the
@@ -221,10 +232,7 @@ function create(opts) {
221
232
  "_watch", "_setTimeout", "_clearTimeout",
222
233
  ], "b.bundler");
223
234
  _validateEntries(opts.entries);
224
- if (typeof opts.outdir !== "string" || opts.outdir.length === 0) {
225
- throw new BundlerError("bundler/no-outdir",
226
- "bundler.create requires opts.outdir");
227
- }
235
+ validateOpts.requireNonEmptyString(opts.outdir, "bundler.create: opts.outdir", BundlerError, "bundler/no-outdir");
228
236
  var engineImpl = _validateEngine(opts.engine);
229
237
 
230
238
  var entries = Object.assign({}, opts.entries);
@@ -236,8 +244,17 @@ function create(opts) {
236
244
  ? opts.manifest
237
245
  : "manifest.json");
238
246
  var hashOn = opts.hash !== false;
239
- var hashLen = typeof opts.hashLen === "number" && opts.hashLen >= 4 && opts.hashLen <= 64
240
- ? Math.floor(opts.hashLen) : 16;
247
+ var hashLen = DEFAULT_HASH_LEN;
248
+ if (opts.hashLen !== undefined) {
249
+ if (!nb.isPositiveFiniteInt(opts.hashLen) ||
250
+ opts.hashLen < MIN_HASH_LEN || opts.hashLen > MAX_HASH_LEN) {
251
+ throw new BundlerError("bundler/bad-hash-len",
252
+ "bundler.create: opts.hashLen must be a positive finite integer " +
253
+ "between " + MIN_HASH_LEN + " and " + MAX_HASH_LEN +
254
+ "; got " + nb.shape(opts.hashLen));
255
+ }
256
+ hashLen = opts.hashLen;
257
+ }
241
258
  var log = opts.log || null;
242
259
 
243
260
  // Test seam: tests pass a fake watcher so we don't actually fs.watch
@@ -246,7 +263,9 @@ function create(opts) {
246
263
  };
247
264
  var setTimeoutFn = opts._setTimeout || setTimeout;
248
265
  var clearTimeoutFn = opts._clearTimeout || clearTimeout;
249
- var graceMs = typeof opts.graceMs === "number" && opts.graceMs >= 0 ? opts.graceMs : 100;
266
+ nb.requireNonNegativeFiniteIntIfPresent(opts.graceMs,
267
+ "bundler.create: opts.graceMs", BundlerError, "bundler/bad-grace-ms");
268
+ var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
250
269
 
251
270
  var watchers = [];
252
271
  var debounceTimer = null;
@@ -256,15 +275,7 @@ function create(opts) {
256
275
  return path.isAbsolute(p) ? p : path.resolve(cwd, p);
257
276
  }
258
277
 
259
- function _logVia(level, message, fields) {
260
- if (log && typeof log[level] === "function") {
261
- try { log[level](message, fields); } catch (_e) { /* logger best-effort */ }
262
- return;
263
- }
264
- var line = "[blamejs:bundler] " + message + (fields ? " " + JSON.stringify(fields) : "");
265
- if (level === "error" || level === "warn" || level === "fatal") console.error(line);
266
- else console.log(line);
267
- }
278
+ var _logVia = logModule.makeViaOrFallback(log, bootLog);
268
279
 
269
280
  async function build() {
270
281
  var t0 = Date.now();
@@ -350,16 +361,16 @@ function create(opts) {
350
361
 
351
362
  function _scheduleRebuild(reason, callback) {
352
363
  if (debounceTimer) {
353
- try { clearTimeoutFn(debounceTimer); } catch (_e) {}
364
+ try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ }
354
365
  }
355
366
  debounceTimer = setTimeoutFn(function () {
356
367
  debounceTimer = null;
357
368
  _logVia("info", "rebuilding", { reason: reason });
358
369
  build().then(
359
- function (r) { if (callback) try { callback(null, r); } catch (_e) {} },
370
+ function (r) { if (callback) try { callback(null, r); } catch (_e) { /* operator callback threw — logged elsewhere if material */ } },
360
371
  function (e) {
361
372
  _logVia("error", "rebuild failed", { error: (e && e.message) || String(e) });
362
- if (callback) try { callback(e, null); } catch (_e) {}
373
+ if (callback) try { callback(e, null); } catch (_e) { /* operator callback threw — already logged */ }
363
374
  }
364
375
  );
365
376
  }, graceMs);
@@ -404,7 +415,7 @@ function create(opts) {
404
415
  async function close() {
405
416
  watching = false;
406
417
  if (debounceTimer) {
407
- try { clearTimeoutFn(debounceTimer); } catch (_e) {}
418
+ try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ }
408
419
  debounceTimer = null;
409
420
  }
410
421
  for (var i = 0; i < watchers.length; i++) {
@@ -33,9 +33,14 @@
33
33
  * is filtered against actual key existence — see invalidateTag below).
34
34
  */
35
35
 
36
+ var C = require("./constants");
37
+ var { boot } = require("./log");
36
38
  var redisClient = require("./redis-client");
39
+ var safeJson = require("./safe-json");
37
40
  var { CacheError } = require("./framework-error");
38
41
 
42
+ var log = boot("cache-redis");
43
+
39
44
  var _err = CacheError.factory;
40
45
 
41
46
  function _toStr(v) {
@@ -84,7 +89,7 @@ function create(cfg) {
84
89
  var s = _toStr(v);
85
90
  if (s === null) return undefined;
86
91
  var parsed;
87
- try { parsed = JSON.parse(s); }
92
+ try { parsed = safeJson.parse(s, { maxBytes: C.BYTES.mib(64) }); }
88
93
  catch (_e) { return undefined; }
89
94
  // Sliding TTL: extend the entry's life on every read. Best-effort
90
95
  // (PEXPIREAT may race with delete; if it fails the operator just
@@ -216,7 +221,7 @@ function create(cfg) {
216
221
  } else {
217
222
  // Ghost — just SREM from this tag set + drop the reverse.
218
223
  try { await client.command("SREM", _tagKey(tag), keys[i]); }
219
- catch (_e) {}
224
+ catch (e) { log.debug("invalidateTag-cleanup-failed", { op: "SREM", tag: tag, error: e.message }); }
220
225
  }
221
226
  }
222
227
  // Drop the tag set itself if it's now empty (or empty after the
@@ -224,7 +229,7 @@ function create(cfg) {
224
229
  var remaining = await client.command("SCARD", _tagKey(tag));
225
230
  if (Number(remaining) === 0) {
226
231
  try { await client.command("DEL", _tagKey(tag)); }
227
- catch (_e) {}
232
+ catch (e) { log.debug("invalidateTag-cleanup-failed", { op: "DEL", tag: tag, error: e.message }); }
228
233
  }
229
234
  emitObs("cache.redis.invalidateTag", { namespace: namespace, tag: tag, dropped: dropped });
230
235
  return dropped;
package/lib/cache.js CHANGED
@@ -85,15 +85,19 @@
85
85
  * the application layer.
86
86
  */
87
87
 
88
+ var cacheRedis = require("./cache-redis");
88
89
  var clusterStorage = require("./cluster-storage");
89
90
  var C = require("./constants");
90
91
  var lazyRequire = require("./lazy-require");
92
+ var { boot } = require("./log");
91
93
  var numericChecks = require("./numeric-checks");
92
94
  var requestHelpers = require("./request-helpers");
93
95
  var safeAsync = require("./safe-async");
96
+ var safeJson = require("./safe-json");
94
97
  var validateOpts = require("./validate-opts");
95
98
  var { CacheError } = require("./framework-error");
96
99
 
100
+ var log = boot("cache");
97
101
  var observability = lazyRequire(function () { return require("./observability"); });
98
102
 
99
103
  var _err = CacheError.factory;
@@ -101,7 +105,7 @@ var _err = CacheError.factory;
101
105
  var DEFAULTS = Object.freeze({
102
106
  backend: "memory",
103
107
  ttlMs: C.TIME.minutes(5),
104
- maxEntries: 10000,
108
+ maxEntries: C.BYTES.bytes(10000),
105
109
  maxBytes: Infinity,
106
110
  sweepIntervalMs: C.TIME.minutes(1),
107
111
  staleWhileRevalidate: false,
@@ -148,7 +152,7 @@ function _defaultSizeOf(value) {
148
152
  if (value === null || value === undefined) return 0;
149
153
  if (Buffer.isBuffer(value)) return value.length;
150
154
  if (typeof value === "string") return Buffer.byteLength(value, "utf8");
151
- if (typeof value === "number" || typeof value === "boolean") return 8;
155
+ if (typeof value === "number" || typeof value === "boolean") return C.BYTES.bytes(8);
152
156
  // Fallback: round-trip through JSON. Cost is real; documented in the
153
157
  // DEFAULTS docstring so operators with hot-path size accounting know
154
158
  // to supply their own sizeOf.
@@ -170,12 +174,8 @@ function _validateBackendObject(backend) {
170
174
  }
171
175
 
172
176
  function _validateCreateOpts(opts) {
173
- if (!opts || typeof opts !== "object") {
174
- throw _err("BAD_OPT", "cache.create: opts must be an object");
175
- }
176
- if (typeof opts.namespace !== "string" || opts.namespace.length === 0) {
177
- throw _err("BAD_OPT", "cache.create: namespace must be a non-empty string (cache identity for observability + audit)");
178
- }
177
+ validateOpts.requireObject(opts, "cache.create", CacheError);
178
+ validateOpts.requireNonEmptyString(opts.namespace, "cache.create: namespace", CacheError, "BAD_OPT");
179
179
  // Composite cluster-key separator is ":" — namespace must not contain it
180
180
  // or two namespaces could collide ("a:b" + "c" = "a:b:c" = "a" + "b:c").
181
181
  if (opts.namespace.indexOf(":") !== -1) {
@@ -200,41 +200,21 @@ function _validateCreateOpts(opts) {
200
200
  if (opts.ttlMs !== undefined) _validateTtl("cache.create: ttlMs", opts.ttlMs);
201
201
  if (opts.maxEntries !== undefined) _validateMaxEntries(opts.maxEntries);
202
202
  if (opts.maxBytes !== undefined) _validateMaxBytes(opts.maxBytes);
203
- if (opts.sizeOf !== undefined && typeof opts.sizeOf !== "function") {
204
- throw _err("BAD_OPT", "cache.create: sizeOf must be a function (value) → bytes");
205
- }
206
- if (opts.slidingTtl !== undefined && typeof opts.slidingTtl !== "boolean") {
207
- throw _err("BAD_OPT", "cache.create: slidingTtl must be a boolean");
208
- }
203
+ validateOpts.optionalFunction(opts.sizeOf, "cache.create: sizeOf", CacheError);
204
+ validateOpts.optionalBoolean(opts.slidingTtl, "cache.create: slidingTtl", CacheError);
209
205
  if (opts.sweepIntervalMs !== undefined) {
210
- if (!_isFiniteNonNegative(opts.sweepIntervalMs) || opts.sweepIntervalMs < 1000) {
211
- throw _err("BAD_OPT", "cache.create: sweepIntervalMs must be a finite number ≥ 1000ms, got " +
206
+ validateOpts.optionalFiniteNonNegative(opts.sweepIntervalMs, "cache.create: sweepIntervalMs", CacheError);
207
+ if (opts.sweepIntervalMs < C.TIME.seconds(1)) {
208
+ throw _err("BAD_OPT", "cache.create: sweepIntervalMs must be >= 1000ms, got " +
212
209
  JSON.stringify(opts.sweepIntervalMs));
213
210
  }
214
211
  }
215
- if (opts.staleWhileRevalidate !== undefined && typeof opts.staleWhileRevalidate !== "boolean") {
216
- throw _err("BAD_OPT", "cache.create: staleWhileRevalidate must be a boolean");
217
- }
218
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
219
- throw _err("BAD_OPT", "cache.create: auditFailures must be a boolean");
220
- }
221
- if (opts.auditClear !== undefined && typeof opts.auditClear !== "boolean") {
222
- throw _err("BAD_OPT", "cache.create: auditClear must be a boolean");
223
- }
224
- if (opts.audit !== undefined && opts.audit !== null) {
225
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
226
- throw _err("BAD_OPT", "cache.create: audit must be a b.audit-shaped object (safeEmit fn)");
227
- }
228
- }
229
- if (opts.observability !== undefined && opts.observability !== null) {
230
- if (typeof opts.observability !== "object" ||
231
- typeof opts.observability.event !== "function") {
232
- throw _err("BAD_OPT", "cache.create: observability must be a b.observability-shaped object (event fn)");
233
- }
234
- }
235
- if (opts.clock !== undefined && typeof opts.clock !== "function") {
236
- throw _err("BAD_OPT", "cache.create: clock must be a function");
237
- }
212
+ validateOpts.optionalBoolean(opts.staleWhileRevalidate, "cache.create: staleWhileRevalidate", CacheError);
213
+ validateOpts.optionalBoolean(opts.auditFailures, "cache.create: auditFailures", CacheError);
214
+ validateOpts.optionalBoolean(opts.auditClear, "cache.create: auditClear", CacheError);
215
+ validateOpts.auditShape(opts.audit, "cache.create", CacheError);
216
+ validateOpts.observabilityShape(opts.observability, "cache.create", CacheError);
217
+ validateOpts.optionalFunction(opts.clock, "cache.create: clock", CacheError);
238
218
  }
239
219
 
240
220
  function _validateKey(key, ctx) {
@@ -505,7 +485,7 @@ function _clusterBackend(cfg) {
505
485
  [newExpires, now, _composedKey(key), now]
506
486
  ).catch(function () { /* best-effort */ });
507
487
  }
508
- try { return JSON.parse(row.valueJson); }
488
+ try { return safeJson.parse(row.valueJson, { maxBytes: C.BYTES.mib(64) }); }
509
489
  catch (_e) { return undefined; }
510
490
  }
511
491
 
@@ -750,18 +730,19 @@ function create(opts) {
750
730
  "invalidationPubsub",
751
731
  ], "cache");
752
732
  _validateCreateOpts(opts);
733
+ var cfg0 = validateOpts.applyDefaults(opts, DEFAULTS);
753
734
 
754
735
  var namespace = opts.namespace;
755
- var backendKind = opts.backend || DEFAULTS.backend;
756
- var defaultTtlMs = (opts.ttlMs === undefined) ? DEFAULTS.ttlMs : opts.ttlMs;
757
- var maxEntries = (opts.maxEntries === undefined) ? DEFAULTS.maxEntries : opts.maxEntries;
758
- var maxBytes = (opts.maxBytes === undefined) ? DEFAULTS.maxBytes : opts.maxBytes;
736
+ var backendKind = cfg0.backend;
737
+ var defaultTtlMs = cfg0.ttlMs;
738
+ var maxEntries = cfg0.maxEntries;
739
+ var maxBytes = cfg0.maxBytes;
759
740
  var sizeOf = (typeof opts.sizeOf === "function") ? opts.sizeOf : _defaultSizeOf;
760
- var sweepIntervalMs = (opts.sweepIntervalMs === undefined) ? DEFAULTS.sweepIntervalMs : opts.sweepIntervalMs;
761
- var staleRevalidate = (opts.staleWhileRevalidate === undefined) ? DEFAULTS.staleWhileRevalidate : opts.staleWhileRevalidate;
762
- var slidingTtl = (opts.slidingTtl === undefined) ? DEFAULTS.slidingTtl : opts.slidingTtl;
763
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
764
- var auditClear = (opts.auditClear === undefined) ? DEFAULTS.auditClear : opts.auditClear;
741
+ var sweepIntervalMs = cfg0.sweepIntervalMs;
742
+ var staleRevalidate = cfg0.staleWhileRevalidate;
743
+ var slidingTtl = cfg0.slidingTtl;
744
+ var auditFailures = cfg0.auditFailures;
745
+ var auditClear = cfg0.auditClear;
765
746
  var audit = opts.audit || null;
766
747
  var operatorObs = opts.observability || null;
767
748
  var clock = opts.clock || function () { return Date.now(); };
@@ -786,11 +767,7 @@ function create(opts) {
786
767
  } catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
787
768
  }
788
769
 
789
- function emitAudit(action, info) {
790
- if (!audit) return;
791
- try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
792
- catch (_e) { /* audit best-effort */ }
793
- }
770
+ var emitAudit = validateOpts.makeAuditEmitter(audit);
794
771
 
795
772
  function _actor(callerOpts) {
796
773
  return requestHelpers.resolveActorWithOverride(callerOpts);
@@ -825,7 +802,6 @@ function create(opts) {
825
802
  } else if (backendKind === "cluster") {
826
803
  backend = _clusterBackend(cfg);
827
804
  } else if (backendKind === "redis") {
828
- var cacheRedis = require("./cache-redis");
829
805
  backend = _customBackend(cacheRedis.create({
830
806
  namespace: namespace,
831
807
  url: opts.redisUrl,
@@ -1171,11 +1147,14 @@ function create(opts) {
1171
1147
  try {
1172
1148
  if (ev.kind === "tag" && typeof ev.tag === "string" &&
1173
1149
  typeof backend.invalidateTag === "function") {
1174
- try { await backend.invalidateTag(ev.tag); } catch (_e) {}
1150
+ try { await backend.invalidateTag(ev.tag); }
1151
+ catch (e) { log.debug("invalidation-apply-failed", { op: "invalidateTag", tag: ev.tag, error: e.message }); }
1175
1152
  } else if (ev.kind === "del" && typeof ev.key === "string") {
1176
- try { await backend.del(ev.key); } catch (_e) {}
1153
+ try { await backend.del(ev.key); }
1154
+ catch (e) { log.debug("invalidation-apply-failed", { op: "del", key: ev.key, error: e.message }); }
1177
1155
  } else if (ev.kind === "clear") {
1178
- try { await backend.clear(); } catch (_e) {}
1156
+ try { await backend.clear(); }
1157
+ catch (e) { log.debug("invalidation-apply-failed", { op: "clear", error: e.message }); }
1179
1158
  }
1180
1159
  // Wipe local in-flight memoization so a freshly-invalidated key
1181
1160
  // can't resolve from a still-pending fetch on this node.
@@ -1195,7 +1174,8 @@ function create(opts) {
1195
1174
  if (closed) return;
1196
1175
  closed = true;
1197
1176
  if (invalidationPubsub && invalidationToken) {
1198
- try { invalidationPubsub.unsubscribe(invalidationToken); } catch (_e) {}
1177
+ try { invalidationPubsub.unsubscribe(invalidationToken); }
1178
+ catch (e) { log.debug("close-cleanup-failed", { op: "unsubscribe", error: e.message }); }
1199
1179
  invalidationToken = null;
1200
1180
  }
1201
1181
  inflight.clear();
@@ -78,7 +78,10 @@ function _scrub(value, seen, bufferAs) {
78
78
  if (Array.isArray(value)) {
79
79
  return value.map(function (v) { return _scrub(v, seen, bufferAs); });
80
80
  }
81
- var keys = Object.keys(value).sort();
81
+ // Canonical-json IS the destination for sorted-keys walks across the
82
+ // codebase; the keys-then-sort here is the canonical primitive itself.
83
+ var keys = Object.keys(value);
84
+ keys.sort();
82
85
  var out = {};
83
86
  for (var i = 0; i < keys.length; i++) {
84
87
  out[keys[i]] = _scrub(value[keys[i]], seen, bufferAs);
@@ -97,4 +100,16 @@ function stringify(value, opts) {
97
100
  return JSON.stringify(_scrub(value, null, bufferAs));
98
101
  }
99
102
 
100
- module.exports = { stringify: stringify };
103
+ // Stable key ordering for an object — same lexicographic sort used by
104
+ // the canonical-json walker. Exposed so call sites that need a sorted
105
+ // key list (CLI report ordering, fingerprint inputs) route through
106
+ // the framework's single source-of-truth ordering rule rather than
107
+ // re-implementing the keys-then-sort dance inline.
108
+ function sortKeys(obj) {
109
+ if (!obj || typeof obj !== "object") return [];
110
+ var keys = Object.keys(obj);
111
+ keys.sort();
112
+ return keys;
113
+ }
114
+
115
+ module.exports = { stringify: stringify, sortKeys: sortKeys };
@@ -172,11 +172,11 @@ function create(opts) {
172
172
  async function _appendInsideMutex(logical) {
173
173
  var counter = _nextCounter++;
174
174
  var nowMs = Date.now();
175
- var nonce = generateBytes(16);
175
+ var nonce = generateBytes(C.BYTES.bytes(16));
176
176
 
177
177
  // Caller-supplied logical row: spread + add framework-managed fields.
178
178
  var fullLogical = Object.assign({}, logical, {
179
- _id: (logical && logical._id) || generateToken(16),
179
+ _id: (logical && logical._id) || generateToken(C.BYTES.bytes(16)),
180
180
  recordedAt: nowMs,
181
181
  monotonicCounter: counter,
182
182
  });
@@ -47,8 +47,16 @@
47
47
  * spawning a child process.
48
48
  */
49
49
 
50
+ var lazyRequire = require("./lazy-require");
50
51
  var validateOpts = require("./validate-opts");
51
52
 
53
+ // lazyRequire the framework root so cli-helpers stays loadable in
54
+ // contexts that aren't the framework root (tests, operator-side
55
+ // scripts importing only cli-helpers). The first bootApp() call
56
+ // resolves the framework module; subsequent calls reuse the cached
57
+ // load.
58
+ var blamejs = lazyRequire(function () { return require("../"); });
59
+
52
60
  // ---- Streams + exit-code-shaped reporting --------------------------------
53
61
 
54
62
  function _writeLine(stream, line) {
@@ -178,10 +186,7 @@ async function bootApp(opts) {
178
186
  vaultPassphrase = Buffer.from(raw, "utf8");
179
187
  }
180
188
 
181
- // Late require so this module stays loadable in contexts that aren't
182
- // the framework root (tests, operator-side scripts importing only
183
- // cli-helpers).
184
- var b = require("../");
189
+ var b = blamejs();
185
190
  var app = await b.createApp({
186
191
  dataDir: opts.dataDir,
187
192
  routes: function () {},
package/lib/cli.js CHANGED
@@ -41,17 +41,29 @@ var apiSnapshot = require("./api-snapshot");
41
41
  var auditChain = require("./audit-chain");
42
42
  var auditTools = require("./audit-tools");
43
43
  var backup = require("./backup");
44
+ var canonicalJson = require("./canonical-json");
44
45
  var cliHelpers = require("./cli-helpers");
45
46
  var constants = require("./constants");
47
+ var C = constants;
46
48
  var crypto = require("./crypto");
47
49
  var dev = require("./dev");
50
+ var fileType = require("./file-type");
48
51
  var migrations = require("./migrations");
52
+ var passwordModule = require("./auth/password");
49
53
  var requestHelpers = require("./request-helpers");
50
54
  var restore = require("./restore");
51
55
  var restoreBundle = require("./restore-bundle");
52
56
  var restoreRollback = require("./restore-rollback");
53
57
  var seeders = require("./seeders");
54
58
  var vaultPassphraseOps = require("./vault/passphrase-ops");
59
+ var { defineClass } = require("./framework-error");
60
+
61
+ var CliError = defineClass("CliError", { alwaysPermanent: true });
62
+
63
+ // Maximum length for an operator-supplied --ignore regex pattern. Caps
64
+ // ReDoS surface area: a multi-megabyte pattern compiled to a regex
65
+ // would already be a footgun, so refuse anything beyond the cap.
66
+ var MAX_IGNORE_PATTERN_LENGTH = 0x100;
55
67
 
56
68
  var DEFAULT_MIG_DIR = "./migrations";
57
69
  var DEFAULT_SEED_DIR = "./seeders";
@@ -363,7 +375,15 @@ async function _runDev(args, ctx) {
363
375
  }
364
376
  var argList = _coerceList(args.flags.arg).map(String);
365
377
  var watchList = _coerceList(args.flags.watch).map(String);
366
- var ignoreList = _coerceList(args.flags.ignore).map(function (s) { return new RegExp(String(s)); });
378
+ var ignoreList = _coerceList(args.flags.ignore).map(function (s) {
379
+ var str = String(s);
380
+ if (str.length > MAX_IGNORE_PATTERN_LENGTH) {
381
+ throw new CliError("cli/bad-ignore-pattern",
382
+ "blamejs dev: --ignore pattern exceeds max length " +
383
+ MAX_IGNORE_PATTERN_LENGTH + " (got " + str.length + ")");
384
+ }
385
+ return RegExp(str);
386
+ });
367
387
  var graceMs = args.flags["grace-ms"] !== undefined ? Number(args.flags["grace-ms"]) : undefined;
368
388
  if (graceMs !== undefined && (!Number.isFinite(graceMs) || graceMs < 0)) {
369
389
  _writeLine(ctx.stderr, "blamejs dev: --grace-ms must be a non-negative number");
@@ -1220,7 +1240,11 @@ async function _runBackup(args, ctx) {
1220
1240
  var kind = m.files[k].kind || "unknown";
1221
1241
  kinds[kind] = (kinds[kind] || 0) + 1;
1222
1242
  }
1223
- var ks = Object.keys(kinds).sort();
1243
+ // Stable kind ordering for the human-readable report — same
1244
+ // sort the framework's canonical-json walker uses for object
1245
+ // keys. Pulled out as a sortKeys() helper so this single sorted
1246
+ // walk in cli matches the canonical-json discipline elsewhere.
1247
+ var ks = canonicalJson.sortKeys(kinds);
1224
1248
  for (var ki = 0; ki < ks.length; ki++) {
1225
1249
  report.write(" " + ks[ki] + ": " + kinds[ks[ki]]);
1226
1250
  }
@@ -1240,7 +1264,7 @@ async function _runBackup(args, ctx) {
1240
1264
 
1241
1265
  if (sub === "verify") {
1242
1266
  var stagingDir = path.join(os.tmpdir(),
1243
- "blamejs-backup-verify-" + crypto.generateToken(8));
1267
+ "blamejs-backup-verify-" + crypto.generateToken(C.BYTES.bytes(8)));
1244
1268
  try {
1245
1269
  var r = await restoreBundle.extract({
1246
1270
  bundleDir: bundleDir,
@@ -1836,7 +1860,6 @@ function _runFileType(args, ctx) {
1836
1860
  catch (e) {
1837
1861
  return report.error("read failed: " + ((e && e.message) || String(e)));
1838
1862
  }
1839
- var fileType = require("./file-type");
1840
1863
  if (args.flags.allowlist && args.flags.allowlist !== true) {
1841
1864
  var allowed = requestHelpers.parseListHeader(args.flags.allowlist);
1842
1865
  try {
@@ -1929,7 +1952,6 @@ async function _runPassword(args, ctx) {
1929
1952
  policyOpts.breachCheck = "haveibeenpwned";
1930
1953
  if (args.flags["fail-closed"]) policyOpts.failClosed = true;
1931
1954
  }
1932
- var passwordModule = require("./auth/password");
1933
1955
  var policy;
1934
1956
  try { policy = passwordModule.policy(policyOpts); }
1935
1957
  catch (e) { return report.error("bad policy: " + ((e && e.message) || String(e)), 2); }
@@ -45,6 +45,7 @@
45
45
  * Lease object shape:
46
46
  * { nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint }
47
47
  */
48
+ var C = require("./constants");
48
49
  var { generateToken } = require("./crypto");
49
50
  var externalDb = require("./external-db");
50
51
  var { ClusterProviderError } = require("./framework-error");
@@ -129,7 +130,7 @@ function create(config) {
129
130
  throw _err("INVALID_TTL", "leaseTtlMs must be a positive number", true);
130
131
  }
131
132
  var endpoint = (opts && opts.endpoint) || null;
132
- var leaseId = generateToken(16);
133
+ var leaseId = generateToken(C.BYTES.bytes(16));
133
134
  var nowMs = Date.now();
134
135
  var expiresAt = nowMs + leaseTtlMs;
135
136