@blamejs/core 0.5.17 → 0.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - **0.5.17** (2026-05-01) — primitive-drift sweep: csv unify + 3 new shared primitives
11
12
  - **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
12
13
  - **0.5.15** (2026-04-30) — b.archive: ZIP creation
13
14
  - **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
@@ -243,6 +243,11 @@ function create(opts) {
243
243
  // running backup against an external db handle still work).
244
244
  if (flushBeforeBackup === null && opts.flushBeforeBackup !== false) {
245
245
  try {
246
+ // Inline require: backup is a leaf module the framework re-exports
247
+ // without forcing operators to also bring in lib/db. Hoisting to
248
+ // top-of-file would require everyone using `b.backup` from
249
+ // outside the framework to have ./db loadable in the same module
250
+ // graph, which CLI tools and stand-alone backup runners don't.
246
251
  var dbModule = require("../db");
247
252
  if (typeof dbModule.flushToDisk === "function") {
248
253
  flushBeforeBackup = function () { dbModule.flushToDisk(); };
package/lib/cluster.js CHANGED
@@ -51,11 +51,21 @@
51
51
  * await cluster.shutdown() releases lease, stops heartbeat
52
52
  */
53
53
  var C = require("./constants");
54
+ var clusterProviderDb = require("./cluster-provider-db");
55
+ var crypto = require("./crypto");
56
+ var lazyRequire = require("./lazy-require");
54
57
  var { boot } = require("./log");
55
58
  var safeAsync = require("./safe-async");
59
+ var safeJson = require("./safe-json");
56
60
  var safeUrl = require("./safe-url");
57
61
  var { FrameworkError, ClusterError } = require("./framework-error");
58
62
 
63
+ // Lazy: vault → db → cluster forms a load-time chain, and external-db is
64
+ // loaded before its init has run; both are safe to call once cluster
65
+ // reaches runtime, but eager require here would deadlock the load order.
66
+ var externalDb = lazyRequire(function () { return require("./external-db"); });
67
+ var vault = lazyRequire(function () { return require("./vault"); });
68
+
59
69
  var DEFAULT_LEASE_TTL = C.TIME.seconds(30);
60
70
  var DEFAULT_HEARTBEAT = C.TIME.seconds(10);
61
71
  var MIN_LEASE_TTL = C.TIME.seconds(5);
@@ -185,8 +195,7 @@ async function init(opts) {
185
195
  throw _err("INVALID_CONFIG",
186
196
  "cluster.init requires either { provider } or { externalDbBackend }", true);
187
197
  }
188
- var dbProvider = require("./cluster-provider-db");
189
- provider = dbProvider.create({
198
+ provider = clusterProviderDb.create({
190
199
  externalDbBackend: opts.externalDbBackend,
191
200
  dialect: opts.dialect,
192
201
  });
@@ -264,15 +273,9 @@ async function init(opts) {
264
273
  // hash → FATAL via process.exit(1). Same posture as the
265
274
  // single-node audit.tip sidecar rollback check.
266
275
  async function _checkChainTipRollback(chainName, logTable, tipTable) {
267
- // Lazy require because external-db isn't available before
268
- // cluster.init (and cluster is required from many subsystems —
269
- // a top-of-file require would form a circular load with
270
- // external-db's audit emit path).
271
- var externalDb = require("./external-db");
272
-
273
276
  var tipRows;
274
277
  try {
275
- tipRows = await externalDb.query(
278
+ tipRows = await externalDb().query(
276
279
  "SELECT atMonotonicCounter, rowHash FROM " + tipTable +
277
280
  " WHERE scope = '" + chainName + "'",
278
281
  [],
@@ -294,7 +297,7 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
294
297
  var tipCounter = Number(tip.atMonotonicCounter);
295
298
  var tipHash = tip.rowHash;
296
299
 
297
- var currentRows = await externalDb.query(
300
+ var currentRows = await externalDb().query(
298
301
  "SELECT MAX(monotonicCounter) AS m FROM " + logTable,
299
302
  [],
300
303
  { backend: configuredExternalDbBackend }
@@ -313,7 +316,7 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
313
316
  }
314
317
 
315
318
  if (tipHash) {
316
- var hashRows = await externalDb.query(
319
+ var hashRows = await externalDb().query(
317
320
  "SELECT rowHash FROM " + logTable + " WHERE monotonicCounter = " +
318
321
  (configuredDialect === "postgres" ? "$1" : "?"),
319
322
  [tipCounter],
@@ -347,14 +350,9 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
347
350
  // posture as the audit-tip rollback check skipping when there's no
348
351
  // audit-tip table.
349
352
  function _vaultKeyFingerprint() {
350
- // Lazy require to avoid the circular load risk: vault → db → cluster
351
- // (the audit module pulls cluster in, db loads audit at init).
352
- var vault = require("./vault");
353
- var crypto = require("./crypto");
354
- var safeJson = require("./safe-json");
355
353
  var keysJson;
356
354
  try {
357
- keysJson = vault.getKeysJson();
355
+ keysJson = vault().getKeysJson();
358
356
  } catch (e) {
359
357
  // vault.init() not called — gates-only mode. Skip silently.
360
358
  if (/vault.init\(\) must be awaited/.test((e && e.message) || "")) {
@@ -380,7 +378,6 @@ async function _checkVaultKeyConsistency() {
380
378
  log("vault not initialized — skipping vault-key consistency check (cluster gates-only mode)");
381
379
  return;
382
380
  }
383
- var externalDb = require("./external-db");
384
381
  var nowMs = Date.now();
385
382
  var ph = configuredDialect === "postgres";
386
383
 
@@ -390,7 +387,7 @@ async function _checkVaultKeyConsistency() {
390
387
  // compares — any mismatch (including ours after a losing race)
391
388
  // surfaces the drift.
392
389
  try {
393
- await externalDb.query(
390
+ await externalDb().query(
394
391
  "INSERT INTO _blamejs_cluster_state " +
395
392
  " (scope, vaultKeyFp, recordedAt, recordedByNode) " +
396
393
  "VALUES ('state', " +
@@ -413,7 +410,7 @@ async function _checkVaultKeyConsistency() {
413
410
 
414
411
  // Read whatever fingerprint is canonical (ours if first boot,
415
412
  // someone else's if we lost the race or are joining an existing cluster).
416
- var rows = await externalDb.query(
413
+ var rows = await externalDb().query(
417
414
  "SELECT vaultKeyFp, recordedByNode, recordedAt FROM _blamejs_cluster_state " +
418
415
  "WHERE scope = 'state'",
419
416
  [],
package/lib/db.js CHANGED
@@ -55,11 +55,17 @@ var cryptoField = require("./crypto-field");
55
55
  var { Query } = require("./db-query");
56
56
  var dbSchema = require("./db-schema");
57
57
  var { boot } = require("./log");
58
+ var lazyRequire = require("./lazy-require");
58
59
  var safeAsync = require("./safe-async");
59
60
  var safeEnv = require("./parsers/safe-env");
60
61
  var safeJson = require("./safe-json");
61
62
  var vault = require("./vault");
62
63
 
64
+ // Lazy: cluster-storage's _localDb pulls db back in, so eager require
65
+ // would deadlock the load order. cluster-storage is only used on the
66
+ // purge-audit-chain external-db path, which always runs after init.
67
+ var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
68
+
63
69
  var AUDIT_TIP_SCHEMA = {
64
70
  type: "object",
65
71
  required: ["atMonotonicCounter"],
@@ -1119,10 +1125,9 @@ module.exports = {
1119
1125
  if (!Number.isFinite(lastPurgedCounter) || lastPurgedCounter < 0) {
1120
1126
  throw new Error("purgeAuditChain: lastPurgedCounter must be a non-negative number");
1121
1127
  }
1122
- var c = require("./cluster");
1123
- if (c.isClusterMode()) {
1128
+ if (cluster.isClusterMode()) {
1124
1129
  // External-db has no append-only triggers; ordinary DELETE works.
1125
- var cs = require("./cluster-storage");
1130
+ var cs = clusterStorage();
1126
1131
  var d = await cs.execute(
1127
1132
  "DELETE FROM audit_log WHERE monotonicCounter <= ?", [lastPurgedCounter]
1128
1133
  );
package/lib/deprecate.js CHANGED
@@ -50,6 +50,7 @@
50
50
  * call counter is still incremented so dep.list() shows usage volume.
51
51
  */
52
52
 
53
+ var safeEnv = require("./parsers/safe-env");
53
54
  var { FrameworkError } = require("./framework-error");
54
55
 
55
56
  class DeprecateError extends FrameworkError {
@@ -65,12 +66,12 @@ class DeprecateError extends FrameworkError {
65
66
  var _seen = new Map();
66
67
 
67
68
  function _modeFromEnv() {
68
- var env = process.env.BLAMEJS_DEPRECATIONS;
69
+ var env = safeEnv.readVar("BLAMEJS_DEPRECATIONS");
69
70
  if (typeof env === "string" && env.length > 0) {
70
71
  var v = env.toLowerCase();
71
72
  if (v === "warn" || v === "silent" || v === "error") return v;
72
73
  }
73
- if (process.env.NODE_ENV === "production") return "silent";
74
+ if (safeEnv.readVar("NODE_ENV") === "production") return "silent";
74
75
  return "warn";
75
76
  }
76
77
 
package/lib/error-page.js CHANGED
@@ -40,6 +40,7 @@
40
40
 
41
41
  var lazyRequire = require("./lazy-require");
42
42
  var requestHelpers = require("./request-helpers");
43
+ var safeEnv = require("./parsers/safe-env");
43
44
  var template = require("./template");
44
45
  var audit = lazyRequire(function () { return require("./audit"); });
45
46
 
@@ -273,7 +274,7 @@ function create(opts) {
273
274
  if (modeOpt === "dev" || modeOpt === "prod") {
274
275
  mode = modeOpt;
275
276
  } else {
276
- var nodeEnv = process.env.NODE_ENV;
277
+ var nodeEnv = safeEnv.readVar("NODE_ENV");
277
278
  mode = (nodeEnv === "production") ? "prod" : "dev";
278
279
  }
279
280
 
@@ -43,6 +43,7 @@
43
43
  * }
44
44
  */
45
45
 
46
+ var C = require("./constants");
46
47
  var safeUrl = require("./safe-url");
47
48
  var { defineClass } = require("./framework-error");
48
49
 
@@ -186,7 +187,7 @@ function create(opts) {
186
187
  if (attrs["max-age"] !== undefined) {
187
188
  var maxAge = parseInt(attrs["max-age"], 10);
188
189
  if (!isNaN(maxAge)) {
189
- expiresAt = maxAge <= 0 ? 0 : (now + maxAge * 1000);
190
+ expiresAt = maxAge <= 0 ? 0 : (now + C.TIME.seconds(maxAge));
190
191
  }
191
192
  } else if (attrs.expires) {
192
193
  expiresAt = _parseHttpDate(attrs.expires);
package/lib/log.js CHANGED
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  var { AsyncLocalStorage } = require("node:async_hooks");
63
+ var nodeCrypto = require("node:crypto");
63
64
  var redact = require("./redact");
64
65
  var validateOpts = require("./validate-opts");
65
66
  var { FrameworkError } = require("./framework-error");
@@ -315,7 +316,7 @@ function create(opts) {
315
316
  ? mwOpts.generate
316
317
  : function () {
317
318
  // 16 random hex chars — short, sufficient correlation entropy
318
- return require("crypto").randomBytes(8).toString("hex");
319
+ return nodeCrypto.randomBytes(8).toString("hex");
319
320
  };
320
321
  return function logRequestIdMiddleware(req, res, next) {
321
322
  var inbound = req.headers && req.headers[headerName];
@@ -174,7 +174,7 @@ function create(opts) {
174
174
  validateOpts(opts, [
175
175
  "keypair", "keypairs", "replayWindowMs", "pruneIntervalMs",
176
176
  "nonceStore", "exemptPaths", "contentTypes", "audit",
177
- "maxDecryptedBytes",
177
+ "maxDecryptedBytes", "trustProxy",
178
178
  ], "middleware.apiEncrypt");
179
179
  var keypairs = _resolveKeypairs(opts);
180
180
  var activeKeypair = keypairs[0];
@@ -205,6 +205,7 @@ function create(opts) {
205
205
  ? opts.contentTypes.slice()
206
206
  : DEFAULT_CONTENT_TYPES.slice());
207
207
  var auditOn = opts.audit !== false;
208
+ var trustProxy = opts.trustProxy === true;
208
209
  var lastPruneAt = 0;
209
210
 
210
211
  function _isExempt(req) {
@@ -233,7 +234,7 @@ function create(opts) {
233
234
  function _emitFailure(req, reason) {
234
235
  var info = {
235
236
  reason: reason,
236
- ip: (req.socket && req.socket.remoteAddress) || null,
237
+ ip: requestHelpers.clientIp(req, { trustProxy: trustProxy }),
237
238
  path: req.pathname || (req.url || "/").split("?")[0],
238
239
  method: req.method,
239
240
  ts: new Date().toISOString(),
package/lib/notify.js CHANGED
@@ -48,6 +48,7 @@
48
48
  */
49
49
 
50
50
  var lazyRequire = require("./lazy-require");
51
+ var bootLog = require("./log");
51
52
  var requestHelpers = require("./request-helpers");
52
53
  var safeAsync = require("./safe-async");
53
54
  var safeUrl = require("./safe-url");
@@ -274,7 +275,6 @@ function logTransport(opts) {
274
275
  if (opts.logger && typeof opts.logger.info === "function") {
275
276
  logger = opts.logger;
276
277
  } else {
277
- var bootLog = require("./log");
278
278
  logger = bootLog.boot("notify.log");
279
279
  // boot returns a fn-with-fields shape; tolerate both.
280
280
  if (typeof logger === "function") {
package/lib/pagination.js CHANGED
@@ -89,6 +89,7 @@
89
89
  */
90
90
 
91
91
  var nodeCrypto = require("node:crypto");
92
+ var crypto = require("./crypto");
92
93
  var { defineClass } = require("./framework-error");
93
94
 
94
95
  var PaginationError = defineClass("PaginationError", { alwaysPermanent: true });
@@ -171,7 +172,7 @@ function decodeCursor(token, secret) {
171
172
  throw new PaginationError("pagination/bad-cursor", "cursor base64 decode failed");
172
173
  }
173
174
  var expected = _tag(sb, json);
174
- if (tag.length !== expected.length || !nodeCrypto.timingSafeEqual(tag, expected)) {
175
+ if (!crypto.timingSafeEqual(tag, expected)) {
175
176
  throw new PaginationError("pagination/cursor-tag-mismatch",
176
177
  "cursor HMAC verification failed (tampered or wrong secret)");
177
178
  }
@@ -57,6 +57,7 @@
57
57
  var C = require("../constants");
58
58
  var atomicFile = require("../atomic-file");
59
59
  var safeBuffer = require("../safe-buffer");
60
+ var safeJson = require("../safe-json");
60
61
  var { FrameworkError } = require("../framework-error");
61
62
  var { boot } = require("../log");
62
63
 
@@ -275,7 +276,6 @@ function _coerceType(rawValue, type, key) {
275
276
  );
276
277
  }
277
278
  if (type === "json") {
278
- var safeJson = require("./../safe-json");
279
279
  try { return safeJson.parse(rawValue); }
280
280
  catch (e) {
281
281
  throw new SafeEnvError("invalid JSON for key '" + key + "': " + e.message,
@@ -437,7 +437,6 @@ function load(filepath, opts) {
437
437
  if (snapshotPath && atomicFile.exists(snapshotPath)) {
438
438
  try {
439
439
  var snapBuf = atomicFile.readSync(snapshotPath);
440
- var safeJson = require("./../safe-json");
441
440
  prevValues = safeJson.parse(snapBuf) || {};
442
441
  } catch (_e) { /* missing/corrupt snapshot → treat as empty */ }
443
442
  }
@@ -31,6 +31,7 @@
31
31
  */
32
32
  var cluster = require("./cluster");
33
33
  var clusterStorage = require("./cluster-storage");
34
+ var C = require("./constants");
34
35
  var { generateToken } = require("./crypto");
35
36
  var cryptoField = require("./crypto-field");
36
37
  var lazyRequire = require("./lazy-require");
@@ -108,7 +109,7 @@ function create(_config) {
108
109
  cluster.requireLeader();
109
110
  opts = opts || {};
110
111
  var nowMs = Date.now();
111
- var availableAt = nowMs + (opts.delaySeconds ? opts.delaySeconds * 1000 : 0);
112
+ var availableAt = nowMs + (opts.delaySeconds ? C.TIME.seconds(opts.delaySeconds) : 0);
112
113
 
113
114
  var priority = (typeof opts.priority === "number" && isFinite(opts.priority))
114
115
  ? Math.floor(opts.priority) : 0;
package/lib/queue.js CHANGED
@@ -31,6 +31,7 @@
31
31
  * failed (status='failed')
32
32
  */
33
33
  var C = require("./constants");
34
+ var clusterStorage = require("./cluster-storage");
34
35
  var crypto = require("./crypto");
35
36
  var lazyRequire = require("./lazy-require");
36
37
  var observability = require("./observability");
@@ -196,7 +197,7 @@ function consume(queueName, handler, opts) {
196
197
  }
197
198
  rateLimit = {
198
199
  max: opts.rateLimit.max,
199
- windowMs: opts.rateLimit.perSeconds * 1000,
200
+ windowMs: C.TIME.seconds(opts.rateLimit.perSeconds),
200
201
  timestamps: [],
201
202
  };
202
203
  }
@@ -602,7 +603,6 @@ function enqueueFlow(spec) {
602
603
  }
603
604
  // Second pass: write dependsOn (translated to jobIds) for children
604
605
  // that need it, and parking-lot their availableAt to MAX_SAFE_INTEGER.
605
- var clusterStorage = require("./cluster-storage");
606
606
  for (var q = 0; q < jobs.length; q++) {
607
607
  var j = jobs[q];
608
608
  if (j.dependsOn.length === 0) continue;
package/lib/router.js CHANGED
@@ -26,6 +26,7 @@ var path = require("path");
26
26
  var { URL } = require("url");
27
27
  var C = require("./constants");
28
28
  var safeAsync = require("./safe-async");
29
+ var safeEnv = require("./parsers/safe-env");
29
30
  var websocket = require("./websocket");
30
31
  var { boot } = require("./log");
31
32
 
@@ -112,7 +113,7 @@ function _makeResponseValidator(spec) {
112
113
  // - BLAMEJS_VALIDATE_RESPONSES=warn (or per-route validateResponse: "warn")
113
114
  // → log a warning; ship the response as-is (prod-safe).
114
115
  var perRoute = spec.validateResponse;
115
- var globalMode = process.env.BLAMEJS_VALIDATE_RESPONSES;
116
+ var globalMode = safeEnv.readVar("BLAMEJS_VALIDATE_RESPONSES");
116
117
  var mode = (perRoute === "throw" || perRoute === "warn") ? perRoute :
117
118
  (globalMode === "throw" || globalMode === "warn") ? globalMode : null;
118
119
  if (!mode) return function passthrough(_req, _res, next) { next(); };
@@ -260,9 +261,10 @@ class Router {
260
261
  // route dispatch) but before any route-specific handler.
261
262
  handlers = [_makeSchemaValidator(split.spec)].concat(handlers);
262
263
  // Response validation (dev/opt-in via env or per-route opt).
264
+ var globalValidateMode = safeEnv.readVar("BLAMEJS_VALIDATE_RESPONSES");
263
265
  if (split.spec.response &&
264
- (process.env.BLAMEJS_VALIDATE_RESPONSES === "throw" ||
265
- process.env.BLAMEJS_VALIDATE_RESPONSES === "warn" ||
266
+ (globalValidateMode === "throw" ||
267
+ globalValidateMode === "warn" ||
266
268
  split.spec.validateResponse)) {
267
269
  handlers = [_makeResponseValidator(split.spec)].concat(handlers);
268
270
  }
package/lib/testing.js CHANGED
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  var fs = require("node:fs");
63
+ var http = require("node:http");
63
64
  var os = require("node:os");
64
65
  var nodePath = require("node:path");
65
66
  var EventEmitter = require("node:events").EventEmitter;
@@ -567,7 +568,6 @@ function listenOnRandomPort(server, host) {
567
568
  // production traffic takes. Server is closed automatically when the
568
569
  // promise resolves or rejects.
569
570
  function request(target) {
570
- var http = require("node:http");
571
571
  // Resolve target → request listener
572
572
  var server;
573
573
  var ownsServer = false;
package/lib/totp.js CHANGED
@@ -60,6 +60,7 @@
60
60
  * 1Password, Bitwarden, Aegis, Microsoft Authenticator all do).
61
61
  */
62
62
  var nodeCrypto = require("crypto");
63
+ var crypto = require("./crypto");
63
64
  var { generateBytes, generateToken } = require("./crypto");
64
65
  var { AuthError } = require("./framework-error");
65
66
 
@@ -214,8 +215,7 @@ function verify(secret, code, opts) {
214
215
  try { expected = compute(secret, step, opts); }
215
216
  catch (_e) { return false; }
216
217
  var expectedBuf = Buffer.from(expected);
217
- if (expectedBuf.length === userBuf.length &&
218
- nodeCrypto.timingSafeEqual(expectedBuf, userBuf)) {
218
+ if (crypto.timingSafeEqual(expectedBuf, userBuf)) {
219
219
  return step;
220
220
  }
221
221
  }
package/lib/webhook.js CHANGED
@@ -539,7 +539,7 @@ function verifier(opts) {
539
539
 
540
540
  // Timestamp window: signed in seconds, compare to ms clock.
541
541
  var nowMs = nowFn();
542
- var ageMs = nowMs - (ts * 1000);
542
+ var ageMs = nowMs - C.TIME.seconds(ts);
543
543
  if (ageMs > toleranceMs) {
544
544
  throw _failure("EXPIRED", "webhook: timestamp older than toleranceMs (age=" + ageMs + "ms)", "expired", ctxReq);
545
545
  }
@@ -570,7 +570,7 @@ function verifier(opts) {
570
570
  }
571
571
 
572
572
  if (nonceStore) {
573
- var expireAt = (ts * 1000) + toleranceMs;
573
+ var expireAt = C.TIME.seconds(ts) + toleranceMs;
574
574
  var fresh = await nonceStore.checkAndInsert(parsed.id, expireAt);
575
575
  if (!fresh) {
576
576
  throw _failure("REPLAY", "webhook: id '" + parsed.id + "' has been seen before", "replay", ctxReq);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",