@blamejs/core 0.16.27 → 0.16.28

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.16.x
10
10
 
11
+ - v0.16.28 (2026-07-13) — **Fix a Sieve filter bypass where an explicit :comparator silently disabled a header/address/envelope test, plus a JSONPath normalized-path round-trip fault and a rejected daemon cwd option.** Three defects surfaced while covering the Sieve interpreter, JSONPath engine, and daemon supervisor. In b.mail.sieve, an explicit :comparator on a header, address, or envelope test silently disabled the whole test: the Sieve parser did not bind the comparator name to its tag, so the comparator string was consumed as the first positional argument (the header/address name) and every real argument shifted by one -- the test looked up a non-existent header, never matched, and the message fell through to implicit keep, so a discard / fileinto / redirect that should have fired instead delivered the message. The parser now binds :comparator to its tag (RFC 5228 §2.7.3), so the comparator is applied and the arguments stay aligned. b.jsonPath.paths emitted a normalized path containing raw control characters (only ' and \ were escaped), which the query parser then rejected, so a path returned by paths() did not round-trip through query(); control characters are now escaped per RFC 9535 §2.7. And b.daemon.start rejected its own documented cwd option with an unknown-option error; cwd is now accepted and forwarded as the detached child's working directory. **Fixed:** *b.jsonPath.paths emits a normalized path that round-trips through b.jsonPath.query* — A normalized path returned by paths() escaped only the single-quote and backslash characters inside a member name, leaving raw control characters (newline, tab, and the rest of U+0000 through U+001F) in the output. The query parser rejects an unescaped control character in a name selector, so such a path could not be fed back into query(). Control characters in a normalized-path name are now escaped as their short form (\b \t \n \f \r) or \uXXXX, per RFC 9535 §2.7, so paths() output round-trips. · *b.daemon.start accepts and applies the documented cwd option* — The start() options validator did not list cwd among the accepted options, so passing the documented cwd (the working directory for the detached child) was rejected with a daemon/bad-opts unknown-option error and the feature was unusable. cwd is now an accepted optional string and is forwarded to the detached child spawn as its working directory. **Security:** *b.mail.sieve applies an explicit :comparator instead of silently disabling the test* — A Sieve script that used an explicit :comparator on a header, address, or envelope test -- for example header :comparator "i;octet" :is "Subject" "..." -- silently stopped filtering. The parser did not attach the comparator's value string to the :comparator tag, so it was consumed as the first positional argument (the header or address name) and shifted the real name and key list by one position. The test then queried a non-existent header, never matched, and evaluation fell through to implicit keep, so a discard, fileinto, or reject the operator intended was not applied and the message was delivered anyway -- a filter bypass for any rule with an explicit comparator. The parser now binds :comparator to its tag per RFC 5228 §2.7.3, so the chosen comparator (i;octet exact vs the default i;ascii-casemap case-insensitive) is applied and the positional arguments stay aligned; a :comparator with no following comparator-name string is refused at parse time.
12
+
11
13
  - 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
14
 
13
15
  - 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.
package/lib/daemon.js CHANGED
@@ -100,6 +100,8 @@ function _validateStartOpts(opts) {
100
100
  },
101
101
  command: { rule: "optional-string", code: "daemon/bad-command",
102
102
  label: "daemon.start: opts.command (path to executable)" },
103
+ cwd: { rule: "optional-string", code: "daemon/bad-cwd",
104
+ label: "daemon.start: opts.cwd (working directory for the detached child)" },
103
105
  args: function (value) {
104
106
  if (value !== undefined && !Array.isArray(value)) {
105
107
  throw new DaemonError("daemon/bad-args",
package/lib/json-path.js CHANGED
@@ -623,12 +623,34 @@ function query(doc, path) {
623
623
  return _evalSegments(ast.segments, doc).map(function (n) { return n.value; });
624
624
  }
625
625
 
626
+ // RFC 9535 §2.7: a normalized-path name is single-quoted and MUST escape
627
+ // `'`, `\`, and every control code (%x0-1F) — the latter as the named
628
+ // short escape (\b \t \n \f \r) or \uXXXX. Emitting a raw control char
629
+ // produces a path that no longer round-trips through the parser (which
630
+ // rejects unescaped control characters in a string literal).
631
+ function _normalizeName(name) {
632
+ var out = "";
633
+ for (var i = 0; i < name.length; i++) {
634
+ var ch = name.charAt(i), cc = name.charCodeAt(i);
635
+ if (ch === "'") out += "\\'";
636
+ else if (ch === "\\") out += "\\\\";
637
+ else if (cc === 0x08) out += "\\b";
638
+ else if (cc === 0x09) out += "\\t";
639
+ else if (cc === 0x0a) out += "\\n";
640
+ else if (cc === 0x0c) out += "\\f";
641
+ else if (cc === 0x0d) out += "\\r";
642
+ else if (cc < 0x20) out += "\\u" + ("0000" + cc.toString(16)).slice(-4);
643
+ else out += ch;
644
+ }
645
+ return out;
646
+ }
647
+
626
648
  function _normalizedPath(tokens) {
627
649
  var out = "$";
628
650
  for (var i = 0; i < tokens.length; i++) {
629
651
  var t = tokens[i];
630
652
  if (typeof t === "number") out += "[" + t + "]";
631
- else out += "['" + String(t).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "']";
653
+ else out += "['" + _normalizeName(String(t)) + "']";
632
654
  }
633
655
  return out;
634
656
  }
package/lib/safe-sieve.js CHANGED
@@ -429,7 +429,39 @@ function _parseScript(tokens, caps, requiredCaps) {
429
429
  }
430
430
  if (t.k === "tag") {
431
431
  consume("tag");
432
- tags.push({ name: t.v });
432
+ // `:comparator <string>` binds the FOLLOWING string as its value
433
+ // (RFC 5228 section 2.7.3). It must be attached to the tag, NOT left in
434
+ // the positional stream -- otherwise the comparator name lands at
435
+ // positional[0] and shifts every real positional arg (the header/address
436
+ // name and the key list) by one, silently breaking the test so it never
437
+ // matches and the message falls through to implicit keep (a filter
438
+ // bypass). In the base grammar `:comparator` is the only value-taking tag.
439
+ if (t.v === "comparator") {
440
+ var cv = peek();
441
+ if (cv.k !== "str") {
442
+ throw new SafeSieveError("safe-sieve/parse-error",
443
+ "safeSieve.parse: :comparator must be followed by a comparator-name string");
444
+ }
445
+ consume("str");
446
+ // Validate the comparator name against the implemented set, exactly as
447
+ // `require ["comparator-<name>"]` does -- otherwise an unsupported
448
+ // comparator (e.g. i;unicode-casemap) would be accepted here and the
449
+ // interpreter would silently treat it as octet (exact) matching,
450
+ // bypassing the capability guard. Fail closed on anything not
451
+ // implemented.
452
+ var compCap = "comparator-" + cv.v;
453
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_CAPABILITIES, compCap)) {
454
+ throw new SafeSieveError("safe-sieve/unknown-capability",
455
+ "safeSieve.parse: unknown comparator \"" + cv.v + "\"");
456
+ }
457
+ if (KNOWN_CAPABILITIES[compCap] === false) {
458
+ throw new SafeSieveError("safe-sieve/unimplemented-capability",
459
+ "safeSieve.parse: unimplemented comparator \"" + cv.v + "\"");
460
+ }
461
+ tags.push({ name: t.v, val: cv.v });
462
+ } else {
463
+ tags.push({ name: t.v });
464
+ }
433
465
  continue;
434
466
  }
435
467
  if (t.k === "num") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.27",
3
+ "version": "0.16.28",
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:569a359f-c69b-4562-a2ec-f31668019c7f",
5
+ "serialNumber": "urn:uuid:85863bdc-58d9-420d-9ab0-d319a39c7432",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T07:47:49.374Z",
8
+ "timestamp": "2026-07-13T10:50:02.597Z",
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.27",
22
+ "bom-ref": "@blamejs/core@0.16.28",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.16.27",
25
+ "version": "0.16.28",
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.27",
29
+ "purl": "pkg:npm/%40blamejs/core@0.16.28",
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.27",
57
+ "ref": "@blamejs/core@0.16.28",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]