@blamejs/core 0.15.10 → 0.15.12

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
@@ -435,7 +435,28 @@ var PROMPT_INJECTION_MARKERS = [
435
435
  "</?(?:assistant|system|user|tool)>",
436
436
  ];
437
437
  var INJECTION_RE = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "i"); // allow:dynamic-regex — composed from the const PROMPT_INJECTION_MARKERS list above; not operator-supplied input
438
- var DANGEROUS_HTML_RE = /<script\b|<iframe\b|<object\b|<embed\b|javascript:/i;
438
+ // vbscript: and data:text/html are dangerous URL schemes the guard claims to
439
+ // neutralize but the original alternation omitted (CodeQL js/incomplete-url-
440
+ // scheme-check). data: is scoped to text/html so a benign data:image/png isn't
441
+ // over-redacted. The alternation stays linear (no nested quantifier).
442
+ var DANGEROUS_HTML_RE = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/i;
443
+
444
+ // Global variants for sanitize-mode redaction. A non-global .replace removes
445
+ // only the LEFTMOST match, so on `data:text/html,<script>alert(1)</script>`
446
+ // the leftmost `data:text/html` would be stripped and the executable
447
+ // `<script>` left behind — sanitize mode returning runnable HTML. _redactAll
448
+ // replaces EVERY dangerous token, repeating to a fixpoint in case removing one
449
+ // abuts two halves into a new one. Input byte-length is bounded by the caller,
450
+ // and [REDACTED] introduces no dangerous token, so the loop terminates quickly.
451
+ var INJECTION_RE_G = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "gi"); // allow:dynamic-regex — same const-composed source as INJECTION_RE
452
+ var DANGEROUS_HTML_RE_G = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/gi;
453
+
454
+ function _redactAll(text, globalRe) {
455
+ var out = text;
456
+ var prev;
457
+ do { prev = out; out = out.replace(globalRe, "[REDACTED]"); } while (out !== prev);
458
+ return out;
459
+ }
439
460
 
440
461
  function _toolResultSanitize(result, opts) {
441
462
  opts = opts || {};
@@ -475,14 +496,15 @@ function _toolResultSanitize(result, opts) {
475
496
  if (INJECTION_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
476
497
  issues.push({ kind: "prompt-injection", index: i });
477
498
  if (posture === "sanitize") {
478
- // Strip the injection marker line — operators wanting
479
- // structural redaction wire their own classifier.
480
- t = t.replace(INJECTION_RE, "[REDACTED]");
499
+ // Strip EVERY injection marker — operators wanting structural
500
+ // redaction wire their own classifier. Global fixpoint redact so a
501
+ // second marker after the first isn't left behind.
502
+ t = _redactAll(t, INJECTION_RE_G);
481
503
  }
482
504
  }
483
505
  if (DANGEROUS_HTML_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
484
506
  issues.push({ kind: "dangerous-html", index: i });
485
- if (posture === "sanitize") t = t.replace(DANGEROUS_HTML_RE, "[REDACTED]");
507
+ if (posture === "sanitize") t = _redactAll(t, DANGEROUS_HTML_RE_G);
486
508
  }
487
509
  cleaned.push({ type: "text", text: t });
488
510
  } else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
@@ -922,7 +944,7 @@ function _elicitationGuard(opts) {
922
944
  }
923
945
  if (posture === "sanitize") {
924
946
  return Object.assign({}, elicitRequest, {
925
- message: message.replace(INJECTION_RE, "[REDACTED]"),
947
+ message: _redactAll(message, INJECTION_RE_G),
926
948
  });
927
949
  }
928
950
  }
@@ -375,6 +375,19 @@ function _detectSmuggling(req) {
375
375
  return null;
376
376
  }
377
377
 
