@blamejs/core 0.6.63 → 0.6.65

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.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.
12
+
13
+ - **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.
14
+
11
15
  - **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.
12
16
 
13
17
  - **0.6.62** (2026-05-03) — URL-length DoS-shape gap closed in two places. **`b.safeSchema.string().url()`** was regex-only with no length cap, accepting arbitrarily long matching strings — same DoS class as v0.6.61's `.email()` gap; an operator chaining `.url()` on a request body would feed multi-megabyte URLs into downstream HTTP clients / SSRF gates / log lines. Now rejects with `string/url-too-long` at 8193+ chars. **`b.safeUrl.parse`** had the same gap at the framework's primary URL-validation surface (used by httpClient + ssrfGuard + every operator-supplied URL). The 8 KB cap now applies BEFORE handing the string to Node's `new URL()` parser; operator-supplied URLs feeding `b.httpClient.request({ url })` from request bodies / webhook configs are bounded. Operators with legitimate non-standard use (proxies, tunnels with embedded payloads) override via `opts.maxUrlLength`. Both bounds reference RFC 7230 §3.1.1's 8000-octet recommendation. **Tests** — 4 new layer-0 assertions on `string().url()` (8 KB / 8193 / 100K / error code) + 4 on `safeUrl.parse` (8192 accept / 8193 reject / error code / `maxUrlLength` opt override). Smoke 7215 → 7222 / wiki e2e 178 / Linux 85.5s / per-primitive integration 16 files / eslint clean / shellcheck clean.
@@ -93,6 +93,13 @@ function _emitEvent(name, value, labels) {
93
93
  // to the larger size; operators pass `{ params: { length: 64 } }`
94
94
  // to opt into the SHA3-512-comparable byte count if they need it.
95
95
  var SHAKE256_DEFAULT_LENGTH = 128;
96
+ // Minimum payload length both hash() and verify() enforce. 16 bytes
97
+ // (128 bits) is the floor below which the digest's collision space
98
+ // becomes brute-forceable. The asymmetry between hash() (rejected < 16
99
+ // at config time) and verify() (silently accepted any length) let a
100
+ // truncated stored envelope verify with trivial work; both ends now
101
+ // refuse short payloads symmetrically.
102
+ var SHAKE256_MIN_LENGTH = 16;
96
103
 
97
104
  function _shake256(secret, length) {
98
105
  // crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
@@ -200,9 +207,11 @@ async function hash(secret, opts) {
200
207
 
201
208
  if (algoId === C.CRED_HASH_IDS.SHAKE256) {
202
209
  var length = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
203
- if (typeof length !== "number" || !isFinite(length) || length < 16 || Math.floor(length) !== length) {
210
+ if (typeof length !== "number" || !isFinite(length) ||
211
+ length < SHAKE256_MIN_LENGTH || Math.floor(length) !== length) {
204
212
  throw new CredentialHashError(
205
- "credentialHash.hash: SHAKE256 length must be an integer >= 16, got " + JSON.stringify(length),
213
+ "credentialHash.hash: SHAKE256 length must be an integer >= " +
214
+ SHAKE256_MIN_LENGTH + ", got " + JSON.stringify(length),
206
215
  "credential-hash/bad-opt");
207
216
  }
208
217
  var env = _envelope(algoId, _shake256(secret, length));
@@ -246,6 +255,19 @@ async function verify(secret, envelope) {
246
255
  var algoName = ID_TO_NAME[decoded.algoId];
247
256
 
248
257
  if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
258
+ // Enforce the same minimum payload length that hash() enforces (16
259
+ // bytes / 128 bits). Without this, a storage bug or attacker
260
+ // tampering that truncates the stored envelope to just a few bytes
261
+ // produces a hash with catastrophically reduced collision space —
262
+ // a 1-byte payload has only 256 possible values, brute-forceable
263
+ // in microseconds. The asymmetry between hash() (rejects < 16) and
264
+ // verify() (accepted anything) was the silent gap; both ends now
265
+ // refuse weak digests symmetrically.
266
+ if (decoded.payload.length < SHAKE256_MIN_LENGTH) {
267
+ _emitEvent("credentialHash.verify", 1,
268
+ { outcome: "failure", reason: "payload-too-short", algo: algoName });
269
+ return false;
270
+ }
249
271
  var expected = _shake256(secret, decoded.payload.length);
250
272
  var ok = crypto.timingSafeEqual(expected, decoded.payload);
251
273
  _emitEvent("credentialHash.verify", 1,
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.63",
3
+ "version": "0.6.65",
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:192ac8a0-4291-44dd-a03a-44af6b4cce65",
5
+ "serialNumber": "urn:uuid:e379d7a4-43ef-4578-b4ba-31b6f69125f5",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-03T14:04:51.705Z",
8
+ "timestamp": "2026-05-03T14:31:10.670Z",
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.63",
22
+ "bom-ref": "@blamejs/core@0.6.65",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.63",
25
+ "version": "0.6.65",
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.63",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.65",
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.63",
57
+ "ref": "@blamejs/core@0.6.65",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]