@blamejs/core 0.18.54 → 0.18.55

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/lib/mcp.js CHANGED
@@ -479,7 +479,12 @@ function _toolResultSanitize(result, opts) {
479
479
  "toolResult.sanitize: posture must be 'refuse' | 'sanitize' | 'audit-only'");
480
480
  }
481
481
  var maxBytes = opts.maxTextBytes || DEFAULT_TOOL_OUTPUT_MAX_BYTES;
482
- var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : [];
482
+ // `null` when no allowlist was supplied, the array when one was — including
483
+ // an EMPTY one, which asks that no URL be permitted. Normalising the absent
484
+ // case to `[]` made the two indistinguishable, and the length test below then
485
+ // read both as "no restriction". A tool result is attacker-influenced
486
+ // content, so this is the allowlist that stops one pointing wherever it likes.
487
+ var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : null;
483
488
  if (!result || typeof result !== "object") {
484
489
  throw new McpError("mcp/bad-tool-result",
485
490
  "toolResult.sanitize: result must be an object");
@@ -522,7 +527,7 @@ function _toolResultSanitize(result, opts) {
522
527
  cleaned.push({ type: "text", text: t });
523
528
  } else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
524
529
  var url = block.url || (block.resource && block.resource.uri);
525
- if (typeof url === "string" && url.length > 0 && allowedHosts.length > 0) {
530
+ if (typeof url === "string" && url.length > 0 && allowedHosts) {
526
531
  var u; try { u = new URL(url); } catch (_e) { u = null; } // allow:raw-new-url-parse-only — operator-supplied tool URL; allowlist enforced below
527
532
  if (!u || allowedHosts.indexOf(u.host) === -1) {
528
533
  issues.push({ kind: "off-allowlist-url", index: i, url: url });
@@ -950,7 +955,10 @@ function _samplingGuard(opts) {
950
955
  function _elicitationGuard(opts) {
951
956
  opts = opts || {};
952
957
  var maxBytes = opts.maxMessageBytes || (8 * 1024); // allow:raw-byte-literal — 8 KiB elicitation message cap
953
- var allowedSchemaTypes = Array.isArray(opts.allowedSchemaTypes) && opts.allowedSchemaTypes.length > 0
958
+ // The default applies when the option is OMITTED. An explicitly empty list
959
+ // says no schema type is acceptable, and falling back to the default there
960
+ // would answer a caller's "none" with "object".
961
+ var allowedSchemaTypes = Array.isArray(opts.allowedSchemaTypes)
954
962
  ? opts.allowedSchemaTypes : ["object"];
955
963
  var posture = opts.posture || "refuse";
956
964
 
@@ -272,7 +272,14 @@ function create(opts) {
272
272
  });
273
273
  }
274
274
  }
275
- if (allowList && allowList.length > 0 && !bCrypto().isCertRevoked(peerCert.raw, allowList)) {
275
+ // Gated on "was an allowlist supplied", NOT on "is it non-empty". The
276
+ // derivation above already spells the two apart — `null` when the option is
277
+ // omitted, an array when it is given — and re-testing the length here
278
+ // collapsed them, so a pin built from configuration that returned zero
279
+ // entries admitted every client certificate instead of none. An allowlist
280
+ // that disappears when empty is a firewall rule set that opens when the
281
+ // last rule is deleted, and here that rule set is the front door.
282
+ if (allowList && !bCrypto().isCertRevoked(peerCert.raw, allowList)) {
276
283
  return _refuse(req, res, "fingerprint-not-allowed", {
277
284
  fingerprint: fp.colon,
278
285
  subject: (peerCert.subject && peerCert.subject.CN) || null,
@@ -549,6 +549,23 @@ function _stripUnreachableCertCompression(merged, caller) {
549
549
  return merged;
550
550
  }
551
551
 
552
+ // The key-agreement preference alone, as the string node:tls reads under
553
+ // `ecdhCurve`. `applyToContext` is the whole-context form and also merges the
554
+ // framework trust store, minimum version and certificate compression; a caller
555
+ // that only wants the group policy — a listener building its own server context
556
+ // — takes this instead, so it does not inherit a `ca` list it never asked for.
557
+ //
558
+ // An override is honoured, and a malformed one is REFUSED rather than replaced:
559
+ // quietly substituting the default would start a listener on groups the
560
+ // operator did not choose, with nothing said. Absent takes the framework
561
+ // default (the ML-KEM hybrids with a classical X25519 fallback).
562
+ function keyAgreementGroups(override, where) {
563
+ if (override !== undefined && override !== null) {
564
+ return _groupPreferenceString(override, where || "tls.keyAgreementGroups");
565
+ }
566
+ return STATE.tlsKeyShares.length > 0 ? STATE.tlsKeyShares.join(":") : null;
567
+ }
568
+
552
569
  function applyToContext(opts) {
553
570
  opts = opts || {};
554
571
  validateOpts(opts, ["base"], "tls.applyToContext");
@@ -4138,6 +4155,7 @@ module.exports = {
4138
4155
  captureBaselineFingerprints: captureBaselineFingerprints,
4139
4156
  detectBaselineDrift: detectBaselineDrift,
4140
4157
  applyToContext: applyToContext,
4158
+ keyAgreementGroups: keyAgreementGroups,
4141
4159
  buildOptions: buildOptions,
4142
4160
  getCaPems: getCaPems,
4143
4161
  ocsp: ocsp,
@@ -81,6 +81,22 @@ var SafeMountInfoError = defineClass("SafeMountInfoError", { alwaysPermanent: tr
81
81
 
82
82
  var DEFAULT_PATH = "/proc/self/mountinfo";
83
83
 
84
+ // The default path is a Linux path, and reaching for it anywhere else is not
85
+ // merely useless. A leading slash is DRIVE-relative on Windows, so the read
86
+ // lands on C:\proc\self\mountinfo — a location an unprivileged local user can
87
+ // create, because the default ACL on C:\ grants Authenticated Users the right
88
+ // to add subdirectories. Whoever plants that file authors the entries
89
+ // bestMatch() and isBindMount() answer from, which is to say they choose what
90
+ // the framework believes about which filesystem a path is on.
91
+ //
92
+ // So off Linux there is no default: a caller who genuinely has a mountinfo
93
+ // file elsewhere still passes opts.path, and everyone else is refused rather
94
+ // than answered from a file anyone could have written. Platform is a parameter
95
+ // so the Windows branch is reachable from a Linux CI host.
96
+ function _defaultPathFor(platform) {
97
+ return platform === "linux" ? DEFAULT_PATH : null;
98
+ }
99
+
84
100
  /**
85
101
  * @primitive b.safeMountInfo.parse
86
102
  * @signature b.safeMountInfo.parse(text, opts?)
@@ -187,12 +203,20 @@ function parse(text, opts) {
187
203
  * @related b.safeMountInfo.parse, b.safeMountInfo.bestMatch
188
204
  *
189
205
  * Read + parse `/proc/self/mountinfo` in one call. Returns the same
190
- * array shape as `parse(text)`. On non-Linux platforms (where /proc
191
- * doesn't exist) returns `opts.fallback` (default `null`); audit
192
- * emission per `safe-mount-info.refused` with code `read-failed`.
206
+ * array shape as `parse(text)`.
207
+ *
208
+ * The default path is used on Linux only. Elsewhere there is no default and
209
+ * `opts.fallback` (default `null`) is returned without reading anything, with
210
+ * an audit emission of `safe-mount-info.refused` carrying code
211
+ * `no-default-path`. `/proc` is not simply absent off Linux — a leading slash
212
+ * is drive-relative on Windows, so the default names `C:\proc\self\mountinfo`,
213
+ * which an unprivileged local user can create; reading it would let whoever
214
+ * wrote it decide which filesystem a path appears to be on. A caller holding a
215
+ * mountinfo file elsewhere passes `opts.path`, which is honored on every
216
+ * platform and reports a read failure as `read-failed`.
193
217
  *
194
218
  * @opts
195
- * path: string, // override path (default /proc/self/mountinfo)
219
+ * path: string, // override path; on Linux defaults to /proc/self/mountinfo, elsewhere there is no default
196
220
  * fallback: any, // returned on read failure (default null)
197
221
  * audit: object, // optional b.audit handle for refusal events
198
222
  * strict: boolean, // forwarded to parse()
@@ -211,12 +235,20 @@ function read(opts) {
211
235
  "safeMountInfo.read");
212
236
  var path = typeof opts.path === "string" && opts.path.length > 0
213
237
  ? opts.path
214
- : DEFAULT_PATH;
238
+ : _defaultPathFor(process.platform);
239
+ if (!path) {
240
+ _refuseEmit(opts, "safe-mount-info/no-default-path",
241
+ "/proc/self/mountinfo is a Linux path and this host is " + process.platform +
242
+ " — pass opts.path to name a mountinfo file explicitly.");
243
+ return ("fallback" in opts) ? opts.fallback : null;
244
+ }
215
245
  var text;
216
246
  try { text = nodeFs.readFileSync(path, "utf8"); }
217
247
  catch (e) {
248
+ // Name the file that was actually read. A caller who passed opts.path and
249
+ // is told "/proc/self/mountinfo unreadable" is looking at the wrong file.
218
250
  _refuseEmit(opts, "safe-mount-info/read-failed",
219
- "/proc/self/mountinfo unreadable: " + ((e && e.message) || String(e)));
251
+ path + " unreadable: " + ((e && e.message) || String(e)));
220
252
  return ("fallback" in opts) ? opts.fallback : null;
221
253
  }
222
254
  return parse(text, opts);
@@ -305,4 +337,5 @@ module.exports = {
305
337
  isBindMount: isBindMount,
306
338
  SafeMountInfoError: SafeMountInfoError,
307
339
  DEFAULT_PATH: DEFAULT_PATH,
340
+ _defaultPathForForTest: _defaultPathFor,
308
341
  };
package/lib/safe-smtp.js CHANGED
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  var { defineClass } = require("./framework-error");
42
+ var guardSmtpCommand = require("./guard-smtp-command");
42
43
 
43
44
  var SafeSmtpError = defineClass("SafeSmtpError", { alwaysPermanent: true });
44
45
 
@@ -110,7 +111,14 @@ function dotUnstuff(buf) {
110
111
  }
111
112
  var out = Buffer.alloc(buf.length);
112
113
  var oi = 0;
113
- for (var i = 0; i < buf.length; i += 1) {
114
+ // Offset 0 IS a line start. RFC 5321 §4.5.2 has the sender stuff a leading
115
+ // dot on any line of the body and grants the first line no exemption, but
116
+ // this only ever looked for a dot AFTER a CRLF — so a body beginning
117
+ // `..signature` was delivered with the extra dot still on it, and dotStuff,
118
+ // which does stuff offset 0, had a pair that would not undo it.
119
+ var start = 0;
120
+ if (buf.length >= 2 && buf[0] === 0x2e && buf[1] === 0x2e) start = 1;
121
+ for (var i = start; i < buf.length; i += 1) {
114
122
  out[oi++] = buf[i];
115
123
  // After \r\n, if the next byte is `.` followed by another non-CR
116
124
  // byte (i.e., not the terminator itself), strip the stuffing dot.
@@ -179,9 +187,96 @@ function dotStuff(buf) {
179
187
  return out.subarray(0, oi);
180
188
  }
181
189
 
190
+ /**
191
+ * @primitive b.safeSmtp.createBodyScanner
192
+ * @signature b.safeSmtp.createBodyScanner()
193
+ * @since 0.18.55
194
+ * @status stable
195
+ * @related b.safeSmtp.findDotTerminator, b.guardSmtpCommand.detectBodySmuggling
196
+ *
197
+ * Watch a DATA body for its terminator and for the smuggling shape as it
198
+ * ARRIVES, in work proportional to each chunk rather than to everything
199
+ * received so far.
200
+ *
201
+ * A listener that answers "has the body ended yet?" by re-deriving the whole
202
+ * accumulated buffer on every chunk does O(n) work per chunk and O(n²) over the
203
+ * message. The byte cap still holds — the collector refuses a chunk that would
204
+ * cross it — but processor cost is not what a byte cap bounds, so a message
205
+ * inside the limit could still cost minutes. Measured before this existed:
206
+ * 1 MiB accepted in 143 ms and 8 MiB in 4949 ms, a growth of 4.67x for each
207
+ * doubling where linear would hold at 2.
208
+ *
209
+ * Each `push` scans only the new bytes plus a four-byte overlap from the
210
+ * previous chunk. Four is exact, not generous: the longest shape either screen
211
+ * matches spans five bytes (`\r\n.\r\n`, and the smuggling scan's widest
212
+ * lookahead), and a pattern of length L needs L-1 bytes of overlap to be caught
213
+ * across a boundary.
214
+ *
215
+ * @example
216
+ * var scan = b.safeSmtp.createBodyScanner();
217
+ * var seen = scan.push(chunk);
218
+ * if (seen.smuggling) refuse();
219
+ * if (seen.terminatorAt !== -1) finish(collector.result(), seen.terminatorAt);
220
+ */
221
+ function createBodyScanner() {
222
+ var OVERLAP = 4; // longest matched shape (5) minus one
223
+ var consumed = 0; // bytes pushed before the current chunk
224
+ var tail = Buffer.alloc(0); // last OVERLAP bytes of what came before
225
+ var sawSmuggling = false;
226
+ var terminatorAt = -1;
227
+ // The byte immediately before `tail`, which the next window will not contain.
228
+ // Classifying a leading LF needs it: overlap gives every other offset its
229
+ // predecessor, but never offset zero, and widening the overlap only changes
230
+ // which byte is stranded there.
231
+ var beforeTail = -1;
232
+
233
+ return {
234
+ push: function (chunk) {
235
+ if (!Buffer.isBuffer(chunk)) {
236
+ throw new SafeSmtpError("safe-smtp/bad-input",
237
+ "createBodyScanner.push: chunk must be a Buffer");
238
+ }
239
+ // Once a verdict is reached it stands: the terminator does not move and a
240
+ // smuggling shape does not un-happen. Later chunks are still counted so
241
+ // the reported offset stays absolute.
242
+ var window = tail.length === 0 ? chunk : Buffer.concat([tail, chunk]);
243
+ var windowStart = consumed - tail.length;
244
+
245
+ // `windowStart === 0`, NOT `consumed === 0`: with the overlap carried in,
246
+ // a window can still begin at body offset 0 after bytes have been
247
+ // consumed. One-byte chunks are the case — the first push is a lone `.`
248
+ // (too short to match), and the second window is `.\n` starting at 0 with
249
+ // one byte already consumed. Testing `consumed` there skipped the
250
+ // dot-at-body-start shape entirely.
251
+ if (!sawSmuggling &&
252
+ guardSmtpCommand.detectBodySmuggling(window, windowStart === 0,
253
+ beforeTail === 0x0d)) {
254
+ sawSmuggling = true;
255
+ }
256
+ if (terminatorAt === -1) {
257
+ var at = findDotTerminator(window);
258
+ if (at !== -1) terminatorAt = windowStart + at;
259
+ }
260
+
261
+ consumed += chunk.length;
262
+ // Capture the byte the next window will drop off its front, before the
263
+ // window itself goes out of scope. When the window is no longer than the
264
+ // overlap nothing is dropped, and the next window still starts at body
265
+ // offset 0, where there is no preceding byte to carry.
266
+ beforeTail = window.length > OVERLAP ? window[window.length - OVERLAP - 1] : -1;
267
+ tail = window.length <= OVERLAP ? window : window.subarray(window.length - OVERLAP);
268
+ return { smuggling: sawSmuggling, terminatorAt: terminatorAt };
269
+ },
270
+ // What the scanner has been shown, so a caller can check its own accounting
271
+ // against this one rather than keeping a second counter.
272
+ bytesSeen: function () { return consumed; },
273
+ };
274
+ }
275
+
182
276
  module.exports = {
183
277
  findDotTerminator: findDotTerminator,
184
278
  dotUnstuff: dotUnstuff,
185
279
  dotStuff: dotStuff,
280
+ createBodyScanner: createBodyScanner,
186
281
  SafeSmtpError: SafeSmtpError,
187
282
  };
package/lib/safe-url.js CHANGED
@@ -312,7 +312,11 @@ function _normalizePctPath(path) {
312
312
  */
313
313
  function parse(url, opts) {
314
314
  opts = opts || {};
315
- var allowed = Array.isArray(opts.allowedProtocols) && opts.allowedProtocols.length > 0
315
+ // The documented default applies when the option is OMITTED. An explicitly
316
+ // empty list is a caller saying no protocol is acceptable, and widening that
317
+ // back to https: answers "none" with "one" — an allowlist that disappears
318
+ // when empty is a firewall rule set that opens when the last rule is deleted.
319
+ var allowed = Array.isArray(opts.allowedProtocols)
316
320
  ? opts.allowedProtocols
317
321
  : ALLOW_HTTP_TLS;
318
322
  var errClass = opts.errorClass;
@@ -531,7 +535,9 @@ function format(url) {
531
535
  */
532
536
  function canonicalize(input, opts) {
533
537
  opts = opts || {};
534
- var allowedSchemes = Array.isArray(opts.allowedSchemes) && opts.allowedSchemes.length > 0
538
+ // Same reading as parse above: the default is for the OMITTED case. An empty
539
+ // list asked for nothing and used to receive all four schemes.
540
+ var allowedSchemes = Array.isArray(opts.allowedSchemes)
535
541
  ? opts.allowedSchemes
536
542
  : ALLOW_ANY;
537
543
 
@@ -231,7 +231,10 @@ function _validatePollOpts(opts) {
231
231
  throw new SelfUpdateError("selfupdate/bad-releases-url",
232
232
  "selfUpdate.poll: opts.releasesUrl is not parseable as a URL");
233
233
  }
234
- var allowedProtocols = Array.isArray(opts.allowedProtocols) && opts.allowedProtocols.length > 0
234
+ // The documented default is for the OMITTED case. An explicitly empty
235
+ // list says no protocol may be dialled, and widening it back to https:
236
+ // answers "none" with "one".
237
+ var allowedProtocols = Array.isArray(opts.allowedProtocols)
235
238
  ? opts.allowedProtocols.slice() : ["https:"];
236
239
  if (allowedProtocols.indexOf(parsedProto) === -1) {
237
240
  throw new SelfUpdateError("selfupdate/bad-releases-url",
@@ -21,7 +21,7 @@
21
21
  "server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
22
22
  "browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
23
23
  },
24
- "refreshedAt": "2026-08-24T23:54:10.282Z"
24
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
25
25
  },
26
26
  "@noble/hashes": {
27
27
  "version": "2.3.0",
@@ -48,7 +48,7 @@
48
48
  "hashes": {
49
49
  "browser": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0"
50
50
  },
51
- "refreshedAt": "2026-08-24T23:54:10.282Z"
51
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
52
52
  },
53
53
  "@noble/curves": {
54
54
  "version": "2.3.0",
@@ -70,7 +70,7 @@
70
70
  "hashes": {
71
71
  "server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
72
72
  },
73
- "refreshedAt": "2026-08-24T23:54:10.282Z",
73
+ "refreshedAt": "2026-08-25T10:45:22.083Z",
74
74
  "components": {
75
75
  "@noble/hashes": {
76
76
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -114,7 +114,7 @@
114
114
  "server": "sha256:fab7ebe5737793862c473444f4ee5912f79dd1edec86683acbb4eecbca0f5892",
115
115
  "browser": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9"
116
116
  },
117
- "refreshedAt": "2026-08-24T23:54:10.282Z",
117
+ "refreshedAt": "2026-08-25T10:45:22.083Z",
118
118
  "components": {
119
119
  "@noble/hashes": {
120
120
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -148,7 +148,7 @@
148
148
  },
149
149
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
150
150
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
151
- "refreshedAt": "2026-08-24T23:54:10.282Z"
151
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
152
152
  },
153
153
  "bimi-trust-anchors": {
154
154
  "version": "operator-managed",
@@ -173,7 +173,7 @@
173
173
  },
174
174
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
175
175
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
176
- "refreshedAt": "2026-08-24T23:54:10.282Z"
176
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
177
177
  },
178
178
  "publicsuffix-list": {
179
179
  "version": "master",
@@ -193,10 +193,10 @@
193
193
  },
194
194
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
195
195
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
196
- "refreshedAt": "2026-08-24T23:54:10.282Z"
196
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
197
197
  },
198
198
  "@blamejs/pki": {
199
- "version": "0.5.30",
199
+ "version": "0.5.31",
200
200
  "license": "Apache-2.0",
201
201
  "author": "blamejs",
202
202
  "source": "https://github.com/blamejs/pki",
@@ -216,12 +216,12 @@
216
216
  "server": "lib/vendor/blamejs-pki.cjs"
217
217
  },
218
218
  "bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
219
- "bundledAt": "2026-08-24T00:00:00Z",
220
- "cpe": "cpe:2.3:a:blamejs:pki:0.5.30:*:*:*:*:node.js:*:*",
219
+ "bundledAt": "2026-08-25T00:00:00Z",
220
+ "cpe": "cpe:2.3:a:blamejs:pki:0.5.31:*:*:*:*:node.js:*:*",
221
221
  "hashes": {
222
- "server": "sha256:fc8b1783ae40c377c1bc7afb7bd902b5282c165ed11354b803867f7d9f75f021"
222
+ "server": "sha256:8cc54fdfb7ac5277fb30f054f57500a9e77ce6b2d2b8f6846a14766d217770db"
223
223
  },
224
- "refreshedAt": "2026-08-24T23:54:10.282Z"
224
+ "refreshedAt": "2026-08-25T10:45:22.083Z"
225
225
  }
226
226
  }
227
227
  }