@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/lib/mail.js
CHANGED
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
* diagnostic logs identify the provider that rejected the message.
|
|
64
64
|
*/
|
|
65
65
|
var C = require("./constants");
|
|
66
|
+
var crypto = require("./crypto");
|
|
66
67
|
var lazyRequire = require("./lazy-require");
|
|
67
|
-
var nodeCrypto = require("node:crypto");
|
|
68
68
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
69
69
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
70
70
|
var net = lazyRequire(function () { return require("net"); });
|
|
@@ -297,7 +297,7 @@ function _newBoundary(label) {
|
|
|
297
297
|
// convention. RFC 5322 only requires uniqueness within a message,
|
|
298
298
|
// but consistency with how every other identifier in lib/ is built
|
|
299
299
|
// wins over premature differentiation.
|
|
300
|
-
return "blamejs-" + label + "-" + Date.now() + "-" +
|
|
300
|
+
return "blamejs-" + label + "-" + Date.now() + "-" + crypto.generateToken(8);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// base64-encode the buffer with line wrapping at 76 chars (RFC 2045
|
|
@@ -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
|
|
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(),
|
|
@@ -113,6 +113,7 @@ var os = require("os");
|
|
|
113
113
|
var path = require("path");
|
|
114
114
|
var nodeCrypto = require("node:crypto");
|
|
115
115
|
var atomicFile = require("../atomic-file");
|
|
116
|
+
var crypto = require("../crypto");
|
|
116
117
|
var safeJson = require("../safe-json");
|
|
117
118
|
var validateOpts = require("../validate-opts");
|
|
118
119
|
var C = require("../constants");
|
|
@@ -708,7 +709,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
708
709
|
|
|
709
710
|
// Generate the tmp path — never derived from the
|
|
710
711
|
// operator-supplied filename.
|
|
711
|
-
var unique =
|
|
712
|
+
var unique = crypto.generateToken(16);
|
|
712
713
|
currentTmpPath = path.join(tmpDir, "blamejs-up-" + unique);
|
|
713
714
|
try {
|
|
714
715
|
currentFd = fs.openSync(currentTmpPath, "wx", 0o600);
|
|
@@ -209,9 +209,10 @@ function _appendVary(existing, token) {
|
|
|
209
209
|
if (!existing) return token;
|
|
210
210
|
var lc = String(existing).toLowerCase();
|
|
211
211
|
if (lc === "*") return "*"; // Vary: * already stops caches; don't dilute.
|
|
212
|
-
var parts =
|
|
212
|
+
var parts = requestHelpers.parseListHeader(existing);
|
|
213
|
+
var lcToken = token.toLowerCase();
|
|
213
214
|
for (var i = 0; i < parts.length; i++) {
|
|
214
|
-
if (parts[i].toLowerCase() ===
|
|
215
|
+
if (parts[i].toLowerCase() === lcToken) return String(existing);
|
|
215
216
|
}
|
|
216
217
|
parts.push(token);
|
|
217
218
|
return parts.join(", ");
|
package/lib/middleware/cors.js
CHANGED
|
@@ -236,7 +236,7 @@ function create(opts) {
|
|
|
236
236
|
if (refuseUnknown) {
|
|
237
237
|
var requestedHdrs = req.headers["access-control-request-headers"];
|
|
238
238
|
if (requestedHdrs) {
|
|
239
|
-
var asked =
|
|
239
|
+
var asked = requestHelpers.parseListHeader(requestedHdrs, { lowercase: true });
|
|
240
240
|
for (var ah = 0; ah < asked.length; ah++) {
|
|
241
241
|
if (allowedHeadersSet.indexOf(asked[ah]) === -1) {
|
|
242
242
|
if (typeof res.writeHead === "function") {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
var C = require("../constants");
|
|
47
47
|
var lazyRequire = require("../lazy-require");
|
|
48
48
|
var requestHelpers = require("../request-helpers");
|
|
49
|
+
var safeAsync = require("../safe-async");
|
|
49
50
|
var validateOpts = require("../validate-opts");
|
|
50
51
|
var clusterStorage = require("../cluster-storage");
|
|
51
52
|
|
|
@@ -71,13 +72,12 @@ function _memoryBackend(opts) {
|
|
|
71
72
|
var buckets = new Map();
|
|
72
73
|
|
|
73
74
|
// Periodic GC of stale buckets so the map doesn't grow unbounded.
|
|
74
|
-
var gcInterval =
|
|
75
|
+
var gcInterval = safeAsync.repeating(function () {
|
|
75
76
|
var cutoff = Date.now() - C.TIME.hours(1);
|
|
76
77
|
for (var k of buckets.keys()) {
|
|
77
78
|
if (buckets.get(k).lastRefillAt < cutoff) buckets.delete(k);
|
|
78
79
|
}
|
|
79
|
-
}, C.TIME.minutes(5));
|
|
80
|
-
if (typeof gcInterval.unref === "function") gcInterval.unref();
|
|
80
|
+
}, C.TIME.minutes(5), { name: "rate-limit-gc" });
|
|
81
81
|
|
|
82
82
|
// Memory backend returns verdicts SYNCHRONOUSLY — no awaitable.
|
|
83
83
|
// The middleware checks `.then` to decide whether to chain. Keeping
|
|
@@ -118,7 +118,7 @@ function _memoryBackend(opts) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function close() {
|
|
121
|
-
try {
|
|
121
|
+
try { gcInterval.stop(); } catch (_e) {}
|
|
122
122
|
buckets.clear();
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -32,6 +32,7 @@ var nodeCrypto = require("node:crypto");
|
|
|
32
32
|
var pki = require("./vendor/pki.cjs");
|
|
33
33
|
|
|
34
34
|
var C = require("./constants");
|
|
35
|
+
var crypto = require("./crypto");
|
|
35
36
|
var { FrameworkError } = require("./framework-error");
|
|
36
37
|
|
|
37
38
|
var x509 = pki.x509;
|
|
@@ -108,7 +109,7 @@ async function generateCa(opts) {
|
|
|
108
109
|
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
109
110
|
var now = new Date();
|
|
110
111
|
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
111
|
-
serialNumber:
|
|
112
|
+
serialNumber: crypto.generateToken(16),
|
|
112
113
|
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
113
114
|
notBefore: now,
|
|
114
115
|
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
@@ -143,7 +144,7 @@ async function signClientCert(opts) {
|
|
|
143
144
|
var now = new Date();
|
|
144
145
|
var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
|
|
145
146
|
var clientCert = await x509.X509CertificateGenerator.create({
|
|
146
|
-
serialNumber:
|
|
147
|
+
serialNumber: crypto.generateToken(16),
|
|
147
148
|
subject: "CN=" + cn,
|
|
148
149
|
issuer: caCert.subject,
|
|
149
150
|
notBefore: now,
|
package/lib/nonce-store.js
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
var clusterStorage = require("./cluster-storage");
|
|
45
45
|
var C = require("./constants");
|
|
46
|
+
var safeAsync = require("./safe-async");
|
|
46
47
|
var { defineClass } = require("./framework-error");
|
|
47
48
|
|
|
48
49
|
var NonceStoreError = defineClass("NonceStoreError");
|
|
@@ -59,13 +60,12 @@ function _memoryBackend(opts) {
|
|
|
59
60
|
var sweepIntervalMs = opts.sweepIntervalMs || DEFAULT_SWEEP_INTERVAL_MS;
|
|
60
61
|
var seen = new Map(); // nonce -> expireAt
|
|
61
62
|
|
|
62
|
-
var sweepTimer =
|
|
63
|
+
var sweepTimer = safeAsync.repeating(function () {
|
|
63
64
|
var now = Date.now();
|
|
64
65
|
for (var entry of seen) {
|
|
65
66
|
if (entry[1] <= now) seen.delete(entry[0]);
|
|
66
67
|
}
|
|
67
|
-
}, sweepIntervalMs);
|
|
68
|
-
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
68
|
+
}, sweepIntervalMs, { name: "nonce-sweep" });
|
|
69
69
|
|
|
70
70
|
function checkAndInsert(nonce, expireAt) {
|
|
71
71
|
if (typeof nonce !== "string" || nonce.length === 0) {
|
|
@@ -92,7 +92,7 @@ function _memoryBackend(opts) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function close() {
|
|
95
|
-
if (sweepTimer) {
|
|
95
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
96
96
|
seen.clear();
|
|
97
97
|
}
|
|
98
98
|
|
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/otel-export.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* hand-rolled wire format with no compelling benefit at this scope.
|
|
41
41
|
*/
|
|
42
42
|
var C = require("./constants");
|
|
43
|
+
var safeAsync = require("./safe-async");
|
|
43
44
|
var validateOpts = require("./validate-opts");
|
|
44
45
|
var { defineClass } = require("./framework-error");
|
|
45
46
|
|
|
@@ -117,7 +118,7 @@ function create(opts) {
|
|
|
117
118
|
var counters = new Map(); // bucketKey → { name, attrs, value, startUnixNano }
|
|
118
119
|
var observations = new Map(); // bucketKey → { name, attrs, sum, count, min, max, startUnixNano }
|
|
119
120
|
var startUnixNano = String(Date.now() * 1e6);
|
|
120
|
-
var
|
|
121
|
+
var loop = null;
|
|
121
122
|
var closed = false;
|
|
122
123
|
|
|
123
124
|
function recordCounter(name, value, attrs) {
|
|
@@ -235,20 +236,14 @@ function create(opts) {
|
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
timer = setTimeout(function () {
|
|
241
|
-
flush().catch(function (_e) { /* never let a flush error crash the timer */ });
|
|
242
|
-
_scheduleFlush();
|
|
243
|
-
}, intervalMs);
|
|
244
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
239
|
+
if (intervalMs > 0) {
|
|
240
|
+
loop = safeAsync.flushLoop(flush, intervalMs, { name: "otel-flush" });
|
|
245
241
|
}
|
|
246
|
-
_scheduleFlush();
|
|
247
242
|
|
|
248
243
|
function close() {
|
|
249
244
|
if (closed) return;
|
|
250
245
|
closed = true;
|
|
251
|
-
if (
|
|
246
|
+
if (loop) { loop.stop(); loop = null; }
|
|
252
247
|
return flush().catch(function (_e) { /* close path swallows final-flush errors */ });
|
|
253
248
|
}
|
|
254
249
|
|
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 (
|
|
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
|
}
|
package/lib/parsers/index.js
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
* xml — RFC-compliant subset; XXE / DOCTYPE / billion-laughs blocked
|
|
8
8
|
* by default; depth + element + attribute count limits;
|
|
9
9
|
* numeric-character-ref bounds checked
|
|
10
|
-
* csv — RFC 4180 parsing + writer with formula-injection prevention
|
|
11
|
-
* (cells starting with =/+/-/@/tab/CR get a single-quote
|
|
12
|
-
* prefix on stringify so Excel doesn't execute them)
|
|
13
10
|
* toml — TOML 1.0 parsing with depth + size limits;
|
|
14
11
|
* prototype-pollution rejection on dotted-key path segments;
|
|
15
12
|
* strict same-key redefinition (silent overwrite would mask
|
|
@@ -49,15 +46,14 @@
|
|
|
49
46
|
*
|
|
50
47
|
* Public API:
|
|
51
48
|
* parsers.xml.parse(input, opts?) → object
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* (CSV moved to top-level `b.csv` in v0.5.17 — same surface unified.)
|
|
54
51
|
*
|
|
55
52
|
* Error types: each parser exports its own *SafeError class with .code
|
|
56
|
-
* matching the format (xml/...,
|
|
53
|
+
* matching the format (xml/..., toml/...).
|
|
57
54
|
*/
|
|
58
55
|
module.exports = {
|
|
59
56
|
xml: require("./safe-xml"),
|
|
60
|
-
csv: require("./safe-csv"),
|
|
61
57
|
toml: require("./safe-toml"),
|
|
62
58
|
yaml: require("./safe-yaml"),
|
|
63
59
|
env: require("./safe-env"),
|
package/lib/parsers/safe-env.js
CHANGED
|
@@ -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
|
}
|
package/lib/queue-local.js
CHANGED
|
@@ -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
|
|
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,8 +31,9 @@
|
|
|
31
31
|
* failed (status='failed')
|
|
32
32
|
*/
|
|
33
33
|
var C = require("./constants");
|
|
34
|
+
var clusterStorage = require("./cluster-storage");
|
|
35
|
+
var crypto = require("./crypto");
|
|
34
36
|
var lazyRequire = require("./lazy-require");
|
|
35
|
-
var nodeCrypto = require("node:crypto");
|
|
36
37
|
var observability = require("./observability");
|
|
37
38
|
var protocolDispatcher = require("./protocol-dispatcher");
|
|
38
39
|
var localProto = require("./queue-local");
|
|
@@ -125,14 +126,13 @@ function init(opts) {
|
|
|
125
126
|
|
|
126
127
|
// Sweep expired leases periodically (every 30s) so crashed-handler jobs
|
|
127
128
|
// get re-pended.
|
|
128
|
-
sweepTimer =
|
|
129
|
+
sweepTimer = safeAsync.repeating(function () {
|
|
129
130
|
Object.keys(backends).forEach(function (n) {
|
|
130
131
|
if (backends[n].sweepExpired) {
|
|
131
132
|
backends[n].sweepExpired().catch(function () { /* best effort */ });
|
|
132
133
|
}
|
|
133
134
|
});
|
|
134
|
-
},
|
|
135
|
-
sweepTimer.unref();
|
|
135
|
+
}, C.TIME.seconds(30), { name: "queue-sweep" });
|
|
136
136
|
|
|
137
137
|
initialized = true;
|
|
138
138
|
}
|
|
@@ -197,7 +197,7 @@ function consume(queueName, handler, opts) {
|
|
|
197
197
|
}
|
|
198
198
|
rateLimit = {
|
|
199
199
|
max: opts.rateLimit.max,
|
|
200
|
-
windowMs: opts.rateLimit.perSeconds
|
|
200
|
+
windowMs: C.TIME.seconds(opts.rateLimit.perSeconds),
|
|
201
201
|
timestamps: [],
|
|
202
202
|
};
|
|
203
203
|
}
|
|
@@ -466,7 +466,7 @@ async function shutdown(opts) {
|
|
|
466
466
|
await safeAsync.sleep(50);
|
|
467
467
|
}
|
|
468
468
|
consumers = [];
|
|
469
|
-
if (sweepTimer) {
|
|
469
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
function listBackends() {
|
|
@@ -485,7 +485,7 @@ function _requireInit() {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
function _resetForTest() {
|
|
488
|
-
if (sweepTimer) {
|
|
488
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
489
489
|
consumers.forEach(function (c) { c.cancel(); });
|
|
490
490
|
consumers = [];
|
|
491
491
|
backends = {};
|
|
@@ -571,7 +571,7 @@ function enqueueFlow(spec) {
|
|
|
571
571
|
return Promise.reject(e);
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
var flowId = "flow-" +
|
|
574
|
+
var flowId = "flow-" + crypto.generateToken(8);
|
|
575
575
|
|
|
576
576
|
return observability.tap("queue.enqueueFlow",
|
|
577
577
|
{ queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
|
|
@@ -603,7 +603,6 @@ function enqueueFlow(spec) {
|
|
|
603
603
|
}
|
|
604
604
|
// Second pass: write dependsOn (translated to jobIds) for children
|
|
605
605
|
// that need it, and parking-lot their availableAt to MAX_SAFE_INTEGER.
|
|
606
|
-
var clusterStorage = require("./cluster-storage");
|
|
607
606
|
for (var q = 0; q < jobs.length; q++) {
|
|
608
607
|
var j = jobs[q];
|
|
609
608
|
if (j.dependsOn.length === 0) continue;
|
package/lib/request-helpers.js
CHANGED
|
@@ -136,7 +136,7 @@ function clientIp(req, opts) {
|
|
|
136
136
|
if (trust && req.headers) {
|
|
137
137
|
var xff = req.headers["x-forwarded-for"];
|
|
138
138
|
if (xff) {
|
|
139
|
-
var hops =
|
|
139
|
+
var hops = parseListHeader(xff);
|
|
140
140
|
if (trust === true) return hops[0];
|
|
141
141
|
if (typeof trust === "number" && trust >= 1 && hops.length >= trust) {
|
|
142
142
|
return hops[hops.length - trust];
|
|
@@ -154,7 +154,8 @@ function requestProtocol(req, opts) {
|
|
|
154
154
|
if (trust && req.headers) {
|
|
155
155
|
var fwd = req.headers["x-forwarded-proto"];
|
|
156
156
|
if (typeof fwd === "string" && fwd.length > 0) {
|
|
157
|
-
|
|
157
|
+
var hops = parseListHeader(fwd, { lowercase: true });
|
|
158
|
+
if (hops.length > 0) return hops[0];
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
if (req.socket && req.socket.encrypted) return "https";
|
|
@@ -162,6 +163,36 @@ function requestProtocol(req, opts) {
|
|
|
162
163
|
return "http";
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// parseListHeader — split a comma-separated header / opt value into a
|
|
167
|
+
// list of trimmed non-empty tokens. Replaces the
|
|
168
|
+
// `String(x).split(",").map(s => s.trim()).filter(Boolean)` chain that
|
|
169
|
+
// was duplicated across cors / compression / scheduler / webhook /
|
|
170
|
+
// websocket / db-schema / cli before v0.5.17.
|
|
171
|
+
//
|
|
172
|
+
// parseListHeader("a, b , ,c") → ["a", "b", "c"]
|
|
173
|
+
// parseListHeader("Foo, Bar", { lowercase: true })
|
|
174
|
+
// → ["foo", "bar"]
|
|
175
|
+
// parseListHeader(undefined) → []
|
|
176
|
+
// parseListHeader("") → []
|
|
177
|
+
//
|
|
178
|
+
// Tier-C: tolerant of non-string input (returns []) — these are read
|
|
179
|
+
// from request headers that the network might omit. Callers needing
|
|
180
|
+
// stricter checks layer their own validation on the result.
|
|
181
|
+
function parseListHeader(value, opts) {
|
|
182
|
+
if (value == null) return [];
|
|
183
|
+
opts = opts || {};
|
|
184
|
+
var s = typeof value === "string" ? value : String(value);
|
|
185
|
+
if (s.length === 0) return [];
|
|
186
|
+
var parts = s.split(",");
|
|
187
|
+
var out = [];
|
|
188
|
+
for (var i = 0; i < parts.length; i++) {
|
|
189
|
+
var t = parts[i].trim();
|
|
190
|
+
if (t.length === 0) continue;
|
|
191
|
+
out.push(opts.lowercase ? t.toLowerCase() : t);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
165
196
|
// Append a token to a `Vary` response header without dropping prior
|
|
166
197
|
// values (compression middleware sets `Vary: Accept-Encoding`, an
|
|
167
198
|
// auth helper might set `Vary: Authorization`, etc.). Idempotent —
|
|
@@ -170,7 +201,7 @@ function appendVary(res, value) {
|
|
|
170
201
|
if (!res || typeof res.getHeader !== "function" || typeof res.setHeader !== "function") return;
|
|
171
202
|
var existing = res.getHeader("Vary");
|
|
172
203
|
if (existing == null || existing === "") { res.setHeader("Vary", value); return; }
|
|
173
|
-
var tokens =
|
|
204
|
+
var tokens = parseListHeader(existing);
|
|
174
205
|
var lower = value.toLowerCase();
|
|
175
206
|
for (var i = 0; i < tokens.length; i++) if (tokens[i].toLowerCase() === lower) return;
|
|
176
207
|
tokens.push(value);
|
|
@@ -266,6 +297,7 @@ module.exports = {
|
|
|
266
297
|
extractActorContext: extractActorContext,
|
|
267
298
|
resolveActorWithOverride: resolveActorWithOverride,
|
|
268
299
|
parseQualityList: parseQualityList,
|
|
300
|
+
parseListHeader: parseListHeader,
|
|
269
301
|
// v0.5.3 — proxy-trust primitives (default refuses forwarded headers)
|
|
270
302
|
clientIp: clientIp,
|
|
271
303
|
requestProtocol: requestProtocol,
|
package/lib/restore.js
CHANGED
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
var fs = require("fs");
|
|
55
|
-
var nodeCrypto = require("node:crypto");
|
|
56
55
|
var os = require("os");
|
|
57
56
|
var path = require("path");
|
|
57
|
+
var crypto = require("./crypto");
|
|
58
58
|
var restoreBundle = require("./restore-bundle");
|
|
59
59
|
var restoreRollback = require("./restore-rollback");
|
|
60
60
|
var lazyRequire = require("./lazy-require");
|
|
@@ -128,7 +128,7 @@ function create(opts) {
|
|
|
128
128
|
"inspect: bundle '" + bundleId + "' not in storage");
|
|
129
129
|
}
|
|
130
130
|
var pullDir = path.join(os.tmpdir(),
|
|
131
|
-
"blamejs-restore-inspect-" +
|
|
131
|
+
"blamejs-restore-inspect-" + crypto.generateToken(4));
|
|
132
132
|
try {
|
|
133
133
|
await storage.readBundle(bundleId, pullDir);
|
|
134
134
|
return restoreBundle.inspect({ bundleDir: pullDir });
|
|
@@ -150,7 +150,7 @@ function create(opts) {
|
|
|
150
150
|
"run: bundle '" + bundleId + "' not in storage");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
var pullId =
|
|
153
|
+
var pullId = crypto.generateToken(4);
|
|
154
154
|
var pullDir = path.join(os.tmpdir(), "blamejs-restore-pull-" + pullId);
|
|
155
155
|
var stagingDir = path.join(os.tmpdir(), "blamejs-restore-staging-" + pullId);
|
|
156
156
|
|
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 =
|
|
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
|
-
(
|
|
265
|
-
|
|
266
|
+
(globalValidateMode === "throw" ||
|
|
267
|
+
globalValidateMode === "warn" ||
|
|
266
268
|
split.spec.validateResponse)) {
|
|
267
269
|
handlers = [_makeResponseValidator(split.spec)].concat(handlers);
|
|
268
270
|
}
|
package/lib/safe-async.js
CHANGED
|
@@ -494,6 +494,126 @@ class Once {
|
|
|
494
494
|
hasInvoked() { return this._promise !== null; }
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
// ---- repeating ----
|
|
498
|
+
//
|
|
499
|
+
// Bounded-cadence interval timer with consistent unref + cancel semantics.
|
|
500
|
+
// Replaces the scattered setInterval ceremony where each caller hand-rolled
|
|
501
|
+
// `var t = setInterval(...); t.unref();` and a corresponding clearInterval
|
|
502
|
+
// in shutdown — easy to forget the unref and silently block process exit.
|
|
503
|
+
//
|
|
504
|
+
// var sweep = b.safeAsync.repeating(function () {
|
|
505
|
+
// return cleanup();
|
|
506
|
+
// }, b.constants.TIME.seconds(30), { name: "cache-sweep" });
|
|
507
|
+
// ...
|
|
508
|
+
// sweep.stop();
|
|
509
|
+
//
|
|
510
|
+
// fn may be sync or async. If fn returns a Promise, the next tick fires
|
|
511
|
+
// `intervalMs` after the prior fn() *started* (matching setInterval's
|
|
512
|
+
// fixed-rate semantics, not after-completion). Promise rejections are
|
|
513
|
+
// captured by the optional onError callback; if none provided, they're
|
|
514
|
+
// silently dropped — a repeating timer is by definition fire-and-forget,
|
|
515
|
+
// and an unhandled rejection here would crash the process.
|
|
516
|
+
//
|
|
517
|
+
// opts.unref defaults true: most repeating timers are background sweepers
|
|
518
|
+
// that should NOT keep the process alive. Cluster heartbeat etc. set
|
|
519
|
+
// `unref: false` so the lease keeps the leader from exiting silently.
|
|
520
|
+
|
|
521
|
+
function repeating(fn, intervalMs, opts) {
|
|
522
|
+
if (typeof fn !== "function") {
|
|
523
|
+
throw new SafeAsyncError("repeating: fn must be a function", "async/bad-arg");
|
|
524
|
+
}
|
|
525
|
+
if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
526
|
+
throw new SafeAsyncError("repeating: intervalMs must be a positive finite number, got " + intervalMs,
|
|
527
|
+
"async/bad-arg");
|
|
528
|
+
}
|
|
529
|
+
opts = opts || {};
|
|
530
|
+
var unref = opts.unref !== false; // default true
|
|
531
|
+
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
532
|
+
|
|
533
|
+
var stopped = false;
|
|
534
|
+
var timer = setInterval(function () {
|
|
535
|
+
if (stopped) return;
|
|
536
|
+
var result;
|
|
537
|
+
try { result = fn(); }
|
|
538
|
+
catch (e) { if (onError) { try { onError(e); } catch (_e) { /* swallow */ } } return; }
|
|
539
|
+
if (result && typeof result.then === "function") {
|
|
540
|
+
result.then(null, function (e) {
|
|
541
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}, intervalMs);
|
|
545
|
+
if (unref && typeof timer.unref === "function") timer.unref();
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
stop: function () {
|
|
549
|
+
if (stopped) return;
|
|
550
|
+
stopped = true;
|
|
551
|
+
clearInterval(timer);
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---- flushLoop ----
|
|
557
|
+
//
|
|
558
|
+
// Schedule fn(), wait for it to settle (resolve or reject), then schedule
|
|
559
|
+
// the next fn() `intervalMs` later. Differs from `repeating` (fixed-rate,
|
|
560
|
+
// fire-and-forget) — flushLoop is the after-completion pattern most
|
|
561
|
+
// background flushers want: never overlap two flushes, and don't accumulate
|
|
562
|
+
// backlog if a flush is slow.
|
|
563
|
+
//
|
|
564
|
+
// var loop = b.safeAsync.flushLoop(function () {
|
|
565
|
+
// return otelExporter.flush();
|
|
566
|
+
// }, b.constants.TIME.seconds(15), { name: "otel-flush" });
|
|
567
|
+
// ...
|
|
568
|
+
// loop.stop();
|
|
569
|
+
//
|
|
570
|
+
// Always unref'd — a pending flush should never keep the process alive
|
|
571
|
+
// (the operator's b.appShutdown drives the final drain explicitly).
|
|
572
|
+
// onError catches rejections; without one, they're silently dropped.
|
|
573
|
+
|
|
574
|
+
function flushLoop(fn, intervalMs, opts) {
|
|
575
|
+
if (typeof fn !== "function") {
|
|
576
|
+
throw new SafeAsyncError("flushLoop: fn must be a function", "async/bad-arg");
|
|
577
|
+
}
|
|
578
|
+
if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
579
|
+
throw new SafeAsyncError("flushLoop: intervalMs must be a positive finite number, got " + intervalMs,
|
|
580
|
+
"async/bad-arg");
|
|
581
|
+
}
|
|
582
|
+
opts = opts || {};
|
|
583
|
+
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
584
|
+
|
|
585
|
+
var stopped = false;
|
|
586
|
+
var timer = null;
|
|
587
|
+
|
|
588
|
+
function _schedule() {
|
|
589
|
+
if (stopped) return;
|
|
590
|
+
timer = setTimeout(function () {
|
|
591
|
+
timer = null;
|
|
592
|
+
if (stopped) return;
|
|
593
|
+
var settled;
|
|
594
|
+
try { settled = Promise.resolve(fn()); }
|
|
595
|
+
catch (e) {
|
|
596
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
597
|
+
_schedule();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
settled.then(null, function (e) {
|
|
601
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
602
|
+
}).then(_schedule);
|
|
603
|
+
}, intervalMs);
|
|
604
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
605
|
+
}
|
|
606
|
+
_schedule();
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
stop: function () {
|
|
610
|
+
if (stopped) return;
|
|
611
|
+
stopped = true;
|
|
612
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
497
617
|
// ---- Re-exports of resilience primitives from lib/retry.js ----
|
|
498
618
|
//
|
|
499
619
|
// withRetry + CircuitBreaker live in lib/retry.js (the canonical home).
|
|
@@ -510,6 +630,8 @@ module.exports = {
|
|
|
510
630
|
withSignal: withSignal,
|
|
511
631
|
withTimeoutSignal: withTimeoutSignal,
|
|
512
632
|
sleep: sleep,
|
|
633
|
+
repeating: repeating,
|
|
634
|
+
flushLoop: flushLoop,
|
|
513
635
|
safeAwait: safeAwait,
|
|
514
636
|
Mutex: Mutex,
|
|
515
637
|
Semaphore: Semaphore,
|