@blamejs/core 0.15.21 → 0.15.22

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - 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.
12
+
11
13
  - v0.15.21 (2026-06-24) — **Adds a portable compare-and-swap UPDATE, an IPv6 /64 rate-limit key, a verify-only legacy-TOTP path, request-id AsyncLocalStorage scoping, schema-agnostic backup signing, side-effect-free FSM transition resolution, and the public codepoint-threat catalog — and fixes an FSM state transition that committed without an audit record when its entry hook threw.** A batch of additive primitives that close gaps operators hit composing the framework: b.sql.guardedUpdate builds the cross-instance-safe compare-and-swap UPDATE (the conditional-INSERT sibling) with b.sql.casWon reading the won/lost result; b.requestHelpers.ipKey (and a rateLimit ipKeyMode) key an IPv6 client by its routing-significant /64 so one end-site can't rotate the low 64 bits to evade a per-IP limit; b.auth.totp gains an opt-in verify-only SHA-1 path for one-final-login legacy-secret migration while generation stays SHA-512-only; b.middleware.requestId can bind the id into an AsyncLocalStorage scope that survives the awaited route chain; b.backupManifest.signBytes/verifyBytes authenticate a consumer's own canonical bytes with the framework signing key; b.fsm gains a side-effect-free instance.target() and a way to defer its transition audit when composed with an external claim; and the codepoint-threat catalog the guard family composes is exposed as b.codepointClass. A correctness fix: an FSM transition whose onEnter hook threw left the state committed but emitted no audit record — the record now always fires (stamped a failure outcome with the hook error) so a state change is never unaudited. **Added:** *b.sql.guardedUpdate + b.sql.casWon — portable compare-and-swap UPDATE* — b.sql.guardedUpdate(table) builds an UPDATE whose required guardWhere(col, expected) fence makes it land only when the row is still in the expected value — the cross-instance-safe way to advance a status or version on a single-statement-per-request backend (a D1-over-HTTP bridge or any autocommit-only adapter without interactive transactions), and the conditional-UPDATE sibling of b.sql.insertSelectWhere. It refuses to render without a fence (an unfenced guardedUpdate is just a plain update). b.sql.casWon(result) reads the won/lost verdict from the affected-row count, normalizing the rowCount / changes / affectedRows field-name divergence across adapters and throwing on an indeterminate result so a phantom win can't ship. Standard SQL across SQLite / Postgres / MySQL; guardWhereOp(col, op, expected) supports a non-equality fence (an optimistic-version or balance guard), and a null fence renders IS NULL. · *b.requestHelpers.ipKey + rateLimit ipKeyMode — IPv6 /64 keying* — b.requestHelpers.ipKey(ip, { ipv6Bits }) derives a stable rate-limit / blocklist key: an IPv4 address verbatim (one IPv4 is one host) but an IPv6 address collapsed to its routing-significant /64 prefix. A single IPv6 end-site is allocated a whole /64 (RFC 6177 / RFC 4291) and freely rotates the low 64 bits, so keying on the full 128-bit address lets one site mint unlimited fresh keys and walk a per-IP throttle or evade an exact-address block; keying on the /64 closes that while still distinguishing real end-sites. b.middleware.rateLimit gains ipKeyMode: "prefix64" to apply this to its default key (the audit record still logs the full client IP). · *b.auth.totp verify-only SHA-1 path for legacy-secret migration* — b.auth.totp.verify(secret, code, { algorithm: "sha1", verifyOnly: true }) authenticates a single legacy code during a re-enrollment flow, so a consumer migrating pre-existing RFC-6238-default (SHA-1) secrets can reuse the maintained verifier — separator stripping, 64-bit counter, drift / replay semantics — instead of hand-rolling a parallel HOTP. SHA-1 is still refused on every generation path (compute / generate / uri), so new-enrollment posture stays SHA-512-only, and the flag is honored only by verify(); each such verification emits an auth.totp.legacy_sha1_verify audit signal. · *b.middleware.requestId AsyncLocalStorage scoping* — b.middleware.requestId({ asyncContext: true }) binds the request id into the framework's AsyncLocalStorage scope so b.log.getRequestId() (and every b.log.create-built logger) returns it inside awaited route-handler code, not just on req.requestId. Because the b.router dispatch model runs the route handler after the middleware returns, the binding uses AsyncLocalStorage.enterWith (which persists forward across the awaited chain) rather than a callback wrap that would close before the handler runs; each request runs in its own async context so the binding stays request-scoped. The underlying b.log.enterRequestId(id) is exposed for callers wiring their own middleware. · *b.backupManifest.signBytes / verifyBytes — schema-agnostic signing* — b.backupManifest.signBytes(canonicalBytes) and verifyBytes(canonicalBytes, signatureBlock, { expectedFingerprint }) sign and verify a consumer's own canonical bytes with the same audit-sign keypair and fingerprint pinning as the v1-manifest sign() / verifySignature(), without adopting the framework's manifest schema — so a bespoke backup-header format can authenticate itself against the framework signing key. Fingerprint pinning is bound to the key the signature actually verifies under: the pin is checked against the fingerprint recomputed from the signature block's own public key (the new b.auditSign.fingerprintOf(publicKeyPem) helper), not the block's self-asserted fingerprint field, so a block can't claim a trusted fingerprint while being signed by a different key. The same binding now also applies to the v1-manifest verifySignature(). b.auditSign.verify additionally no longer requires init() when given an explicit public key, so a downstream verifier that holds only a trusted public key can check a detached signature. · *b.fsm side-effect-free transition resolution + deferrable audit* — instance.target(event) resolves a transition's destination state side-effect-free — the same edge and guard check as can() but returning the to-state (or null when the edge is illegal or guard-refused) — so a consumer composing an external compare-and-swap (b.sql.guardedUpdate on an autocommit-only substrate) can build the SET status = <to> claim without calling transition(), which would mutate state and emit an audit before the cross-instance claim is known to land. transition(event, { audit: false }) suppresses the built-in emit so that composition can emit its own enriched record once the claim resolves. · *b.codepointClass — the codepoint-threat catalog on the public surface* — The Unicode bidi-override / C0-control / zero-width / null-byte / Unicode-Tags tables and the UTS #39 confusable-script detector that the b.guard* family composes internally are now exposed as b.codepointClass, so a consumer can build a custom unconstrained-free-text screen — detectCharThreats / assertNoCharThreats / applyCharStripPolicies / scriptFor / detectMixedScripts plus the compiled regexes — without re-rolling the regexes (where the zero-width class is mistyped and the astral Unicode-Tags block forgotten) or coupling to an internal module path. For a ready-made free-text guard, b.guardText remains the first-class entry point. **Fixed:** *An FSM transition committed its state change without an audit record when the entry hook threw* — b.fsm commits the new state and pushes the history entry before running the destination state's onEnter hook. When that hook threw, control skipped the audit emission entirely — leaving a committed state transition with no audit record, a compliance gap for any regime that requires state changes to be auditable. The transition audit now always fires even when onEnter throws: it records the committed transition stamped with a failure outcome and the hook error, and still re-raises the error to the caller (the documented contract that an onEnter throw surfaces so the operator can roll back is unchanged).
