@blamejs/core 0.16.25 → 0.16.27

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.16.x
10
10
 
11
+ - v0.16.27 (2026-07-12) — **Fix b.pubsub pattern subscriptions silently dropping every message for a single-wildcard pattern such as orders.*.created.** b.pubsub.subscribePattern with a * wildcard between dotted segments never matched any channel, so a pattern subscriber received nothing. The documented flagship pattern orders.*.created silently dropped orders.eu.created, and the same held for a.*.c, a.*.b.*.c, and any pattern whose wildcard sits between literal segments -- the source @example itself did not work. The segment matcher required a literal that follows a * to fit entirely before the next dot, but a trailing literal like .created contains a dot and legitimately begins before that boundary, so the match was rejected. The matcher now lets a post-wildcard literal begin at or before the next dot and matches it verbatim, while a single-segment * still never spans a dot -- so orders.*.created matches orders.eu.created but not orders.eu.fr.created or orders.created, and a pattern subscriber never receives a message from an extra path segment. Surfaced while covering the pub/sub matcher's branches. **Fixed:** *b.pubsub.subscribePattern matches a single-segment wildcard between literal segments* — A pattern with a * between dotted segments (the documented orders.*.created, or any a.*.c / a.*.b.*.c) matched no channel at all, so the pattern subscriber's handler never fired. The matcher rejected the literal that follows a wildcard unless it fit entirely before the next dot, but a trailing literal such as .created contains a dot and starts before that boundary, so a legitimate match was dropped. The literal after a wildcard is now allowed to begin at or before the next dot and is matched verbatim; the wildcard still matches exactly one non-dot segment, so orders.*.created matches orders.eu.created but not orders.eu.fr.created (two segments) or orders.created (none) -- a pattern subscription never widens to an extra path segment. Behaviour is now consistent with the documented example.
12
+
13
+ - v0.16.26 (2026-07-12) — **Stop b.db.exportCsv from crashing on an out-of-range timestamp value, and fix two CLI reporting faults (a vault-status stack trace and rollback-point metadata that never rendered).** Three defects surfaced while covering the db, cli, and http-client/mail-store error branches. b.db.exportCsv formatted a declared timestampFields column with new Date(v).toISOString(), which throws a RangeError for a finite millisecond value outside JavaScript's representable date range (beyond +/-8.64e15) and aborted the entire export; the one out-of-range value now degrades to its raw numeric string. The blamejs vault status command crashed with an uncaught VaultPassphraseError and a stack trace when the data directory did not exist, because the status path lacked the try/catch its sibling seal / unseal / rotate paths use; it now reports the same one-line reporter error. And blamejs restore list-rollbacks annotated each point from a non-existent recordedAt / bundleId, so the bundle id and timestamp never rendered; it now reads them from the fields b.restoreRollback.list returns (swappedAt, and bundleId / reason on the marker's operator metadata). **Fixed:** *b.db.exportCsv degrades an out-of-range timestamp value instead of aborting the export* — For a column named in timestampFields, exportCsv rendered a numeric value as new Date(value).toISOString(). A finite millisecond value beyond JavaScript's representable date range (greater than +/-8.64e15) produces an Invalid Date whose toISOString() throws a RangeError, which propagated out and aborted the whole export -- one out-of-range row could deny the entire CSV. Such a value now degrades to its raw numeric string, so the rest of the export completes. · *b.cli vault status reports a clean error when the data directory is absent* — The vault status subcommand called the sealable/unsealable preflight checks without the try/catch that its sibling seal / unseal / rotate subcommands use, so a missing or unreadable data directory raised an uncaught VaultPassphraseError -- the CLI shim printed a stack trace and exited on an unhandled rejection instead of the one-line "data-dir does not exist" reporter error the other vault subcommands give. The status path now catches the preflight error and reports it cleanly. · *b.cli restore list-rollbacks renders each rollback point's metadata* — The list-rollbacks row annotated each point with recordedAt and bundleId read from the top level of the point object, but b.restoreRollback.list returns { rollbackPath, swappedAt, marker } with the operator metadata (bundleId, reason) on marker.operator -- so those annotations were always empty. The listing now reads swappedAt from the point and bundleId / reason from the marker's operator metadata, so a rollback point's provenance is actually shown.
14
+
11
15
  - v0.16.25 (2026-07-12) — **Correct the OTLP protobuf span encoder's dropped_links_count field number so a future non-zero count cannot corrupt the Status field.** The OTLP/protobuf span encoder wrote the dropped_links_count placeholder on proto field 15, which is the Span's status field, instead of field 14 (dropped_links_count) per the OpenTelemetry trace proto. The emitted bytes are identical today because the count is always zero and proto3 omits zero-valued scalars, so no exported span is affected; but if a non-zero dropped-links count were ever emitted it would place a bare varint on the status field number and corrupt the Status message for a strict OTLP collector. The field number is corrected to 14. This surfaced while covering the OTLP exporter's previously-untested wire-format branches; a field-layout assertion now pins the span message's field numbers so a regression onto the status field is caught. **Fixed:** *b.observability OTLP protobuf span encoder emits dropped_links_count on the correct proto field* — In the protobuf span serializer, the dropped_links_count placeholder was encoded on field 15 -- the Span's status field, a length-delimited message -- rather than field 14, its correct number in the OpenTelemetry trace proto schema (the code's own inline comment already named field 14). Because the count is hardcoded to zero and proto3 omits zero-valued scalar fields, the serialized bytes are byte-identical today and no exported trace is affected. Were the placeholder ever set to a non-zero value (for example when span-link counting is added), each protobuf-encoded span would carry a wire-type-0 varint on the status field number and corrupt the Status message for a strict OTLP collector. The field number is now 14, and a wire-layout test pins the span message's field numbers (dropped_attributes_count at 10, dropped_events_count at 12, status length-delimited at 15) so this cannot regress silently.
