@blamejs/core 0.17.11 → 0.17.13

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/watcher.js CHANGED
@@ -74,6 +74,14 @@ var DEFAULT_POLL_MAX_FILES = 50000;
74
74
  // Operators with legitimate high-churn directories raise this via opts.
75
75
  var DEFAULT_MAX_PENDING = 10000; // pending-event queue cap
76
76
 
77
+ // Native fs.watch runtime-error codes that leave the underlying handle dead —
78
+ // change detection is permanently off, not a transient blip. Classified as a
79
+ // fatal watcher/handle-dead so a wrapper can recreate/alert rather than ignore.
80
+ var HANDLE_DEAD_CODES = ["EPERM", "EBADF", "ENOSPC", "EMFILE", "ENFILE", "EACCES"];
81
+ // Windows MAX_PATH is 260; a path approaching it needs the \\?\ long-path
82
+ // prefix for lstat to resolve it (below the threshold, plain paths are fine).
83
+ var WIN32_LONG_PATH_THRESHOLD = 250;
84
+
77
85
  // ---- glob-style matcher ----
78
86
  //
79
87
  // Supports three shapes per entry:
@@ -133,7 +141,7 @@ function _matchGlobBasename(parts, base) {
133
141
  return true;
134
142
  }
135
143
 
136
- function _compileIgnore(patterns) {
144
+ function _compileIgnore(patterns, caseFold) {
137
145
  if (!Array.isArray(patterns) || patterns.length === 0) {
138
146
  return function () { return false; };
139
147
  }
@@ -154,26 +162,33 @@ function _compileIgnore(patterns) {
154
162
  throw new WatcherError("watcher/bad-ignore",
155
163
  "watcher.create: ignore[" + i + "] exceeds " + MAX_IGNORE_STAR_COUNT + "-wildcard cap");
156
164
  }
157
- if (p.indexOf("**") !== -1) {
165
+ // When ignoreCaseFold is on, fold the pattern to lower case at compile time
166
+ // and the walked path at match time, so the walk-prune aligns with a
167
+ // case-insensitive consumer's own filter (an on-disk 'Node_Modules' is
168
+ // pruned by a 'node_modules/**' ignore, not stat'd toward pollMaxFiles).
169
+ // Length + wildcard caps stay measured on the original pattern.
170
+ var pk = caseFold ? p.toLowerCase() : p;
171
+ if (pk.indexOf("**") !== -1) {
158
172
  // dir/** prefix-match — strip the trailing **; reject `**` mid-pattern.
159
- if (!/^[^*]*\/?\*\*$/.test(p)) {
173
+ if (!/^[^*]*\/?\*\*$/.test(pk)) {
160
174
  throw new WatcherError("watcher/bad-ignore",
161
175
  "watcher.create: ignore[" + i + "] '**' is only supported as a trailing dir/** prefix form");
162
176
  }
163
- var prefix = p.replace(/\/?\*\*$/, "");
177
+ var prefix = pk.replace(/\/?\*\*$/, "");
164
178
  compiled.push({ kind: "prefix", value: prefix });
165
179
  } else if (starCount > 0) {
166
- compiled.push({ kind: "glob", value: _parseGlobBasename(p) });
180
+ compiled.push({ kind: "glob", value: _parseGlobBasename(pk) });
167
181
  } else {
168
- compiled.push({ kind: "exact", value: p });
182
+ compiled.push({ kind: "exact", value: pk });
169
183
  }
170
184
  }
171
185
  return function (relPath) {
172
- var base = nodePath.basename(relPath);
173
- var normalized = relPath.split(nodePath.sep).join("/");
186
+ var matchPath = caseFold ? relPath.toLowerCase() : relPath;
187
+ var base = nodePath.basename(matchPath);
188
+ var normalized = matchPath.split(nodePath.sep).join("/");
174
189
  for (var j = 0; j < compiled.length; j += 1) {
175
190
  var c = compiled[j];
176
- if (c.kind === "exact" && (c.value === relPath || c.value === normalized)) return true;
191
+ if (c.kind === "exact" && (c.value === matchPath || c.value === normalized)) return true;
177
192
  if (c.kind === "prefix" && (normalized === c.value || normalized.indexOf(c.value + "/") === 0)) return true;
178
193
  if (c.kind === "glob" && _matchGlobBasename(c.value, base)) return true;
179
194
  }
@@ -312,6 +327,21 @@ function _validateOpts(opts) {
312
327
  throw new WatcherError("watcher/bad-ignore",
313
328
  "watcher.create: ignore must be an array of glob patterns");
314
329
  }
330
+ validateOpts.optionalBoolean(opts.ignoreCaseFold, "ignoreCaseFold", WatcherError, "watcher/bad-ignore");
331
+ }
332
+
333
+ function _lstatLongPathSafe(fullPath) {
334
+ // Windows MAX_PATH (260): lstatSync on a path at/over the limit without the
335
+ // \\?\ long-path prefix spuriously throws ENOENT, which _normalizeAndDispatch
336
+ // would misread as a delete. The prefix needs a backslashed absolute path
337
+ // (the watcher root is realpathSync.native'd → absolute) and disables path
338
+ // normalization, which is safe here — fullPath has no relative components. A
339
+ // genuinely-missing file still ENOENTs on the plain-path fallback → real delete.
340
+ if (process.platform === "win32" && fullPath.length >= WIN32_LONG_PATH_THRESHOLD && fullPath.indexOf("\\\\?\\") !== 0) {
341
+ try { return nodeFs.lstatSync("\\\\?\\" + fullPath); }
342
+ catch (_e) { /* fall through to the plain path so a real ENOENT resurfaces */ }
343
+ }
344
+ return nodeFs.lstatSync(fullPath);
315
345
  }
316
346
 
317
347
  function create(opts) {
@@ -339,7 +369,7 @@ function create(opts) {
339
369
  var onChange = opts.onChange || function () {};
340
370
  var onDelete = opts.onDelete || function () {};
341
371
  var onError = opts.onError || function () {};
342
- var isIgnored = _compileIgnore(opts.ignore);
372
+ var isIgnored = _compileIgnore(opts.ignore, opts.ignoreCaseFold === true);
343
373
  var auditOn = opts.audit !== false;
344
374
 
345
375
  // Pre-flight: root must exist and be a directory.
@@ -375,13 +405,33 @@ function create(opts) {
375
405
  try { onError(err); } catch (_e) { /* operator error handler must not crash the watcher */ }
376
406
  }
377
407
 
408
+ // Native-backend (fs.watch) error handler. A runtime error whose code leaves
409
+ // the handle dead (EPERM after the watched root is deleted/recreated on
410
+ // Windows, EBADF, inotify exhaustion) permanently stops detection — classify
411
+ // it fatal so a consumer can react (recreate/alert) rather than treat it as a
412
+ // transient blip. Non-handle-dead errors pass through unclassified.
413
+ function _handleBackendError(err) {
414
+ if (err && HANDLE_DEAD_CODES.indexOf(err.code) !== -1) {
415
+ var dead = new WatcherError("watcher/handle-dead",
416
+ "watcher: native watch handle died (" + err.code + ") — change detection has " +
417
+ "permanently stopped; recreate the watcher to resume: " + (err.message || String(err)));
418
+ dead.fatal = true;
419
+ dead.cause = err;
420
+ _safeError(dead);
421
+ return;
422
+ }
423
+ _safeError(err);
424
+ }
425
+
378
426
  function _normalizeAndDispatch(relPath) {
379
427
  if (stopped) return;
380
428
  if (isIgnored(relPath)) return;
381
429
  var fullPath = nodePath.join(root, relPath);
382
- // lstat (NOT stat) — refuses to follow symlinks out of root.
430
+ // lstat (NOT stat) — refuses to follow symlinks out of root. Long-path-safe
431
+ // so a Windows path over MAX_PATH isn't misread as a delete (issue: a deep
432
+ // change delivered to onDelete instead of onChange).
383
433
  var lst;
384
- try { lst = nodeFs.lstatSync(fullPath); }
434
+ try { lst = _lstatLongPathSafe(fullPath); }
385
435
  catch (e) {
386
436
  if (e && e.code === "ENOENT") {
387
437
  // Path is gone — delete event. Type unknown by the time we
@@ -456,10 +506,23 @@ function create(opts) {
456
506
  var absDir = relDir === "" ? root : nodePath.join(root, relDir);
457
507
  var entries;
458
508
  try { entries = nodeFs.readdirSync(absDir, { withFileTypes: true }); }
459
- catch (_e) {
460
- // Root vanished mid-walk OR an inner dir got deleted between
461
- // the parent listing and the descent. Skip the next tick's
462
- // walk surfaces the deletion via the snapshot diff.
509
+ catch (rootErr) {
510
+ if (relDir === "") {
511
+ // The ROOT itself is unreadable (unmounted NFS/SMB/USB volume, or a
512
+ // deleted root). Returning an empty snapshot would diff as a delete
513
+ // of EVERY tracked entry — a mass-delete a consumer can't tell apart
514
+ // from a real rm -rf. Surface a distinct fatal root-lost signal and
515
+ // abort the walk; _pollTick's catch keeps the prior snapshot, so no
516
+ // spurious onDelete fires.
517
+ var lost = new WatcherError("watcher/root-lost",
518
+ "watcher.poll: root '" + root + "' is no longer readable: " +
519
+ ((rootErr && rootErr.message) || String(rootErr)));
520
+ lost.fatal = true;
521
+ lost.cause = rootErr;
522
+ throw lost;
523
+ }
524
+ // Inner dir vanished mid-walk between the parent listing and the
525
+ // descent. Skip — the next tick's diff surfaces the deletion.
463
526
  continue;
464
527
  }
465
528
  for (var i = 0; i < entries.length; i += 1) {
@@ -544,7 +607,7 @@ function create(opts) {
544
607
  if (rel === "" || rel === ".") return;
545
608
  _enqueue(rel);
546
609
  });
547
- watcherHandle.on("error", function (err) { _safeError(err); });
610
+ watcherHandle.on("error", function (err) { _handleBackendError(err); });
548
611
  } catch (e) {
549
612
  if (e && (e.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM" || e.code === "ENOSYS")) {
550
613
  throw new WatcherError("watcher/recursive-unsupported",
@@ -609,10 +672,19 @@ function create(opts) {
609
672
  root: root,
610
673
  mode: mode,
611
674
  _flushForTest: _flushForTest,
675
+ // Test seam — drives the native-backend error classification path (the OS
676
+ // killing the handle isn't deterministically reproducible). Not part of the
677
+ // operator contract.
678
+ _simulateBackendErrorForTest: _handleBackendError,
612
679
  };
613
680
  }
614
681
 
615
682
  module.exports = {
616
683
  create: create,
617
684
  WatcherError: WatcherError,
685
+ // Exported so a consumer pre-filtering operator ignore patterns (to keep one
686
+ // bad pattern from aborting the whole watcher) can align with the caps
687
+ // create() enforces, instead of pinning hand-copied mirror constants.
688
+ MAX_IGNORE_PATTERN_LEN: MAX_IGNORE_PATTERN_LEN,
689
+ MAX_IGNORE_STAR_COUNT: MAX_IGNORE_STAR_COUNT,
618
690
  };
@@ -72,7 +72,7 @@ var HTTP_OK_MAX = 300;
72
72
  function _validateTableName(name, label) {
73
73
  validateOpts.requireNonEmptyString(name, label, WebhookDispatcherError, "webhook-dispatcher/bad-opts");
74
74
  // safeSql.quoteIdentifier refuses an injection-bearing name at construction.
75
- safeSql.quoteIdentifier(name);
75
+ safeSql.quoteIdentifier(name, undefined, { allowReserved: true }); // parity with b.db.from()
76
76
  return name;
77
77
  }
78
78
 
package/lib/ws-client.js CHANGED
@@ -268,6 +268,7 @@ function connect(target, opts) {
268
268
  // performs the (async) check + pinning before the dial and reruns it on
269
269
  // every reconnect, so a urlFor-swapped target is validated too.
270
270
  client._prepareDial().then(function () {
271
+ if (client._closed) return; // close() during the async SSRF re-resolve retires the dial
271
272
  client._dial();
272
273
  }).catch(function (e) {
273
274
  setImmediate(function () { client._handleSocketError(e); });
@@ -366,6 +367,10 @@ class WsClient extends EventEmitter {
366
367
  }
367
368
 
368
369
  _dial() {
370
+ // Guard the shared dial funnel: a close() during the pending _prepareDial
371
+ // (initial dial) or the reconnect backoff sets _closed; without this check
372
+ // the resolved continuation would open a socket + heartbeat nobody owns.
373
+ if (this._closed) return;
369
374
  var self = this;
370
375
  this._readyState = "connecting";
371
376
 
@@ -883,6 +888,18 @@ class WsClient extends EventEmitter {
883
888
  this._closed = true;
884
889
  return;
885
890
  }
891
+ if (this._readyState === "connecting") {
892
+ // No socket is established yet (the dial is still in _prepareDial's SSRF
893
+ // re-resolve or the handshake). There is no peer to exchange a graceful
894
+ // close frame with, so the 1000ms grace window buys nothing — and leaving
895
+ // _closed false across it lets a slow _prepareDial resolve into _dial()
896
+ // and open a socket nobody owns. Retire immediately (sets _closed and
897
+ // destroys any partial socket).
898
+ code = (typeof code === "number") ? code : CLOSE_NORMAL;
899
+ reason = (typeof reason === "string") ? reason : "";
900
+ this._teardown(code, reason, false);
901
+ return;
902
+ }
886
903
  code = (typeof code === "number") ? code : CLOSE_NORMAL;
887
904
  reason = (typeof reason === "string") ? reason : "";
888
905
  // RFC 6455 §5.5: control frames must be <= 125 bytes total. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.17.11",
3
+ "version": "0.17.13",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:8b687625-0aed-45e7-a4c6-fd40648ecbeb",
5
+ "serialNumber": "urn:uuid:dd71bb9a-bc83-498e-b1da-0abc600f88d2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-18T06:24:31.795Z",
8
+ "timestamp": "2026-07-23T20:20:36.895Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.17.11",
22
+ "bom-ref": "@blamejs/core@0.17.13",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.17.11",
25
+ "version": "0.17.13",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.17.11",
29
+ "purl": "pkg:npm/%40blamejs/core@0.17.13",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.17.11",
57
+ "ref": "@blamejs/core@0.17.13",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]