@blamejs/core 0.18.45 → 0.18.46

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,30 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.18.x
10
10
 
11
+ - v0.18.46 (2026-08-22) — **A failing test reported the handle it leaked instead of the check that failed.** Thirty test files ran their assertions inside a `try` whose `finally` drained open handles. A throw from a `finally` replaces the error the body raised, so when a test failed and its skipped teardown left a socket behind, what surfaced was "a handle leaked" — the consequence — while the check that actually failed was discarded. That is the shape of failure that reads as an unexplained flake, and it was reachable in every suite that opens a server. **Fixed:** *A drain no longer replaces the failure it followed* — `try { ...tests... } finally { await drain() }` reads as correct. It is not. When the body throws, every teardown after the throw is skipped, so the drain finds the servers those teardowns would have closed and throws too — and a throw from a `finally` replaces the body's error. The run reports a leaked handle, which is true and useless: the check that failed is gone, and the leak looks like the bug.
12
+
13
+ The drain now runs through one helper that keeps the body's error and appends the others to it, so a run that failed for a reason reports that reason, and a run that only leaked still reports the leak. Thirty files moved onto it.
14
+
15
+ Six of those thirty had their own cleanup — destroying a keep-alive agent, closing websocket clients and the sockets an upgrade detached from the server, resetting module state — sitting in the same `finally`. They were the worse instance rather than the exception: more throwable work in the place that replaces the failure. The helper took an optional teardown so their work runs inside the structure that preserves the error, instead of those six taking an exemption.
16
+
17
+ One file drained fifty-six times, once per section. A drain is a leak VERDICT rather than cleanup, so that was fifty-six places a real assertion failure could surface as a leak. Its sections now clean up and the verdict is taken once, at the end, where nothing is left to mask. The trade is that a leak there is reported against the file rather than the section; what the drain names is the surviving handles with their addresses and states, which is the attribution that matters. · *The helper that makes that promise is now tested* — Thirty files rely on "the body's error wins", and nothing checked it. A helper can stop doing the one thing it exists for while every suite using it still passes, because the suites only exercise it on the path where nothing goes wrong.
18
+
19
+ It is now driven directly: a body that throws, a teardown that throws after it, both throwing at once, a teardown that throws when the body passed, and a body that throws a string rather than an Error — which used to lose the appended note entirely, since assigning to a property of a primitive is a silent no-op outside strict mode.
20
+
21
+ The suite was confirmed to fail against the behaviour it forbids before it was kept. · *Two ways the failure-preserving helper could still lose the failure* — A thrown `null` was not a failure. Both helpers decided whether something had failed by asking whether the value it threw was truthy, so `throw null`, `throw ""` and `throw 0` read as passes: the drain helper returned normally, and the polling helper dropped "last predicate threw" from its timeout message, leaving "the condition never came true" where the truth was "the condition could not be evaluated". Both now track whether a throw happened as its own flag, separate from what was thrown.
22
+
23
+ And annotating an error could destroy it, or be invisible. When a run fails and its teardown fails after, the second failure is appended to the first — by mutating the error's message. An error can refuse to be mutated: a frozen one, or a `message` defined non-writable. That assignment throws, and the throw escapes from the annotator and replaces the failure it was called to annotate. Appending is now attempted and, when refused, the same information is carried in a new error that keeps the original as its cause.
24
+
25
+ The note also has to reach the place anybody reads. A runner prints `.stack`, not `.message`, and a stack string is formatted once on first read and then cached — so an error that some assertion or logger had already touched carried a stack headed by the original message, and the appended note never appeared. The note now goes on the stack as well, guarded on whether it is already there rather than on whether the stack was cached, so it lands exactly once.
26
+
27
+ All of it is the swallowing this release is about, one level further in: the helper that exists to stop a failure being lost had several ways of its own to lose one. · *A polling timeout blamed an exception the predicate had stopped raising* — The polling helper names the last exception in its timeout message, which is the difference between "the condition never came true" and "the condition could not be evaluated". It reported whatever had most recently been thrown, including from a poll many rounds earlier — so a predicate that raised while the thing it watches was still settling, and then answered cleanly, blamed the timeout on an exception it had stopped raising.
28
+
29
+ A throw during polling is not the verdict. The predicate is being asked repeatedly precisely because the state is still settling, so the loop now treats a throw as "not yet" and keeps nothing from it. The diagnostic speaks for one attempt — the one at the deadline — and answers the question worth asking: was the predicate still failing to evaluate when the wait gave up?
30
+
31
+ The timeout diagnostic now has its own tests, which is what surfaced this: whether the helper eventually returns is exercised by every suite that polls, and the message it produces when it does not was exercised by none of them. · *Two test files said they were outside the smoke run* — Both carried a header stating they were not part of the smoke surface. The runner takes every `*.test.js` under the layer directory, and the only opt-out is a `STANDALONE_ONLY` marker in the first two kilobytes, which neither file carries — so both have been running all along. A comment that says a failure here will not block is the kind that gets believed. **Detectors:** *A handle drain may not sit in a `finally`* — The shape is refused wherever it reappears. It matches both spellings — a direct call and a local wrapper — because a word boundary alone misses the underscore-prefixed form, which is exactly how six of the thirty files went uncounted when the population was first measured.
32
+
33
+ Anchored on the `finally` and the drain together and tempered so it cannot cross a function-closing brace; the quantifier is a backstop far above any real `finally` body rather than the precision mechanism. It was confirmed firing on all thirty before any of them were converted, and silent after the last one.
34
+
11
35
  - v0.18.45 (2026-08-21) — **`b.publicSuffix` returned names that cannot exist — a 64-octet label, and `ex*ample.com`.** `canonicalDomain` is where the framework decides what a domain IS, and two kinds of non-domain got through it. A label longer than the 63 octets RFC 1035 allows came back as an organizational domain, and 17 of 31 non-LDH characters survived because `domainToASCII` maps a name without validating it as a host. Both answers then travelled: into DMARC alignment, cookie-domain comparisons and residency routing, where a name no resolver will ever return is compared against one that exists. **Changed:** *Vendored `@blamejs/pki` 0.5.23 to 0.5.25* — The toolkit behind `b.mtlsCa` and `b.auth.passkey`. Two releases land together.
12
36
 
13
37
  0.5.24 binds an ACME certificate download to the order that asked for it. The end-entity certificate that comes back must carry the public key the order's CSR asked to have certified, and its identifier set must equal the order's — its dNSName and iPAddress subject alternative names, with the subject common name read only where it asserts none. `downloadCertificate` now requires `expectedSpki`, `identifiers`, or both, and refuses a call supplying neither rather than returning a certificate nothing looked at. An identifier that maps to no certificate name is refused rather than dropped from the comparison, which is what let an order for a name plus one other identifier be satisfied by a certificate covering only the name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.45",
3
+ "version": "0.18.46",
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:0c868505-f25e-4aac-a3e5-9d64001e0f98",
5
+ "serialNumber": "urn:uuid:7d5783b2-ffef-415c-b303-2b4cc46bc73c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-22T05:10:30.712Z",
8
+ "timestamp": "2026-08-22T08:31:57.142Z",
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.18.45",
22
+ "bom-ref": "@blamejs/core@0.18.46",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.45",
25
+ "version": "0.18.46",
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.18.45",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.46",
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.18.45",
57
+ "ref": "@blamejs/core@0.18.46",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]