@blamejs/core 0.5.16 → 0.5.17

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/lib/forms.js CHANGED
@@ -35,7 +35,7 @@
35
35
  * forms.escapeAttribute(value) → string (double-quoted-attr context)
36
36
  * forms.escapeHtml = template.escapeHtml (re-export for convenience)
37
37
  */
38
- var nodeCrypto = require("crypto");
38
+ var crypto = require("./crypto");
39
39
  var safeSchema = require("./safe-schema");
40
40
  var safeUrl = require("./safe-url");
41
41
  var template = require("./template");
@@ -47,16 +47,13 @@ var template = require("./template");
47
47
  var CSRF_TOKEN_BYTES = 32;
48
48
 
49
49
  function generateCsrfToken() {
50
- return nodeCrypto.randomBytes(CSRF_TOKEN_BYTES).toString("hex");
50
+ return crypto.generateToken(CSRF_TOKEN_BYTES);
51
51
  }
52
52
 
53
53
  function verifyCsrfToken(submitted, expected) {
54
54
  if (typeof submitted !== "string" || typeof expected !== "string") return false;
55
55
  if (submitted.length === 0 || submitted.length !== expected.length) return false;
56
- var a = Buffer.from(submitted, "utf8");
57
- var b = Buffer.from(expected, "utf8");
58
- if (a.length !== b.length) return false;
59
- return nodeCrypto.timingSafeEqual(a, b);
56
+ return crypto.timingSafeEqual(submitted, expected);
60
57
  }
61
58
 
62
59
  // ============================================================
@@ -64,10 +64,10 @@
64
64
  var http = require("http");
65
65
  var https = require("https");
66
66
  var http2 = require("http2");
67
- var nodeCrypto = require("node:crypto");
68
67
  var nodeStream = require("node:stream");
69
68
  var { URL } = require("url");
70
69
  var C = require("./constants");
70
+ var crypto = require("./crypto");
71
71
  var pqcAgent = require("./pqc-agent");
72
72
  var safeAsync = require("./safe-async");
73
73
  var safeBuffer = require("./safe-buffer");
@@ -356,7 +356,7 @@ function _attachJarCookie(headers, jar, url) {
356
356
  // parser accepts so round-trip from one blamejs app's outbound to
357
357
  // another's inbound is exact.
358
358
  function _buildMultipartBody(spec) {
359
- var boundary = "----blamejs-mp-" + nodeCrypto.randomBytes(16).toString("hex");
359
+ var boundary = "----blamejs-mp-" + crypto.generateToken(16);
360
360
  var CRLF = "\r\n";
361
361
  var parts = [];
362
362
 
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() + "-" + nodeCrypto.randomBytes(8).toString("hex");
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
@@ -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 = nodeCrypto.randomBytes(16).toString("hex");
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 = String(existing).split(",").map(function (p) { return p.trim(); });
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() === token.toLowerCase()) return String(existing);
215
+ if (parts[i].toLowerCase() === lcToken) return String(existing);
215
216
  }
216
217
  parts.push(token);
217
218
  return parts.join(", ");
@@ -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 = String(requestedHdrs).toLowerCase().split(",").map(function (s) { return s.trim(); }).filter(Boolean);
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 = setInterval(function () {
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 { clearInterval(gcInterval); } catch (_e) {}
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: nodeCrypto.randomBytes(16).toString("hex"),
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: nodeCrypto.randomBytes(16).toString("hex"),
147
+ serialNumber: crypto.generateToken(16),
147
148
  subject: "CN=" + cn,
148
149
  issuer: caCert.subject,
149
150
  notBefore: now,
@@ -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 = setInterval(function () {
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) { clearInterval(sweepTimer); sweepTimer = null; }
95
+ if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
96
96
  seen.clear();
97
97
  }
98
98
 
@@ -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 timer = null;
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
- function _scheduleFlush() {
239
- if (intervalMs === 0 || closed) return;
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 (timer) { clearTimeout(timer); timer = null; }
246
+ if (loop) { loop.stop(); loop = null; }
252
247
  return flush().catch(function (_e) { /* close path swallows final-flush errors */ });
253
248
  }
254
249
 
@@ -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
- * parsers.csv.parse(input, opts?) → array
53
- * parsers.csv.stringify(rows, opts?) → string
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/..., csv/...).
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/queue.js CHANGED
@@ -31,8 +31,8 @@
31
31
  * failed (status='failed')
32
32
  */
33
33
  var C = require("./constants");
34
+ var crypto = require("./crypto");
34
35
  var lazyRequire = require("./lazy-require");
35
- var nodeCrypto = require("node:crypto");
36
36
  var observability = require("./observability");
37
37
  var protocolDispatcher = require("./protocol-dispatcher");
38
38
  var localProto = require("./queue-local");
@@ -125,14 +125,13 @@ function init(opts) {
125
125
 
126
126
  // Sweep expired leases periodically (every 30s) so crashed-handler jobs
127
127
  // get re-pended.
128
- sweepTimer = setInterval(function () {
128
+ sweepTimer = safeAsync.repeating(function () {
129
129
  Object.keys(backends).forEach(function (n) {
130
130
  if (backends[n].sweepExpired) {
131
131
  backends[n].sweepExpired().catch(function () { /* best effort */ });
132
132
  }
133
133
  });
134
- }, 30000);
135
- sweepTimer.unref();
134
+ }, C.TIME.seconds(30), { name: "queue-sweep" });
136
135
 
137
136
  initialized = true;
138
137
  }
@@ -466,7 +465,7 @@ async function shutdown(opts) {
466
465
  await safeAsync.sleep(50);
467
466
  }
468
467
  consumers = [];
469
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
468
+ if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
470
469
  }