378
+ // Generic, operator-safe status phrase for a client error body — used when the
379
+ // thrown error is NOT a framework-classified BodyParserError (or is a 5xx), so
380
+ // an internal exception's message (fs errno + tmp path, a parse hook's thrown
381
+ // detail) is never echoed to the client (CWE-209 / CodeQL js/stack-trace-exposure).
382
+ var _GENERIC_REASON = {};
383
+ _GENERIC_REASON[HTTP_STATUS.BAD_REQUEST] = "Bad Request";
384
+ _GENERIC_REASON[HTTP_STATUS.PAYLOAD_TOO_LARGE] = "Payload Too Large";
385
+ _GENERIC_REASON[HTTP_STATUS.UNSUPPORTED_MEDIA_TYPE] = "Unsupported Media Type";
386
+ _GENERIC_REASON[HTTP_STATUS.INTERNAL_SERVER_ERROR] = "Internal Server Error";
387
+ function _genericReason(status) {
388
+ return _GENERIC_REASON[status] || (status >= 500 ? "Internal Server Error" : "Bad Request");
389
+ }
390
+
378
391
  function _writeError(res, status, message, code) {
379
392
  if (res.headersSent) return;
380
393
  var body = JSON.stringify({ error: message, code: code });
@@ -469,10 +482,13 @@ async function _parseJson(req, opts) {
469
482
  }
470
483
  if (typeof opts.parseHook === "function") {
471
484
  try { parsed = opts.parseHook(parsed); }
472
- catch (e) {
485
+ catch (_e) {
473
486
  throw new BodyParserError(
474
487
  "body-parser/json-hook",
475
- "JSON parseHook failed: " + ((e && e.message) || String(e)),
488
+ // Operator parseHook threw surface a fixed, safe message; the hook's
489
+ // own thrown detail (which may carry secrets) is the operator's to log,
490
+ // never echoed to the client (CWE-209).
491
+ "request body rejected by parse hook",
476
492
  true, HTTP_STATUS.BAD_REQUEST
477
493
  );
478
494
  }
@@ -1476,8 +1492,32 @@ function create(opts) {
1476
1492
  }
1477
1493
  var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
1478
1494
  var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
1479
- var message = (e && e.message) ? e.message : String(e);
1480
- _writeError(res, status, message, code);
1495
+ // Only a framework-classified 4xx BodyParserError carries a curated,
1496
+ // operator-safe message (malformed JSON, poisoned key, smuggling). Any
1497
+ // other thrown error — and every 5xx — gets a generic status phrase, so
1498
+ // an internal exception (fs errno + tmp path, a parse hook's thrown
1499
+ // secret) is never echoed to the client (CWE-209). The full diagnostic
1500
+ // stays on the audit chain, redacted by safeEmit.
1501
+ // Object(e) tolerates a non-object throw (throw null, or a string from
1502
+ // an operator parse hook) without a truthiness guard on the caught
1503
+ // binding — the caught value is always defined to CodeQL, so `e && …`
1504
+ // reads as a dead sub-condition; this keeps the null-safety explicit.
1505
+ var eo = Object(e);
1506
+ var clientMessage = (eo.isBodyParserError === true && status < 500 && typeof eo.message === "string")
1507
+ ? eo.message
1508
+ : _genericReason(status);
1509
+ try {
1510
+ audit().safeEmit({
1511
+ action: "body-parser.error",
1512
+ outcome: status >= 500 ? "failure" : "denied",
1513
+ metadata: {
1514
+ status: status,
1515
+ code: code,
1516
+ message: (e && e.message) ? String(e.message).slice(0, 256) : "",
1517
+ },
1518
+ });
1519
+ } catch (_e) { /* audit best-effort — never mask the response */ }
1520
+ _writeError(res, status, clientMessage, code);
1481
1521
  }
1482
1522
  };
1483
1523
  }
@@ -1501,9 +1541,11 @@ async function _parseJsonFromBuf(buf, opts) {
1501
1541
  }
