@blamejs/core 0.5.16 → 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 +2 -0
- package/lib/backup/index.js +7 -2
- package/lib/cache.js +5 -9
- package/lib/cli.js +4 -3
- package/lib/cluster.js +21 -24
- package/lib/csv.js +208 -162
- package/lib/db.js +13 -8
- package/lib/deprecate.js +3 -2
- package/lib/error-page.js +2 -1
- package/lib/external-db.js +4 -3
- package/lib/forms.js +3 -6
- package/lib/http-client-cookie-jar.js +2 -1
- package/lib/http-client.js +2 -2
- package/lib/log.js +2 -1
- package/lib/mail.js +2 -2
- package/lib/middleware/api-encrypt.js +3 -2
- package/lib/middleware/body-parser.js +2 -1
- package/lib/middleware/compression.js +3 -2
- package/lib/middleware/cors.js +1 -1
- package/lib/middleware/rate-limit.js +4 -4
- package/lib/mtls-engine-default.js +3 -2
- package/lib/nonce-store.js +4 -4
- package/lib/notify.js +1 -1
- package/lib/otel-export.js +5 -10
- package/lib/pagination.js +2 -1
- package/lib/parsers/index.js +3 -7
- package/lib/parsers/safe-env.js +1 -2
- package/lib/queue-local.js +2 -1
- package/lib/queue.js +8 -9
- package/lib/request-helpers.js +35 -3
- package/lib/restore.js +3 -3
- package/lib/router.js +5 -3
- package/lib/safe-async.js +122 -0
- package/lib/testing.js +1 -1
- package/lib/totp.js +2 -2
- package/lib/tracing.js +3 -3
- package/lib/webhook.js +5 -5
- package/lib/websocket.js +6 -4
- package/package.json +1 -1
- package/lib/parsers/safe-csv.js +0 -224
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ 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
|
|
12
|
+
- **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
|
|
11
13
|
- **0.5.15** (2026-04-30) — b.archive: ZIP creation
|
|
12
14
|
- **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
|
|
13
15
|
- **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
|
package/lib/backup/index.js
CHANGED
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
var fs = require("fs");
|
|
63
|
-
var nodeCrypto = require("node:crypto");
|
|
64
63
|
var os = require("os");
|
|
65
64
|
var path = require("path");
|
|
65
|
+
var crypto = require("../crypto");
|
|
66
66
|
var atomicFile = require("../atomic-file");
|
|
67
67
|
var backupBundle = require("./bundle");
|
|
68
68
|
var lazyRequire = require("../lazy-require");
|
|
@@ -80,7 +80,7 @@ function _isValidBundleId(s) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function _generateBundleId() {
|
|
83
|
-
return atomicFile.pathTimestamp() + "-" +
|
|
83
|
+
return atomicFile.pathTimestamp() + "-" + crypto.generateToken(4);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function _dirSize(p) {
|
|
@@ -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/cache.js
CHANGED
|
@@ -94,6 +94,7 @@ var clusterStorage = require("./cluster-storage");
|
|
|
94
94
|
var C = require("./constants");
|
|
95
95
|
var lazyRequire = require("./lazy-require");
|
|
96
96
|
var requestHelpers = require("./request-helpers");
|
|
97
|
+
var safeAsync = require("./safe-async");
|
|
97
98
|
var validateOpts = require("./validate-opts");
|
|
98
99
|
var { CacheError } = require("./framework-error");
|
|
99
100
|
|
|
@@ -434,12 +435,11 @@ function _memoryBackend(cfg) {
|
|
|
434
435
|
|
|
435
436
|
function _startSweep(intervalMs) {
|
|
436
437
|
if (sweepTimer) return;
|
|
437
|
-
sweepTimer =
|
|
438
|
-
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
438
|
+
sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep" });
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
async function close() {
|
|
442
|
-
if (sweepTimer) {
|
|
442
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
443
443
|
entries.clear();
|
|
444
444
|
tagIndex.clear();
|
|
445
445
|
totalBytes = 0;
|
|
@@ -579,15 +579,11 @@ function _clusterBackend(cfg) {
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
function _startSweep(intervalMs) {
|
|
582
|
-
|
|
583
|
-
_sweep().catch(function () { /* sweeper best-effort; next pass picks it up */ });
|
|
584
|
-
}, intervalMs);
|
|
585
|
-
if (typeof t.unref === "function") t.unref();
|
|
586
|
-
cfg._sweepTimer = t;
|
|
582
|
+
cfg._sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep-cluster" });
|
|
587
583
|
}
|
|
588
584
|
|
|
589
585
|
async function close() {
|
|
590
|
-
if (cfg._sweepTimer) {
|
|
586
|
+
if (cfg._sweepTimer) { cfg._sweepTimer.stop(); cfg._sweepTimer = null; }
|
|
591
587
|
}
|
|
592
588
|
|
|
593
589
|
return {
|
package/lib/cli.js
CHANGED
|
@@ -35,15 +35,16 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
var fs = require("node:fs");
|
|
38
|
-
var nodeCrypto = require("node:crypto");
|
|
39
38
|
var os = require("node:os");
|
|
40
39
|
var path = require("path");
|
|
41
40
|
var apiSnapshot = require("./api-snapshot");
|
|
42
41
|
var auditTools = require("./audit-tools");
|
|
43
42
|
var cliHelpers = require("./cli-helpers");
|
|
44
43
|
var constants = require("./constants");
|
|
44
|
+
var crypto = require("./crypto");
|
|
45
45
|
var dev = require("./dev");
|
|
46
46
|
var migrations = require("./migrations");
|
|
47
|
+
var requestHelpers = require("./request-helpers");
|
|
47
48
|
var restoreBundle = require("./restore-bundle");
|
|
48
49
|
var seeders = require("./seeders");
|
|
49
50
|
var vaultPassphraseOps = require("./vault/passphrase-ops");
|
|
@@ -746,7 +747,7 @@ async function _runApiKey(args, ctx) {
|
|
|
746
747
|
var scopes = args.flags.scopes;
|
|
747
748
|
if (!ownerId || ownerId === true) return report.error("--owner-id <id> is required", 2);
|
|
748
749
|
if (!scopes || scopes === true) return report.error("--scopes <comma-separated> is required", 2);
|
|
749
|
-
var scopeList =
|
|
750
|
+
var scopeList = requestHelpers.parseListHeader(scopes);
|
|
750
751
|
if (scopeList.length === 0) {
|
|
751
752
|
return report.error("--scopes must contain at least one non-empty scope", 2);
|
|
752
753
|
}
|
|
@@ -902,7 +903,7 @@ async function _runBackup(args, ctx) {
|
|
|
902
903
|
|
|
903
904
|
if (sub === "verify") {
|
|
904
905
|
var stagingDir = path.join(os.tmpdir(),
|
|
905
|
-
"blamejs-backup-verify-" +
|
|
906
|
+
"blamejs-backup-verify-" + crypto.generateToken(8));
|
|
906
907
|
try {
|
|
907
908
|
var r = await restoreBundle.extract({
|
|
908
909
|
bundleDir: bundleDir,
|
package/lib/cluster.js
CHANGED
|
@@ -51,10 +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");
|
|
58
|
+
var safeAsync = require("./safe-async");
|
|
59
|
+
var safeJson = require("./safe-json");
|
|
55
60
|
var safeUrl = require("./safe-url");
|
|
56
61
|
var { FrameworkError, ClusterError } = require("./framework-error");
|
|
57
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
|
+
|
|
58
69
|
var DEFAULT_LEASE_TTL = C.TIME.seconds(30);
|
|
59
70
|
var DEFAULT_HEARTBEAT = C.TIME.seconds(10);
|
|
60
71
|
var MIN_LEASE_TTL = C.TIME.seconds(5);
|
|
@@ -184,8 +195,7 @@ async function init(opts) {
|
|
|
184
195
|
throw _err("INVALID_CONFIG",
|
|
185
196
|
"cluster.init requires either { provider } or { externalDbBackend }", true);
|
|
186
197
|
}
|
|
187
|
-
|
|
188
|
-
provider = dbProvider.create({
|
|
198
|
+
provider = clusterProviderDb.create({
|
|
189
199
|
externalDbBackend: opts.externalDbBackend,
|
|
190
200
|
dialect: opts.dialect,
|
|
191
201
|
});
|
|
@@ -227,8 +237,7 @@ async function init(opts) {
|
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
// Start heartbeat
|
|
230
|
-
heartbeatTimer =
|
|
231
|
-
heartbeatTimer.unref();
|
|
240
|
+
heartbeatTimer = safeAsync.repeating(_heartbeat, heartbeatMs, { name: "cluster-heartbeat" });
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
// Cluster-mode equivalent of db.js's single-node audit.tip-sidecar
|
|
@@ -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
|
[],
|
|
@@ -619,7 +616,7 @@ function onTransition(handler) {
|
|
|
619
616
|
async function shutdown() {
|
|
620
617
|
if (!initialized) return;
|
|
621
618
|
if (heartbeatTimer) {
|
|
622
|
-
|
|
619
|
+
heartbeatTimer.stop();
|
|
623
620
|
heartbeatTimer = null;
|
|
624
621
|
}
|
|
625
622
|
if (lease) {
|
|
@@ -649,7 +646,7 @@ async function shutdown() {
|
|
|
649
646
|
// ---- test helpers — not part of public contract ----
|
|
650
647
|
|
|
651
648
|
function _resetForTest() {
|
|
652
|
-
if (heartbeatTimer)
|
|
649
|
+
if (heartbeatTimer) heartbeatTimer.stop();
|
|
653
650
|
heartbeatTimer = null;
|
|
654
651
|
initialized = false;
|
|
655
652
|
terminated = false;
|