@blamejs/core 0.6.64 → 0.6.66

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.6.x
10
10
 
11
+ - **0.6.66** (2026-05-03) — `b.auditChain.canonicalize` deep-walks values and rejects non-plain types instead of silently round-tripping garbage. Same bug class as the v0.6.60 pagination canonicalize fix: pre-fix the walker did `Object.keys(row).map(...)` and JSON.stringify'd whatever fell through, which silently encoded **Map / Set / RegExp** as `{}`, **Symbol / function** as missing keys, **BigInt** as a thrown `Do not know how to serialize` mid-emit (DoS-shape on any operator routing bigint IDs into audit metadata), and **circular references** as the unwrapped JSON.stringify error message. The post-fix walker: BigInt → decimal string (no data loss for large account IDs / 64-bit counters from external-DB drivers); Date → ISO string (already worked via JSON.stringify but now explicit before the recursive walk); Map / Set / RegExp / Symbol / function reject with a clear constructor-name message; circular refs detected via WeakSet and rejected as a framework error. Walks recursively so nested structures (`{ a: [BigInt(1), BigInt(2)] }`, `{ a: { b: Uint8Array(…) } }`) all serialise correctly. **Stored-row compat** — existing audit rows verify identically because their stored `metadata` columns are already JSON strings; the change only affects EMIT-time serialisation. **Tests** — 17 new layer-0 assertions in `test/00-primitives.js`. Smoke 7234 → 7251 / wiki e2e 178 / Linux 86.1s / eslint clean / shellcheck clean.
12
+
13
+ - **0.6.65** (2026-05-03) — `b.scheduler.parseCron` rejects step values exceeding the field's range. Pre-fix `*/99999 * * * *` was silently accepted and degenerated to "minute 0 of every hour" (because the for-loop adding values stopped at the first iteration when `step > range`); an operator typing the typo got a once-per-hour schedule when they probably meant once per N minutes. Now `_parseCronField` rejects with `scheduler/invalid-cron` when `step > (range.max - range.min + 1)`. The bound is inclusive so `*/60 * * * *` (= "minute 0 of every hour" written with a redundant explicit step) still accepts. **Tests** — 4 new layer-0 assertions: `*/60` accepts, `*/61` rejects, `*/99999` rejects (the silent-degenerate case), `* */25 * * *` rejects (step > 24-hour range). Smoke 7230 → 7234 / wiki e2e 178 / Linux 86.9s / eslint clean / shellcheck clean.
14
+
11
15
  - **0.6.64** (2026-05-03) — `b.credentialHash.verify` now enforces the same 16-byte minimum payload that `hash()` enforces. Pre-fix the asymmetry was a real risk: `hash()` refused to *create* a hash shorter than 16 bytes (`length < 16` rejected with `credential-hash/bad-opt`), but `verify()` silently accepted any payload length, including 1-byte payloads where the collision space is **256** — a hand-crafted envelope `{ magic, algoId, SHAKE256(targetPassword, 1)[0] }` would verify against the target password in microseconds. A storage bug or attacker tampering that truncated the stored envelope produced a verifiable but catastrophically weak hash; an attacker with DB write access who couldn't replace the hash outright could *truncate* it to a nearly-cleartext-equivalent value. Both ends now refuse short payloads symmetrically via the new `SHAKE256_MIN_LENGTH = 16` constant; verify returns false (with `payload-too-short` observability label) when the envelope's payload is < 16 bytes. Legitimate truncation that keeps the payload ≥ 16 bytes still verifies (the framework's design intentionally supports XOF variable-length digests via `params: { length: N }`); the bound only refuses hashes too short to provide meaningful collision resistance. **Tests** — 4 new layer-0 assertions on `b.credentialHash.verify` (1-byte attack envelope rejected, 15-byte rejected, 16-byte accepts, hash() rejects length 15). Smoke 7226 → 7230 / wiki e2e 178 / Linux 86.5s / eslint clean / shellcheck clean.
12
16
 
13
17
  - **0.6.63** (2026-05-03) — `b.parsers.toml.parse` enforces `maxDepth` on dotted-key paths. Pre-fix the parser walked arbitrarily deep table headers (`[a.b.c.d.e…]`) without applying the existing `maxDepth` (which only ran inside `_parseValue` for inline tables / arrays); a 10,000-segment path built a tree deep enough to stack-overflow the recursive `_normalize` walker post-parse. An attacker submitting a malicious TOML config to `b.config` would crash the framework at boot or whenever the file was reloaded. Now `_parseDottedKey` rejects a path with more than `maxDepth` segments via the same `toml/too-deep` error code already used for value-side depth. **Tests** — 4 new layer-0 assertions: 99-segment path under default depth accepts, 150-segment path rejects with `toml/too-deep`, 10K-segment path rejects (no stack overflow), `maxDepth: 1000` opt allows a 500-segment path. Smoke 7222 → 7226 / wiki e2e 178 / Linux 85.6s / eslint clean / shellcheck clean.
@@ -26,16 +26,55 @@ var ZERO_HASH = "0".repeat(128);
26
26
  // Canonicalize a row for hashing. Excludes the hash/nonce columns themselves
27
27
  // and any caller-specified columns. Sorted keys, JSON-encoded values; Buffer
28
28
  // values converted to hex for stable byte serialization.
29
+ //
30
+ // Deeply walks the value tree so non-plain types reject cleanly instead of
31
+ // silently round-tripping as `{}` (Map / Set / RegExp), as `{"0":97,…}`
32
+ // (Buffer / typed-array nested inside arrays / objects), or as missing
33
+ // keys (Symbol / function). BigInt converts to its decimal string —
34
+ // large account IDs / monotonic counters from external-DB drivers
35
+ // frequently land as BigInt, and crashing the audit emit on those would
36
+ // turn them into availability incidents. Circular refs throw a clean
37
+ // framework Error rather than the raw JSON.stringify message.
38
+ function _scrub(value, seen) {
39
+ if (value === null || typeof value === "undefined") return null;
40
+ var t = typeof value;
41
+ if (t === "string" || t === "boolean" || t === "number") return value;
42
+ if (t === "bigint") return String(value);
43
+ if (t === "symbol" || t === "function") {
44
+ throw new Error("audit-chain canonicalize: " + t + " value is not " +
45
+ "serialisable; convert to a string before emit");
46
+ }
47
+ if (Buffer.isBuffer(value)) return value.toString("hex");
48
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("hex");
49
+ if (value instanceof Date) return value.toISOString();
50
+ // After the primitives + Buffer + Date, any remaining "object" must be
51
+ // a plain object or array. Map / Set / RegExp / class instances all
52
+ // reject with a clear constructor name in the error so operators see
53
+ // exactly which audit metadata field is the culprit.
54
+ if (value instanceof Map || value instanceof Set || value instanceof RegExp) {
55
+ throw new Error("audit-chain canonicalize: " + value.constructor.name +
56
+ " is not serialisable; convert to a plain primitive / array / object first");
57
+ }
58
+ seen = seen || new WeakSet();
59
+ if (seen.has(value)) {
60
+ throw new Error("audit-chain canonicalize: circular reference in audit row");
61
+ }
62
+ seen.add(value);
63
+ if (Array.isArray(value)) return value.map(function (v) { return _scrub(v, seen); });
64
+ var keys = Object.keys(value).sort();
65
+ var out = {};
66
+ for (var i = 0; i < keys.length; i++) {
67
+ out[keys[i]] = _scrub(value[keys[i]], seen);
68
+ }
69
+ return out;
70
+ }
71
+
29
72
  function canonicalize(row, excludeKeys) {
30
73
  var ex = new Set(excludeKeys || []);
31
74
  var keys = Object.keys(row).filter(function (k) { return !ex.has(k); }).sort();
32
75
  var pairs = {};
33
76
  for (var i = 0; i < keys.length; i++) {
34
- var v = row[keys[i]];
35
- if (Buffer.isBuffer(v)) v = v.toString("hex");
36
- else if (v instanceof Uint8Array) v = Buffer.from(v).toString("hex");
37
- else if (v === undefined) v = null;
38
- pairs[keys[i]] = v;
77
+ pairs[keys[i]] = _scrub(row[keys[i]]);
39
78
  }
40
79
  return JSON.stringify(pairs);
41
80
  }
package/lib/scheduler.js CHANGED
@@ -123,6 +123,21 @@ function _parseCronField(text, range) {
123
123
  throw new SchedulerError("scheduler/invalid-cron",
124
124
  "bad step '" + stepStr + "' in cron field '" + range.name + "'", true);
125
125
  }
126
+ // Reject step > field-range, even though the for-loop below would
127
+ // silently produce a single-value schedule (e.g. `*/99999` for
128
+ // minutes degenerates to "minute 0 of every hour"). An operator
129
+ // typing `*/99999` clearly meant something else; silent acceptance
130
+ // hides the typo and produces a schedule that fires once per hour
131
+ // when the operator probably wanted once per N minutes for small N.
132
+ // The bound is `range.max - range.min + 1` so e.g. minutes (0-59)
133
+ // accepts step up to 60 (inclusive — `*/60` is "minute 0 of every
134
+ // hour" written with a redundant step).
135
+ var rangeSize = range.max - range.min + 1;
136
+ if (step > rangeSize) {
137
+ throw new SchedulerError("scheduler/invalid-cron",
138
+ "step '" + stepStr + "' exceeds field range (" + rangeSize +
139
+ ") in cron field '" + range.name + "'", true);
140
+ }
126
141
  part = part.slice(0, stepIdx);
127
142
  }
128
143
  var lo, hi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.64",
3
+ "version": "0.6.66",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:e636e4f9-23c0-41e8-8e78-f898e223c0e7",
5
+ "serialNumber": "urn:uuid:cfffbb8d-56d9-4859-af94-b238e781b705",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-03T14:18:03.806Z",
8
+ "timestamp": "2026-05-03T14:41:25.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.6.64",
22
+ "bom-ref": "@blamejs/core@0.6.66",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.64",
25
+ "version": "0.6.66",
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.6.64",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.66",
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.6.64",
57
+ "ref": "@blamejs/core@0.6.66",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]