1502
1542
  if (typeof opts.parseHook === "function") {
1503
1543
  try { parsed = opts.parseHook(parsed); }
1504
- catch (e) {
1544
+ catch (_e) {
1545
+ // Operator parseHook threw — fixed safe message; the hook's thrown
1546
+ // detail (possible secrets) is never echoed to the client (CWE-209).
1505
1547
  throw new BodyParserError("body-parser/json-hook",
1506
- "JSON parseHook failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
1548
+ "request body rejected by parse hook", true, HTTP_STATUS.BAD_REQUEST);
1507
1549
  }
1508
1550
  }
1509
1551
  return parsed;
@@ -29,6 +29,7 @@
29
29
  var defineClass = require("../framework-error").defineClass;
30
30
  var lazyRequire = require("../lazy-require");
31
31
  var validateOpts = require("../validate-opts");
32
+ var safeBuffer = require("../safe-buffer");
32
33
 
33
34
  var audit = lazyRequire(function () { return require("../audit"); });
34
35
 
@@ -140,11 +141,12 @@ function create(opts) {
140
141
  if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
141
142
  var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
142
143
  (typeof chunk === "string" ? chunk : "");
143
- // Inject after <body> opening tag if present, else after <html>
144
- // opening tag, else prepend.
145
- var bodyMatch = body.match(/<body[^>]*>/i);
146
- if (bodyMatch) {
147
- var idx = bodyMatch.index + bodyMatch[0].length;
144
+ // Inject after the <body> opening tag if present, else prepend.
145
+ // Linear tag-find — NOT body.match(/<body[^>]*>/i), which is O(n^2)
146
+ // in V8 on a body carrying many `<body` starts with no closing `>`
147
+ // (rendered user content can produce exactly that).
148
+ var idx = safeBuffer.indexAfterOpenTag(body, "body");
149
+ if (idx !== -1) {
148
150
  body = body.slice(0, idx) + "\n" + bannerHtml + "\n" + body.slice(idx);
149
151
  } else {
150
152
  body = bannerHtml + "\n" + body;
@@ -65,6 +65,7 @@
65
65
  */
66
66
 
67
67
  var validateOpts = require("../validate-opts");
68
+ var safeBuffer = require("../safe-buffer");
68
69
 
69
70
  // Per W3C draft + Chromium implementation. `immediate` triggers the
70
71
  // speculation as soon as the rules are seen; `conservative` waits
@@ -287,10 +288,11 @@ function create(opts) {
287
288
  if (headClose !== -1) {
288
289
  body = body.slice(0, headClose) + tag + body.slice(headClose);
289
290
  } else {
290
- var bodyOpen = body.match(/<body[^>]*>/i);
291
- if (bodyOpen) {
292
- var idx = bodyOpen.index + bodyOpen[0].length;
293
- body = body.slice(0, idx) + tag + body.slice(idx);
291
+ // Linear tag-find — NOT body.match(/<body[^>]*>/i) (O(n^2) in V8
292
+ // on a body with many `<body` starts and no closing `>`).
293
+ var bodyIdx = safeBuffer.indexAfterOpenTag(body, "body");
294
+ if (bodyIdx !== -1) {
295
+ body = body.slice(0, bodyIdx) + tag + body.slice(bodyIdx);
294
296
  } else {
295
297
  // No <head> or <body> — prepend so the rules at least
296
298
  // reach the parser. This matches the bot-disclose fallback.
@@ -149,7 +149,7 @@ function setServers(serverList) {
149
149
  throw new DnsError("dns/setservers-failed", "dns.setServers failed: " + e.message);
150
150
  }
151
151
  _clearCache();
152
- _emitObs("network.dns.servers.set", { count: serverList.length });
152
+ observability().safeEvent("network.dns.servers.set", 1, { count: serverList.length });
153
153
  }
154
154
 
155
155
  function getServers() {
@@ -169,7 +169,7 @@ function setResultOrder(order) {
169
169
  try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
170
170
  }
171
171
  _clearCache();
172
- _emitObs("network.dns.result_order.set", { order: order });
172
+ observability().safeEvent("network.dns.result_order.set", 1, { order: order });
173
173
  }
174
174
 
175
175
  function setFamily(fam) {
@@ -215,7 +215,7 @@ function useSystemResolver() {
215
215
  STATE.systemResolver = true;
216
216
  _resetDotPool();
217
217
  _clearCache();
218
- _emitObs("network.dns.system_resolver.set", {});
218
+ observability().safeEvent("network.dns.system_resolver.set", 1, {});
219
219
  }
220
220
 
221
221
  function useDnsOverHttps(opts) {
@@ -246,7 +246,7 @@ function useDnsOverHttps(opts) {
246
246
  }
247
247
  STATE.doh = { url: url, method: method, ca: opts.ca || null };
248
248
  _clearCache();
249
- _emitObs("network.dns.doh.set", { url: url, method: method || "auto" });
249
+ observability().safeEvent("network.dns.doh.set", 1, { url: url, method: method || "auto" });
250
250
  }
251
251
 
252
252
  function useDnsOverTls(opts) {
@@ -267,7 +267,7 @@ function useDnsOverTls(opts) {
267
267
  };
268
268
  _resetDotPool();
269
269
  _clearCache();
270
- _emitObs("network.dns.dot.set", { host: STATE.dot.host, port: STATE.dot.port });
270
+ observability().safeEvent("network.dns.dot.set", 1, { host: STATE.dot.host, port: STATE.dot.port });
271
271
  }
272
272
 
273
273
  function _withTimeout(promise, ms, host) {
@@ -1178,13 +1178,13 @@ async function _querySvcbLike(host, qtype, opts) {
1178
1178
  throw new DnsError("dns/bad-transport",
1179
1179
  "dns.querySvcb: transport must be 'doh' | 'dot' | 'system' | undefined");
1180
1180
  }
1181
- _emitObs("network.dns.svcb.requested", { qtype: qtype, transport: opts.transport || "auto" });
1181
+ observability().safeEvent("network.dns.svcb.requested", 1, { qtype: qtype, transport: opts.transport || "auto" });
1182
1182
  var startMs = _now();
1183
1183
  var reply;
1184
1184
  try {
1185
1185
  reply = await _rawQuery(host, qtype, opts.transport);
1186
1186
  } catch (e) {
1187
- _emitObs("network.dns.svcb.failure", {
1187
+ observability().safeEvent("network.dns.svcb.failure", 1, {
1188
1188
  latencyMs: _now() - startMs,
1189
1189
  code: e.code || "unknown",
1190
1190
  });
@@ -1198,7 +1198,7 @@ async function _querySvcbLike(host, qtype, opts) {
1198
1198
  records.push(_parseSvcbRdata(decoded.msg, ans.rdataOff, ans.rdlen));
1199
1199
  }
1200
1200
  records.sort(function (a, b) { return a.priority - b.priority; });
1201
- _emitObs("network.dns.svcb.success", {
1201
+ observability().safeEvent("network.dns.svcb.success", 1, {
1202
1202
  latencyMs: _now() - startMs,
1203
1203
  count: records.length,
1204
1204
  qtype: qtype,
@@ -1322,7 +1322,7 @@ async function discoverEncrypted(opts) {
1322
1322
  try {
1323
1323
  records = await _querySvcbLike(name, DNS_QTYPE_SVCB, { transport: transport });
1324
1324
  } catch (e) {
1325
- _emitObs("network.dns.ddr.failure", {
1325
+ observability().safeEvent("network.dns.ddr.failure", 1, {
1326
1326
  latencyMs: _now() - startMs,
1327
1327
  code: e.code || "unknown",
1328
1328
  });
@@ -1333,7 +1333,7 @@ async function discoverEncrypted(opts) {
1333
1333
  throw e;
1334
1334
  }
1335
1335
  if (records.length === 0) {
1336
- _emitObs("network.dns.ddr.empty", { latencyMs: _now() - startMs });
1336
+ observability().safeEvent("network.dns.ddr.empty", 1, { latencyMs: _now() - startMs });
1337
1337
  throw new DnsError("dns/ddr-not-discovered",
1338
1338
  "dns.discoverEncrypted: resolver returned empty DDR record set at " + name);
1339
1339
  }
@@ -1364,7 +1364,7 @@ async function discoverEncrypted(opts) {
1364
1364
  throw new DnsError("dns/ddr-not-discovered",
1365
1365
  "dns.discoverEncrypted: DDR records present but none advertised a recognized transport (alpn=dot/h2/h3)");
1366
1366
  }
1367
- _emitObs("network.dns.ddr.success", {
1367
+ observability().safeEvent("network.dns.ddr.success", 1, {
1368
1368
  latencyMs: _now() - startMs,
1369
1369
  count: resolvers.length,
1370
1370
  });
@@ -1454,7 +1454,7 @@ function useDesignatedResolvers(list) {
1454
1454
  });
1455
1455
  }
1456
1456
  _designatedResolvers = validated.slice();
1457
- _emitObs("network.dns.dnr.set", {
1457
+ observability().safeEvent("network.dns.dnr.set", 1, {
1458
1458
  count: validated.length,
1459
1459
  active: j,
1460
1460
  transport: v.transport,
@@ -1462,7 +1462,7 @@ function useDesignatedResolvers(list) {
1462
1462
  return { active: j, count: validated.length };
1463
1463
  } catch (e) {
1464
1464
  lastErr = e;
1465
- _emitObs("network.dns.dnr.entry_failed", {
1465
+ observability().safeEvent("network.dns.dnr.entry_failed", 1, {
1466
1466
  index: j,
1467
1467
  transport: v.transport,
1468
1468
  code: e.code || "unknown",
@@ -1511,7 +1511,7 @@ async function lookup(host, opts) {
1511
1511
  if (cached.error) throw cached.error;
1512
1512
  return opts.all ? cached.value : cached.value[0];
1513
1513
  }
1514
- _emitObs("network.dns.lookup.requested", { family: cacheKey });
1514
+ observability().safeEvent("network.dns.lookup.requested", 1, { family: cacheKey });
1515
1515
  var startMs = _now();
1516
1516
  // Resolve secure-DNS default on first use. Idempotent.
1517
1517
  _ensureSecureDefault();
@@ -1546,11 +1546,11 @@ async function lookup(host, opts) {
1546
1546
  throw new DnsError("dns/no-result", "dns lookup of '" + host + "' returned no addresses");
1547
1547
  }
1548
1548
  _cachePutPositive(host, cacheKey, normalized);
1549
- _emitObs("network.dns.lookup.success", { latencyMs: _now() - startMs, count: normalized.length });
1549
+ observability().safeEvent("network.dns.lookup.success", 1, { latencyMs: _now() - startMs, count: normalized.length });
1550
1550
  return opts.all ? normalized : normalized[0];
1551
1551
  } catch (e) {
1552
1552
  _cachePutNegative(host, cacheKey, e);
1553
- _emitObs("network.dns.lookup.failure", { latencyMs: _now() - startMs, code: e.code || "unknown" });
1553
+ observability().safeEvent("network.dns.lookup.failure", 1, { latencyMs: _now() - startMs, code: e.code || "unknown" });
1554
1554
  throw e;
1555
1555
  }
1556
1556
  }
@@ -1571,7 +1571,7 @@ async function _resolveProtocol(host, family, opts) {
1571
1571
  throw new DnsError("dns/bad-transport",
1572
1572
  "dns.resolve" + family + ": transport must be 'doh' | 'dot' | 'system' | undefined");
1573
1573
  }
1574
- _emitObs("network.dns.resolve.requested", { family: family, transport: opts.transport || "auto" });
1574
+ observability().safeEvent("network.dns.resolve.requested", 1, { family: family, transport: opts.transport || "auto" });
1575
1575
  var startMs = _now();
1576
1576
  try {
1577
1577
  var addrs;
@@ -1597,10 +1597,10 @@ async function _resolveProtocol(host, family, opts) {
1597
1597
  if (normalized.length === 0) {
1598
1598
  throw new DnsError("dns/no-result", "dns.resolve" + family + " of '" + host + "' returned no addresses");
1599
1599
  }
1600
- _emitObs("network.dns.resolve.success", { family: family, latencyMs: _now() - startMs, count: normalized.length });
1600
+ observability().safeEvent("network.dns.resolve.success", 1, { family: family, latencyMs: _now() - startMs, count: normalized.length });
1601
1601
  return normalized;
1602
1602
  } catch (e) {
1603
- _emitObs("network.dns.resolve.failure", { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
1603
+ observability().safeEvent("network.dns.resolve.failure", 1, { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
1604
1604
  if (e instanceof DnsError) throw e;
1605
1605
  throw new DnsError("dns/resolve-failed",
1606
1606
  "dns.resolve" + family + " of '" + host + "' failed: " + (e.message || String(e)));
@@ -1643,16 +1643,16 @@ async function reverse(ip) {
1643
1643
  throw new DnsError("dns/bad-ip",
1644
1644
  "dns.reverse: '" + ip + "' is not a valid IPv4 or IPv6 address");
1645
1645
  }
1646
- _emitObs("network.dns.reverse.requested", { family: net.isIPv6(ip) ? 6 : 4 });
1646
+ observability().safeEvent("network.dns.reverse.requested", 1, { family: net.isIPv6(ip) ? 6 : 4 });
1647
1647
  var startMs = _now();
1648
1648
  try {
1649
1649
  var ptrs = await _withTimeout(dnsPromises.reverse(ip), STATE.lookupTimeoutMs, ip);
1650
- _emitObs("network.dns.reverse.success", {
1650
+ observability().safeEvent("network.dns.reverse.success", 1, {
1651
1651
  latencyMs: _now() - startMs, count: Array.isArray(ptrs) ? ptrs.length : 0,
1652
1652
  });
1653
1653
  return Array.isArray(ptrs) ? ptrs : [];
1654
1654
  } catch (e) {
1655
- _emitObs("network.dns.reverse.failure", {
1655
+ observability().safeEvent("network.dns.reverse.failure", 1, {
1656
1656
  latencyMs: _now() - startMs, code: e.code || "unknown",
1657
1657
  });
1658
1658
  if (e instanceof DnsError) throw e;
@@ -1674,10 +1674,6 @@ function nodeLookup(host, options, callback) {
1674
1674
  );
1675
1675
  }
1676
1676
 
1677
- function _emitObs(name, fields) {
1678
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
1679
- }
1680
-
1681
1677
  function _stateForTest() { return STATE; }
1682
1678
  function _resetForTest() {
1683
1679
  STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
@@ -259,7 +259,7 @@ function statuses() {
259
259
 
260
260
  function _emitObsProbe(entry, result) {
261
261
  try {
262
- observability().emit("network.heartbeat.probe", {
262
+ observability().safeEvent("network.heartbeat.probe", 1, {
263
263
  name: entry.target.name,
264
264
  type: entry.target.type,
265
265
  ok: result.ok,
@@ -381,7 +381,7 @@ function passive(opts) {
381
381
 
382
382
  function _emitObsPong(state) {
383
383
  try {
384
- observability().emit("network.heartbeat.passive.pong", {
384
+ observability().safeEvent("network.heartbeat.passive.pong", 1, {
385
385
  pongCount: state.pongCount,
386
386
  timeoutMs: state.timeoutMs,
387
387
  });
@@ -390,7 +390,7 @@ function _emitObsPong(state) {
390
390
 
391
391
  function _emitObsTimeout(state) {
392
392
  try {
393
- observability().emit("network.heartbeat.passive.timeout", {
393
+ observability().safeEvent("network.heartbeat.passive.timeout", 1, {
394
394
  pongCount: state.pongCount,
395
395
  lastPongMs: state.lastPongMs,
396
396
  timeoutMs: state.timeoutMs,
@@ -70,7 +70,7 @@ function set(opts) {
70
70
  if (opts.no !== undefined) STATE.noProxy = _parseNoProxy(opts.no);
71
71
  if (opts.auth !== undefined) STATE.auth = opts.auth ? _parseAuth(opts.auth) : null;
72
72
  STATE.agentCache.clear();
73
- _emitObs("network.proxy.set", {
73
+ observability().safeEvent("network.proxy.set", 1, {
74
74
  httpSet: !!STATE.http,
75
75
  httpsSet: !!STATE.https,
76
76
  noProxyCount: STATE.noProxy.length,
@@ -93,7 +93,7 @@ function fromEnv(envObj) {
93
93
  if (authEnv) { STATE.auth = _parseAuth(authEnv); changed = true; }
94
94
  if (changed) {
95
95
  STATE.agentCache.clear();
96
- _emitObs("network.proxy.from_env", {
96
+ observability().safeEvent("network.proxy.from_env", 1, {
97
97
  httpSet: !!STATE.http,
98
98
  httpsSet: !!STATE.https,
99
99
  noProxyCount: STATE.noProxy.length,
@@ -255,7 +255,7 @@ function agentFor(targetUrl) {
255
255
  };
256
256
  }
257
257
  STATE.agentCache.set(key, agent);
258
- _emitObs("network.proxy.agent.created", { protocol: u.protocol });
258
+ observability().safeEvent("network.proxy.agent.created", 1, { protocol: u.protocol });
259
259
  return agent;
260
260
  }
261
261
 
@@ -268,10 +268,6 @@ function snapshot() {
268
268
  };
269
269
  }
270
270
 
271
- function _emitObs(name, fields) {
272
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
273
- }
274
-
275
271
  function _resetForTest() {
276
272
  STATE.http = null; STATE.https = null; STATE.noProxy = []; STATE.auth = null;
277
273
  STATE.agentCache.clear();
@@ -23,6 +23,29 @@ var networkDns = lazyRequire(function () { return require("./network-dns"); });
23
23
  var httpClient = lazyRequire(function () { return require("./http-client"); });
24
24
  var asn1 = require("./asn1-der");
25
25
 
26
+ // Audit + observability emit for an outbound TLS connection that runs with
27
+ // peer-certificate validation DISABLED (an explicit operator opt-in —
28
+ // rejectUnauthorized:false / allowInsecure — never a framework default).
29
+ // Emitted at the point the disable is HONORED so the degraded posture is
30
+ // observable (compliance evidence + incident response), parallel to the
31
+ // tls.classical_downgrade audit. Drop-silent best-effort (§8 hot-path sink) —
32
+ // an audit-sink failure must never break the TLS connect itself.
33
+ function auditInsecureTls(meta) {
34
+ meta = meta || {};
35
+ try {
36
+ observability().safeEvent("tls.insecure_skip_verify", 1, {
37
+ host: meta.host || null, port: meta.port || null, source: meta.source || null,
38
+ });
39
+ } catch (_e) { /* drop-silent */ }
40
+ try {
41
+ audit().safeEmit({
42
+ action: "tls.insecure_skip_verify",
43
+ outcome: "success",
44
+ metadata: { host: meta.host || null, port: meta.port || null, source: meta.source || null },
45
+ });
46
+ } catch (_e) { /* drop-silent — audit best-effort, never break TLS */ }
47
+ }
48
+
26
49
  // STATE.tlsKeyShares is initialized to the default PQC group list at
27
50
  // module load — operator setKeyShares() overrides; resetKeyShares()
28
51
  // restores the default. Empty array means "fall back to Node's TLS
@@ -144,7 +167,7 @@ function addCa(pemOrPath, opts) {
144
167
  added.push(meta);
145
168
  }
146
169
  _emitAuditAdd(added, opts);
147
- _emitObs("network.tls.ca.added", { count: added.length });
170
+ observability().safeEvent("network.tls.ca.added", 1, { count: added.length });
148
171
  return added;
149
172
  }
150
173
 
@@ -154,7 +177,7 @@ function addCaBundle(p, opts) {
154
177
 
155
178
  function useSystemTrust(enable) {
156
179
  STATE.systemTrust = enable !== false;
157
- _emitObs("network.tls.system_trust.set", { enabled: STATE.systemTrust });
180
+ observability().safeEvent("network.tls.system_trust.set", 1, { enabled: STATE.systemTrust });
158
181
  }
159
182
 
160
183
  function isSystemTrustEnabled() { return !!STATE.systemTrust; }
@@ -216,7 +239,7 @@ function removeCa(fingerprint256, opts) {
216
239
  });
217
240
  if (removed.length === 0) return 0;
218
241
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove");
219
- _emitObs("network.tls.ca.removed", { count: removed.length, reason: "operator" });
242
+ observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "operator" });
220
243
  return removed.length;
221
244
  }
222
245
 
@@ -234,7 +257,7 @@ function removeCaByLabel(label, opts) {
234
257
  });
235
258
  if (removed.length === 0) return 0;
236
259
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove-by-label");
237
- _emitObs("network.tls.ca.removed", { count: removed.length, reason: "label" });
260
+ observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "label" });
238
261
  return removed.length;
239
262
  }
240
263
 
@@ -243,7 +266,7 @@ function clearAll(opts) {
243
266
  var removed = STATE.cas.map(function (e) { return Object.assign({ label: e.label }, e.meta); });
244
267
  STATE.cas = [];
245
268
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-clear-all");
246
- _emitObs("network.tls.ca.cleared", { count: removed.length });
269
+ observability().safeEvent("network.tls.ca.cleared", 1, { count: removed.length });
247
270
  return removed.length;
248
271
  }
249
272
 
@@ -260,7 +283,7 @@ function purgeExpired(opts) {
260
283
  });
261
284
  if (removed.length === 0) return 0;
262
285
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "expired");
263
- _emitObs("network.tls.ca.purged_expired", { count: removed.length });
286
+ observability().safeEvent("network.tls.ca.purged_expired", 1, { count: removed.length });
264
287
  return removed.length;
265
288
  }
266
289
 
@@ -705,10 +728,6 @@ function _emitAuditAdd(metaList, opts) {
705
728
  }
706
729
  }
707
730
 
708
- function _emitObs(name, fields) {
709
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
710
- }
711
-
712
731
  function _resetForTest() {
713
732
  STATE.cas = [];
714
733
  STATE.systemTrust = false;
@@ -2725,6 +2744,7 @@ function connectWithEch(opts) {
2725
2744
  }
2726
2745
  if (opts.rejectUnauthorized === false) {
2727
2746
  connectOpts.rejectUnauthorized = false;
2747
+ auditInsecureTls({ host: opts.host, port: port, source: "network.tls.connectWithEch" });
2728
2748
  }
2729
2749
  var echAttached = false;
2730
2750
  if (echConfigBuf && nodeSupportsEch) {
@@ -2735,7 +2755,7 @@ function connectWithEch(opts) {
2735
2755
  // gracefully with a one-shot warn so operators know they're
2736
2756
  // sending an outer-only ClientHello.
2737
2757
  try {
2738
- observability().emit("network.tls.ech.unsupported", {
2758
+ observability().safeEvent("network.tls.ech.unsupported", 1, {
2739
2759
  host: opts.host, source: sourceLabel,
2740
2760
  });
2741
2761
  } catch (_e) { /* drop-silent */ }
@@ -2772,7 +2792,7 @@ function connectWithEch(opts) {
2772
2792
  settled = true;
2773
2793
  if (to) clearTimeout(to);
2774
2794
  try {
2775
- observability().emit("network.tls.ech.connected", {
2795
+ observability().safeEvent("network.tls.ech.connected", 1, {
2776
2796
  host: opts.host, echAttached: echAttached, source: sourceLabel,
2777
2797
  });
2778
2798
  } catch (_e) { /* drop-silent */ }
@@ -2832,7 +2852,7 @@ function connectWithEch(opts) {
2832
2852
  // operator still gets a working TLS session. Emit obs so the
2833
2853
  // operator sees the degradation.
2834
2854
  try {
2835
- observability().emit("network.tls.ech.dns_failed", {
2855
+ observability().safeEvent("network.tls.ech.dns_failed", 1, {
2836
2856
  host: opts.host, error: (e && e.message) || String(e),
2837
2857
  });
2838
2858
  } catch (_e) { /* drop-silent */ }
@@ -3174,6 +3194,7 @@ function wrapSNICallback(operatorCb) {
3174
3194
  }
3175
3195
 
3176
3196
  module.exports = {
3197
+ auditInsecureTls: auditInsecureTls,
3177
3198
  addCa: addCa,
3178
3199
  addCaBundle: addCaBundle,
3179
3200
  removeCa: removeCa,
package/lib/network.js CHANGED
@@ -151,7 +151,7 @@ var ntpFacade = {
151
151
  throw new NetworkError("ntp/bad-servers", "ntp.setServers: expected non-empty array");
152
152
  }
153
153
  ntpFacade._defaultServers = list.slice();
154
- _emitObs("network.ntp.servers.set", { count: list.length });
154
+ observability().safeEvent("network.ntp.servers.set", 1, { count: list.length });
155
155
  },
156
156
  getServers: function () {
157
157
  return (ntpFacade._defaultServers || ntpCheck.DEFAULT_SERVERS).slice();
@@ -262,7 +262,7 @@ function bootFromEnv(opts) {
262
262
  } catch (_e) { /* audit best-effort — never break boot */ }
263
263
  }
264
264
  }
265
- _emitObs("network.boot.from_env", { source: "env" });
265
+ observability().safeEvent("network.boot.from_env", 1, { source: "env" });
266
266
  return applied;
267
267
  }
268
268
 
@@ -301,10 +301,6 @@ function snapshot() {
301
301
  };
302
302
  }
303
303
 
304
- function _emitObs(name, fields) {
305
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
306
- }
307
-
308
304
  function _resetForTest() {
309
305
  ntpFacade._defaultServers = null;
310
306
  ntpFacade._defaultTimeoutMs = null;