12
14
 
13
15
  - v0.15.20 (2026-06-24) — **Vendored SBOM version fields are derived from the bundle so they cannot drift, and the Public Suffix List + @simplewebauthn/server bundles are refreshed.** The vendor manifest recorded each bundled package's version in two scanner-facing places that were hand-maintained and could drift from the code actually shipped — the structured components[].version (the CycloneDX component versions) and the cpe string. Two had drifted: peculiar-pki's @peculiar/x509 component read 1.13.0 while the bundle shipped 2.0.0, and @noble/curves' cpe read 0.0.0 while the bundle shipped 2.2.0. A CVE scanner (Trivy / Grype / a CycloneDX export, or a consumer mirroring the manifest into its own SBOM) keys on those structured fields, so an advisory was matched against the wrong version — a false negative on a real fix or a false positive on a patched one. Both fields are corrected, the vendor-bundle script now derives them from the actually-installed package versions at bundle time so they cannot drift again, and a manifest gate fails the build if they ever disagree. Separately, the vendored Mozilla Public Suffix List is refreshed to the current upstream revision and @simplewebauthn/server is refreshed to 13.3.2. **Changed:** *Vendored Public Suffix List and @simplewebauthn/server refreshed* — The vendored Mozilla Public Suffix List is refreshed to the current upstream revision (used by b.publicSuffix for DMARC / BIMI / cookie-scope / same-site domain classification). @simplewebauthn/server is refreshed from 13.3.1 to 13.3.2, which improves WebAuthn attestation certificate-path validation; the published-tarball diff was reviewed (no install scripts, no network/eval, a self-contained code change) before re-vendoring. **Fixed:** *Vendored SBOM version metadata is bundle-derived, not hand-maintained* — lib/vendor/MANIFEST.json recorded each package's version in two places a CVE/SBOM scanner reads — the structured components[].version sub-object and the cpe string — both hand-maintained alongside the human version string, so they could (and did) drift from the bundled code. @peculiar/x509's component version read 1.13.0 while the bundle shipped 2.0.0; @noble/curves' cpe read 0.0.0 while the bundle shipped 2.2.0. Either drift makes a scanner match advisories against the wrong version. Both are corrected to the shipped versions, and the durable fix is structural: scripts/vendor-update.sh now writes both the structured component versions and the cpe version from the ACTUALLY-INSTALLED package versions captured at bundle time, so a maintainer can no longer update one field and forget the other. A smoke-time manifest gate additionally fails the build if any component or cpe version disagrees with the package version, catching a manual drift before it ships.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.21",
3
+ "version": "0.15.22",
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:a9a56fb1-5dfb-4492-9a9e-65e6860f5ab4",
5
+ "serialNumber": "urn:uuid:43bc5585-96f4-48d7-b9ae-fd831f741d1a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-25T06:39:23.056Z",
8
+ "timestamp": "2026-06-25T07:50:15.970Z",
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.21",
22
+ "bom-ref": "@blamejs/core@0.15.22",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.15.21",
25
+ "version": "0.15.22",
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.21",
29
+ "purl": "pkg:npm/%40blamejs/core@0.15.22",
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.21",
57
+ "ref": "@blamejs/core@0.15.22",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]