@blamejs/core 0.15.24 → 0.15.26
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 +4 -0
- package/lib/sandbox.js +23 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.26 (2026-06-25) — **Internal test-harness correctness only — the published library is byte-for-byte identical to 0.15.25.** The smoke runner requires each test module and awaits its exported run(). Several tests were instead written as a top-level (async function () {...})() IIFE that runs detached at require-time, so the runner measured and reported the test's result before the IIFE's post-await assertions executed — those checks silently never ran (one parser test exercised 4 of its 26 assertions, and a failure after the first await would have gone unseen as a false pass). Those tests are converted to the exported-run form so the runner awaits their full assertion set, and a codebase-patterns detector now refuses a top-level async IIFE in a test file so the pattern cannot return. No shipped framework code changed; this release is byte-for-byte identical to 0.15.25 for operators. **Fixed:** *Detached-IIFE tests now run their full assertion set under the smoke runner* — A test written as a top-level (async function () {...})() IIFE runs detached when the runner requires it: the runner only awaits an exported run(), so it reported the file's result before the IIFE's awaited assertions executed, and every check after the first await silently did not count (one parsers test ran 4 of 26). Such tests are rewritten to define async function run(), export it, and invoke it under if (require.main === module), so the runner awaits the complete set; a detector refuses a re-introduced top-level async IIFE in a test file. This is test-harness correctness only — no shipped framework behavior changed.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- 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).
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:eb6d4748-4fa9-4e44-b755-1d82d903256b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-25T22:27:20.120Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.26",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.26",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.26",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.15.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|