12
16
 
13
17
  - v0.16.24 (2026-07-12) — **Restore the file-persistence mode of the outbound HTTP cookie jar, which failed on its first run before any cookies were saved.** b.httpClient.cookieJar.create({ persist: "file", file }) threw a CookieJarError with code LOAD_FAILED on the first run -- when the persist file does not yet exist -- instead of starting with an empty jar as documented ("loaded at create() if the file exists"). The load path recognized only a raw ENOENT for the missing file, but the safe reader it uses reports a missing file as the typed code atomic-file/enoent, so the normal first-use case fell through to LOAD_FAILED and the file-persistence mode never worked from a clean state. A missing persist file now starts an empty jar; a genuine read failure (symlink, over-size, or a concurrent-truncation TOCTOU) still fails closed as LOAD_FAILED. This surfaced while covering the cookie jar's previously-untested parse, match, and persistence branches. **Fixed:** *b.httpClient.cookieJar file persistence starts an empty jar on first run* — A cookie jar created with persist: "file" loads the persist file at create() time. On the very first run the file does not exist yet, and the safe file reader surfaces that missing file as the typed code atomic-file/enoent; the jar's load handler only treated a raw ENOENT as "start empty", so every first use threw CookieJarError LOAD_FAILED and the advertised file-persistence mode could never be initialized. The handler now treats both the raw ENOENT and the typed atomic-file/enoent as an empty starting state, while any other read failure (a symlink where a regular file was expected, an over-size file, or a truncation race) still fails closed as LOAD_FAILED. **Security:** *Continuous fuzzing now covers the DER/ASN.1, CMS, and HTTP Link-header parsers* — The DER/ASN.1 byte parser -- which sits under every untrusted-certificate path in the framework (peer TLS certificates, S/MIME, BIMI VMCs, CMS, ACME and TSA responses) -- the CMS SignedData / EnvelopedData parser (b.cms), and the HTTP Link response-header parser (b.linkHeader.parse) all consume attacker-controlled bytes, but because they are not named safe-* or guard-* they had sat outside the fuzz-harness discipline every other parser in the framework follows and had no fuzz coverage. Each now ships a coverage-guided fuzz harness run in CI, and the fuzz gate now requires a harness for an untrusted-byte parser regardless of its filename, so a regression that crashes one of these parsers instead of refusing the input cannot ship unnoticed. The current parsers refuse malformed input with typed errors under this fuzzing; the change is preventive coverage for the whole hostile-input parsing surface, not a fix for a present crash.
package/lib/cli.js CHANGED
@@ -1051,9 +1051,14 @@ async function _runRestore(args, ctx) {
1051
1051
  report.write("rollback points at " + rollbackRootL + ": " + pts.length);
1052
1052
  for (var p = 0; p < pts.length; p++) {
1053
1053
  var pt = pts[p];
1054
+ // restoreRollback.list() returns { rollbackPath, swappedAt, marker },
1055
+ // and operator metadata (bundleId / reason) rides on marker.operator --
1056
+ // not a top-level recordedAt/bundleId, so those never rendered.
1057
+ var ptOp = pt.marker && pt.marker.operator;
1054
1058
  report.write(" " + (pt.rollbackPath || pt) +
1055
- (pt.recordedAt ? " recordedAt=" + pt.recordedAt : "") +
1056
- (pt.bundleId ? " bundleId=" + pt.bundleId : ""));
1059
+ (pt.swappedAt ? " swappedAt=" + pt.swappedAt : "") +
1060
+ (ptOp && ptOp.bundleId ? " bundleId=" + ptOp.bundleId : "") +
1061
+ (ptOp && ptOp.reason ? " reason=" + ptOp.reason : ""));
1057
1062
  }
1058
1063
  return report.ok();