471
470
 
472
471
  function listBackends() {
@@ -485,7 +484,7 @@ function _requireInit() {
485
484
  }
486
485
 
487
486
  function _resetForTest() {
488
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
487
+ if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
489
488
  consumers.forEach(function (c) { c.cancel(); });
490
489
  consumers = [];
491
490
  backends = {};
@@ -571,7 +570,7 @@ function enqueueFlow(spec) {
571
570
  return Promise.reject(e);
572
571
  }
573
572
 
574
- var flowId = "flow-" + nodeCrypto.randomBytes(8).toString("hex");
573
+ var flowId = "flow-" + crypto.generateToken(8);
575
574
 
576
575
  return observability.tap("queue.enqueueFlow",
577
576
  { queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
@@ -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 = String(xff).split(",").map(function (s) { return s.trim(); });
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
- return String(fwd).split(",")[0].trim().toLowerCase();
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 = String(existing).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
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-" + nodeCrypto.randomBytes(4).toString("hex"));
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 = nodeCrypto.randomBytes(4).toString("hex");
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/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,
package/lib/tracing.js CHANGED
@@ -81,7 +81,7 @@
81
81
  * async-context, which is fine for the surfaces we instrument.
82
82
  */
83
83
 
84
- var nodeCrypto = require("node:crypto");
84
+ var crypto = require("./crypto");
85
85
  var validateOpts = require("./validate-opts");
86
86
  var { defineClass } = require("./framework-error");
87
87
  var { resolveRoute, captureResponseStatus } = require("./request-helpers");
@@ -134,10 +134,10 @@ function _formatTraceparent(traceId, spanId, flags) {
134
134
  }
135
135
 
136
136
  function _newTraceId() {
137
- return nodeCrypto.randomBytes(16).toString("hex");
137
+ return crypto.generateToken(16); // 32 hex chars (W3C trace-context)
138
138
  }
139
139
  function _newSpanId() {
140
- return nodeCrypto.randomBytes(8).toString("hex");
140
+ return crypto.generateToken(8); // 16 hex chars (W3C trace-context)
141
141
  }
142
142
 
143
143
  // ---- Pass-through span (used when OTel isn't installed) ----
package/lib/webhook.js CHANGED
@@ -225,10 +225,10 @@ function _pqcVerify(publicKeyPem, data, expectedHex) {
225
225
  // a kid → signature pair. Whitespace tolerated around commas.
226
226
 
227
227
  function _parseSignatureHeader(headerValue) {
228
- var parts = headerValue.split(",");
228
+ var segs = requestHelpers.parseListHeader(headerValue);
229
229
  var t = null, id = null, sigs = {};
230
- for (var i = 0; i < parts.length; i++) {
231
- var seg = parts[i].trim();
230
+ for (var i = 0; i < segs.length; i++) {
231
+ var seg = segs[i];
232
232
  var eq = seg.indexOf("=");
233
233
  if (eq <= 0) continue; // skip malformed segments rather than failing whole header
234
234
  var name = seg.slice(0, eq);
package/lib/websocket.js CHANGED
@@ -84,6 +84,8 @@
84
84
  var nodeCrypto = require("crypto");
85
85
  var { EventEmitter } = require("events");
86
86
  var C = require("./constants");
87
+ var requestHelpers = require("./request-helpers");
88
+ var safeAsync = require("./safe-async");
87
89
  var safeBuffer = require("./safe-buffer");
88
90
  var { FrameworkError } = require("./framework-error");
89
91
  var { boot } = require("./log");
@@ -192,7 +194,7 @@ function validateUpgradeRequest(req) {
192
194
  function negotiateSubprotocol(req, supported) {
193
195
  if (!supported || supported.length === 0) return null;
194
196
  var raw = (req.headers || {})["sec-websocket-protocol"] || "";
195
- var offered = raw.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
197
+ var offered = requestHelpers.parseListHeader(raw);
196
198
  for (var i = 0; i < offered.length; i++) {
197
199
  if (supported.indexOf(offered[i]) !== -1) return offered[i];
198
200
  }
@@ -417,8 +419,8 @@ class WebSocketConnection extends EventEmitter {
417
419
  this._lastPongAt = Date.now();
418
420
 
419
421
  var self = this;
420
- this._pingTimer = setInterval(function () { self._heartbeat(pongMs); }, pingMs);
421
- this._pingTimer.unref();
422
+ this._pingTimer = safeAsync.repeating(function () { self._heartbeat(pongMs); },
423
+ pingMs, { name: "websocket-ping" });
422
424
 
423
425
  socket.on("data", function (chunk) { self._onData(chunk); });
424
426
  socket.on("error", function (err) {
@@ -446,7 +448,7 @@ class WebSocketConnection extends EventEmitter {
446
448
  if (this._state === STATE_CLOSED) return;
447
449
  this._state = STATE_CLOSED;
448
450
  if (error) this.lastError = error;
449
- if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
451
+ if (this._pingTimer) { this._pingTimer.stop(); this._pingTimer = null; }
450
452
  if (this._closeTimer) { clearTimeout(this._closeTimer); this._closeTimer = null; }
451
453
  // Surface diagnosable errors via 'error' first — but only if the
452
454
  // operator is listening AND this is a real diagnosable case.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",