@blamejs/core 0.15.23 → 0.15.25

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.25 (2026-06-25) — **`b.sandbox.run` no longer leaks the worker thread's MessagePort — it now waits for the worker to terminate before resolving.** b.sandbox.run spawned a worker thread to execute untrusted code and called worker.terminate() on both the result and timeout paths, but it settled the caller's promise BEFORE the asynchronous terminate() completed — leaving the worker's MessagePort alive past the resolve. In a long-lived process that runs sandboxed code repeatedly, each call leaked a MessagePort handle, keeping the event loop populated and slowing graceful shutdown. The call now awaits worker.terminate() before settling, so the handle is released when the promise resolves. Behaviour and return values are unchanged. **Fixed:** *b.sandbox.run releases the worker MessagePort before resolving* — b.sandbox.run resolved (or rejected) the caller's promise as soon as the worker reported a result or the timeout fired, while worker.terminate() — which is asynchronous — was still in flight, so the worker thread's MessagePort outlived the call and lingered as an open handle. Repeated sandbox runs in a long-lived process accumulated leaked MessagePorts that kept the event loop alive and delayed shutdown. The result and timeout paths now defer settling until worker.terminate() resolves; the error and exit paths already imply the worker is gone. The return value, audit events, and timeout semantics are unchanged.
12
+
13
+ - v0.15.24 (2026-06-25) — **Supply-chain hardening for the build: the example wiki container's base images are digest-pinned (and tracked for patches by Dependabot), and the SEA-build esbuild binary pin is centralized and release-enforced so a version bump can't silently lose its reviewed hash.** A build supply-chain hardening release; no runtime API changes. The example wiki container's Chainguard base images are now pinned by digest (tag@sha256) for reproducible builds and provenance, with Dependabot's docker updater bumping the digests so they keep tracking Chainguard's continuous CVE rebuilds; the published multi-arch image is unaffected because the release workflow still resolves the rolling tag to a fresh digest at build time, so the scanned image equals the published image and the committed digest never reaches the image-scan gate. The esbuild native-binary supply-chain pin — the reviewed per-platform SHA-256 the bundler smoke gate checks the on-disk compiler against — is centralized in one file and enforced both at release time and in the pattern gate by a single shared verifier: the version package.json and the CI/publish workflows install must carry a complete reviewed-hash entry, so bumping esbuild without re-reviewing and re-pinning the binary now fails closed instead of silently degrading the verification to a skip. **Security:** *Wiki container base images are digest-pinned and patch-tracked* — examples/wiki/Dockerfile now pins its Chainguard base images by digest (cgr.dev/chainguard/node:<tag>@sha256:...) for reproducible local builds and supply-chain provenance, and Dependabot's docker updater bumps those digests so they keep tracking Chainguard's continuously-rebuilt CVE fixes instead of freezing. The published multi-arch image is unchanged: the release workflow still resolves the rolling tag to a fresh digest at build time and passes it as a build-arg, so the scanned image equals the published image and always carries current patches — the committed digest never reaches the image-scan gate. · *esbuild SEA-build binary pin is centralized and fails closed on an un-reviewed bump* — The reviewed per-platform SHA-256 hashes that the bundler smoke gate verifies the esbuild native compiler against are now a single source of truth (scripts/esbuild-binary-pin.json), checked by one shared verifier that runs both at release time and in the pattern gate. It enforces that the esbuild version package.json declares and the CI/publish workflows install carries a complete reviewed-hash entry for every required platform. Previously a bump to a version with no reviewed hash made the smoke gate note-and-skip the binary verification; that omission now fails the release, so the binary pin can no longer be silently lost. The hashes remain captured by hand on each bump (diff the published tarballs, sha256 the binary) — the verifier checks the pin is present, it never derives it.
14
+
11
15
  - v0.15.23 (2026-06-24) — **`b.wsClient` error objects now carry a usable terminal-vs-transient signal (and the client's auto-reconnect, previously dead, works again), and `b.safePath` resolves cross-platform containment with the target platform's path semantics — fixing both a false refusal of in-base paths and a backslash-traversal escape when validating for Windows on a POSIX host.** Two correctness fixes. b.wsClient marked every WsClientError permanent, so a consumer driving its own reconnect loop could not tell a terminal handshake failure (a bad URL, a 4xx rejection, an accept-mismatch, a protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong/handshake timeout, a dropped socket) and had to re-derive the taxonomy from error codes. err.permanent now reflects the actual transience of each error, the single bad-status code is split by the carried HTTP status (4xx terminal, 5xx transient) with the status exposed as err.statusCode, and — because the client's own auto-reconnect keyed off the same always-true flag — auto-reconnect was silently disabled for every transient failure and now fires correctly. Separately, b.safePath.resolve / resolveOrNull / validate performed its lexical containment with the runtime path module while the per-segment naming walk used opts.platform. The two disagreeing broke cross-platform validation both ways: every legitimate in-base path was refused with safe-path/escapes-base when opts.platform differed from the host, and — more seriously — a POSIX host validating opts.platform: "windows" accepted a backslash traversal (ok\..\..\outside), because the runtime resolver treats \ as an ordinary filename character and never collapsed the .. segments, so the path escaped the base once a Windows consumer read the backslashes. The lexical resolve and containment boundary now use the target platform's path module (node:path.win32 / node:path.posix), matching the segment walk, so in-base paths resolve and cross-platform traversals are refused under any opts.platform override; the realpath check, which touches the live filesystem, keeps its runtime resolve. **Fixed:** *b.wsClient: WsClientError carries a real terminal/transient signal, and auto-reconnect works again* — WsClientError was declared always-permanent, so err.permanent was true for every error — a consumer's reconnect loop could not distinguish a terminal handshake failure (bad URL, bad subprotocol, malformed handshake header, accept-mismatch, bad upgrade, bad status line, a 4xx rejection, an oversized/protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong- or handshake-timeout, a dropped socket) and had to maintain its own error-code list tracking the framework's taxonomy. err.permanent now reflects each error's actual transience, derived from its code (a new/unknown code defaults to terminal, so it fails closed rather than redialing a hopeless target forever); the single ws-client/bad-status code is split by the response status (4xx terminal, 5xx transient) and the status is exposed as err.statusCode (the existing err.status alias is preserved). The same always-permanent flag also drove the client's built-in auto-reconnect, which therefore never retried any framework-surfaced transient failure (a 5xx handshake or a keepalive timeout); reconnect now fires for transient failures and still skips terminal ones. · *b.safePath: cross-platform containment resolves with the target platform's path semantics* — b.safePath.resolve / resolveOrNull / validate performed its lexical containment (resolve rel under base, then bound the result with a separator slice) using the runtime path module, while the per-segment naming walk used opts.platform. When the two disagreed, validation broke both ways. Benign direction: a Linux service validating server-origin names against the stricter Windows ruleset (the recommended cross-platform pattern) had every legitimate in-base path refused with safe-path/escapes-base, because the runtime-separated resolved path could never match a Windows-separator boundary. Security direction: a POSIX host validating opts.platform: "windows" accepted a backslash traversal such as ok\..\..\outside — the segment walk splits on \ for Windows, but the runtime resolver on POSIX treats \ as an ordinary filename character and never collapsed the .. segments, so the path passed containment and resolved to <base>/ok\..\..\outside, which escapes the base once a Windows consumer interprets the backslashes. The lexical resolve and the containment boundary now use the target platform's path module (node:path.win32 when validating for Windows, node:path.posix otherwise), matching the segment walk, so in-base paths resolve correctly and a genuine traversal is refused under any opts.platform override. The realpath check, which resolves symlinks on the live filesystem, keeps a separate runtime resolve because a foreign-platform path cannot be symlink-resolved on the host. opts.platform continues to gate the per-segment naming rules (reserved names, trailing dot/space, NTFS ADS colon).
12
16
 
13
17
  - v0.15.22 (2026-06-24) — **Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.21.** This release changes only the repository's own test harness and test files; nothing under the shipped tarball (index.js, bin/, lib/) is touched, so the API, behavior, and published files are identical to 0.15.21 and operators have nothing to do. The smoke worker now attributes a fire-and-forget failure — an unawaited promise or an unreleased handle (a retry timer, a shutdown wait, a watcher/reload restart) that throws AFTER a test's assertions already passed — to the exact test that caused it and retries it once, instead of the prior unattributable, intermittent failure that only surfaced on a resource-starved CI runner. Five test files that invoked their run() at module scope (re-running and, in some cases, exiting the worker before it could report a result) are corrected to run only under require.main === module, and a test-discipline check keeps the pattern from returning. An opt-in handle-leak audit (SMOKE_AUDIT_HANDLES) surfaces tests that hold a timer / socket / server / worker past completion, for a follow-up cleanup pass. **Changed:** *Smoke test harness attributes late-async-error failures and fixes module-level test self-execution* — The forked smoke worker installs unhandledRejection / uncaughtException handlers and a settle tick so a failure that fires after a test's assertions pass — a leaked handle's callback or an unawaited promise from retry / shutdown / reload logic — is reported against the test that caused it and retried once (a transient passes, a persistent one fails again and names the bug), replacing the prior intermittent, unattributable 'fork failed' that only appeared on a starved CI runner. Five test files (two integration, three layer-0, plus two wiki-suite harnesses) that called run() at module scope are corrected to execute only under require.main === module so they no longer double-run or exit the worker prematurely when required, and a codebase-patterns test-discipline detector prevents the unguarded-module-level-run shape from recurring. A SMOKE_AUDIT_HANDLES opt-in reports per-file handle leaks for a tracked cleanup. None of this is in the published package.
package/lib/sandbox.js CHANGED
@@ -266,23 +266,41 @@ function run(opts) {
266
266
  return;
267
267
  }
268
268
 
269
+ // Terminate the worker and only settle the caller's promise once the
270
+ // termination resolves. worker.terminate() is asynchronous: settling
271
+ // before it completes leaves the worker thread's MessagePort alive past
272
+ // the resolve, keeping the event loop open (a leaked handle that delays
273
+ // a shut-down on a slow runner). Awaiting terminate() releases it.
274
+ function _terminateThen(finish) {
275
+ var done = false;
276
+ function _once() { if (done) return; done = true; finish(); }
277
+ var p;
278
+ try { p = worker.terminate(); } catch (_e) { p = null; }
279
+ if (p && typeof p.then === "function") { p.then(_once, _once); }
280
+ else { _once(); }
281
+ }
282
+
269
283
  var timer = setTimeout(function () {
270
284
  if (settled) return;
271
285
  settled = true;
272
- try { worker.terminate(); } catch (_e) { /* terminate best-effort */ }
273
286
  var elapsed = Date.now() - startedAt;
274
287
  _emitAudit("sandbox.run.refused", "failure", {
275
288
  reason: "sandbox/timeout", runtimeMs: elapsed, peakBytes: 0, sourceBytes: sourceBytes,
276
289
  });
277
- reject(new SandboxError("sandbox/timeout",
278
- "sandbox.run: worker exceeded timeoutMs=" + timeoutMs + " (elapsed " + elapsed + "ms)"));
290
+ _terminateThen(function () {
291
+ reject(new SandboxError("sandbox/timeout",
292
+ "sandbox.run: worker exceeded timeoutMs=" + timeoutMs + " (elapsed " + elapsed + "ms)"));
293
+ });
279
294
  }, timeoutMs);
280
295
 
281
296
  worker.on("message", function (msg) {
282
297
  if (settled) return;
283
298
  settled = true;
284
299
  clearTimeout(timer);
285
- try { worker.terminate(); } catch (_e) { /* terminate best-effort */ }
300
+ _terminateThen(function () { _handleMessage(msg); });
301
+ });
302
+
303
+ function _handleMessage(msg) {
286
304
  if (!msg || typeof msg !== "object") {
287
305
  _emitAudit("sandbox.run.refused", "failure", {
288
306
  reason: "sandbox/bad-worker-message", runtimeMs: Date.now() - startedAt, peakBytes: 0, sourceBytes: sourceBytes,
@@ -312,7 +330,7 @@ function run(opts) {
312
330
  });
313
331
  return reject(new SandboxError(msg.code || "sandbox/runtime-error",
314
332
  msg.message || "sandbox.run: worker reported a refusal"));
315
- });
333
+ }
316
334
 
317
335
  worker.on("error", function (err) {
318
336
  if (settled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.23",
3
+ "version": "0.15.25",
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:8b15a312-89a7-49e1-b4c1-f90240608195",
5
+ "serialNumber": "urn:uuid:5197f455-603c-4d3d-b5eb-39adf581713f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-25T09:42:41.053Z",
8
+ "timestamp": "2026-06-25T20:36:43.824Z",
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.15.23",
22
+ "bom-ref": "@blamejs/core@0.15.25",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.15.23",
25
+ "version": "0.15.25",
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.15.23",
29
+ "purl": "pkg:npm/%40blamejs/core@0.15.25",
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.15.23",
57
+ "ref": "@blamejs/core@0.15.25",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]