1059
1064
  } catch (e) {
@@ -1612,10 +1617,21 @@ async function _runVault(args, ctx) {
1612
1617
  var dataDir = _resolvePath(String(args.flags["data-dir"] || "./data"), ctx.cwd);
1613
1618
 
1614
1619
  if (sub === "status") {
1615
- var pre = vaultPassphraseOps.preflightSealable({ dataDir: dataDir });
1616
- var unsealable = vaultPassphraseOps.preflightUnsealable
1617
- ? vaultPassphraseOps.preflightUnsealable({ dataDir: dataDir })
1618
- : null;
1620
+ // A missing / unreadable data-dir must surface as the standard clean
1621
+ // reporter error (exit 1), not an uncaught throw: preflightSealable /
1622
+ // preflightUnsealable raise VaultPassphraseError when the dir doesn't
1623
+ // exist, and without this catch that rejection propagates out of main()
1624
+ // — the bin shim would print a stack trace instead of the one-line
1625
+ // "data-dir does not exist" the sibling seal / unseal / rotate paths give.
1626
+ var pre, unsealable;
1627
+ try {
1628
+ pre = vaultPassphraseOps.preflightSealable({ dataDir: dataDir });
1629
+ unsealable = vaultPassphraseOps.preflightUnsealable
1630
+ ? vaultPassphraseOps.preflightUnsealable({ dataDir: dataDir })
1631
+ : null;
1632
+ } catch (e) {
1633
+ return report.error((e && e.message) || String(e));
1634
+ }
1619
1635
  report.write("data-dir: " + dataDir);
1620
1636
  report.write("vault.key (plaintext): " +
1621
1637
  (pre.ok ? "present (sealable)" : "absent — " + (pre.reason || "n/a")));
package/lib/db.js CHANGED
@@ -2255,7 +2255,12 @@ function exportCsv(opts) {
2255
2255
  var col = columns[cj];
2256
2256
  var v = src[col];
2257
2257
  if (timestampFields.indexOf(col) !== -1 && typeof v === "number" && isFinite(v)) {
2258
- out[cj] = new Date(v).toISOString();
2258
+ // A finite ms value outside ±8.64e15 (JS Date's representable range)
2259
+ // yields an Invalid Date whose toISOString() throws RangeError. Degrade
2260
+ // to the raw numeric string so one garbage timestamp value can't crash
2261
+ // the whole export.
2262
+ var ts = new Date(v);
2263
+ out[cj] = isNaN(ts.getTime()) ? String(v) : ts.toISOString();
2259
2264
  } else if (Buffer.isBuffer(v)) {
2260
2265
  out[cj] = v.toString("base64");
2261
2266
  } else if (v === null || v === undefined) {
package/lib/pubsub.js CHANGED
@@ -102,19 +102,30 @@ function _matchGlobPart(part, channel, fromIdx) {
102
102
  if (channel.substr(pos, lit.length) !== lit) return -1;
103
103
  pos += lit.length;
104
104
  } else if (i === segments.length - 1) {
105
- // Trailing literal: search forward in this segment (no '.' jump).
105
+ // Trailing literal after the final '*'. The '*' gap between the
106
+ // previous literal and this one matches non-'.' chars only, so the
107
+ // literal must START at or before the next '.' boundary — but the
108
+ // literal itself MAY contain '.' (it is matched verbatim, e.g. the
109
+ // '.created' tail in 'orders.*.created'). Greedy: pick the
110
+ // rightmost dot-free-reachable start.
106
111
  var hardStop = channel.indexOf(".", pos);
107
- var searchEnd = hardStop === -1 ? channel.length : hardStop;
108
- if (lit === "") { pos = searchEnd; break; }
109
- var found = channel.lastIndexOf(lit, searchEnd - lit.length);
110
- if (found < pos) return -1;
111
- pos = found + lit.length;
112
+ var maxStart = hardStop === -1 ? channel.length : hardStop;
113
+ if (lit === "") { pos = maxStart; break; }
114
+ var placed = -1;
115
+ for (var s = maxStart; s >= pos; s--) {
116
+ if (channel.substr(s, lit.length) === lit) { placed = s; break; }
117
+ }
118
+ if (placed < 0) return -1;
119
+ pos = placed + lit.length;
112
120
  } else {
113
- // Middle literal: find next occurrence within current segment.
121
+ // Middle literal after a '*'. The '*' gap must be dot-free, so the
122
+ // literal must START at or before the next '.'; the literal itself
123
+ // MAY contain '.'. Leftmost occurrence keeps the preceding '*'
124
+ // minimal.
114
125
  var hardStop2 = channel.indexOf(".", pos);
115
- var searchEnd2 = hardStop2 === -1 ? channel.length : hardStop2;
126
+ var maxStart2 = hardStop2 === -1 ? channel.length : hardStop2;
116
127
  var f2 = channel.indexOf(lit, pos);
117
- if (f2 < 0 || f2 + lit.length > searchEnd2) return -1;
128
+ if (f2 < 0 || f2 > maxStart2) return -1;
118
129
  pos = f2 + lit.length;
119
130
  }
120
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.25",
3
+ "version": "0.16.27",
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:62d0a818-c929-43df-a5e4-0d012b57491c",
5
+ "serialNumber": "urn:uuid:569a359f-c69b-4562-a2ec-f31668019c7f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T04:53:32.990Z",
8
+ "timestamp": "2026-07-13T07:47:49.374Z",
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.16.25",
22
+ "bom-ref": "@blamejs/core@0.16.27",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.16.25",
25
+ "version": "0.16.27",
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.16.25",
29
+ "purl": "pkg:npm/%40blamejs/core@0.16.27",
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.16.25",
57
+ "ref": "@blamejs/core@0.16.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]