@blamejs/blamejs-shop 0.4.53 → 0.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +255 -1
  3. package/lib/asset-manifest.json +3 -3
  4. package/lib/storefront.js +135 -0
  5. package/lib/vendor/MANIFEST.json +41 -35
  6. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  7. package/lib/vendor/blamejs/SECURITY.md +1 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  9. package/lib/vendor/blamejs/examples/wiki/lib/html-entities.js +24 -0
  10. package/lib/vendor/blamejs/examples/wiki/lib/symbol-index.js +7 -5
  11. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +9 -1
  12. package/lib/vendor/blamejs/examples/wiki/test/validate-nav-coverage.js +2 -8
  13. package/lib/vendor/blamejs/lib/acme.js +7 -11
  14. package/lib/vendor/blamejs/lib/client-hints.js +3 -1
  15. package/lib/vendor/blamejs/lib/cluster.js +4 -2
  16. package/lib/vendor/blamejs/lib/guard-filename.js +6 -2
  17. package/lib/vendor/blamejs/lib/http-client-cache.js +3 -1
  18. package/lib/vendor/blamejs/lib/http-message-signature.js +25 -8
  19. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +12 -1
  20. package/lib/vendor/blamejs/lib/log-stream-syslog.js +6 -0
  21. package/lib/vendor/blamejs/lib/log.js +24 -2
  22. package/lib/vendor/blamejs/lib/mail.js +5 -0
  23. package/lib/vendor/blamejs/lib/middleware/body-parser.js +48 -6
  24. package/lib/vendor/blamejs/lib/network-dns.js +22 -26
  25. package/lib/vendor/blamejs/lib/network-heartbeat.js +3 -3
  26. package/lib/vendor/blamejs/lib/network-proxy.js +3 -7
  27. package/lib/vendor/blamejs/lib/network-tls.js +34 -13
  28. package/lib/vendor/blamejs/lib/network.js +2 -6
  29. package/lib/vendor/blamejs/lib/notify.js +7 -12
  30. package/lib/vendor/blamejs/lib/seeders.js +5 -10
  31. package/lib/vendor/blamejs/lib/structured-fields.js +38 -1
  32. package/lib/vendor/blamejs/package.json +1 -1
  33. package/lib/vendor/blamejs/release-notes/v0.15.12.json +47 -0
  34. package/lib/vendor/blamejs/test/00-primitives.js +24 -0
  35. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-error-redaction.test.js +74 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +18 -8
  37. package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +11 -0
  38. package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +33 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/log-stream-otlp-grpc.test.js +27 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +31 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields.test.js +14 -0
  42. package/package.json +1 -1
@@ -1089,8 +1089,10 @@ function discoveryHandler() {
1089
1089
  body = { leader: null, self: selfInfo };
1090
1090
  status = 503;
1091
1091
  }
1092
- } catch (e) {
1093
- body = { leader: null, self: selfInfo, error: e.message };
1092
+ } catch (_e) {
1093
+ // Generic client-facing reason the caught error's message (a DB error
1094
+ // detail / DSN / host:port) is not echoed to the client (CWE-209).
1095
+ body = { leader: null, self: selfInfo, error: "leader lookup unavailable" };
1094
1096
  status = 503;
1095
1097
  }
1096
1098
  var json = JSON.stringify(body);
@@ -80,7 +80,10 @@ var _err = GuardFilenameError.factory;
80
80
  // Windows: < > : " / \ | ? *
81
81
  // Unix: /
82
82
  // Both: null and C0 controls (handled separately via codepoint-class)
83
- var RESERVED_CHARS_RE = /[<>:"/\\|?*]/;
83
+ // Global so reservedCharPolicy:"strip" replaces EVERY reserved char, not just
84
+ // the first (CodeQL js/incomplete-multi-character-sanitization). Only consumer
85
+ // is the .replace() in _sanitize — no stateful .test()/.exec() lastIndex hazard.
86
+ var RESERVED_CHARS_RE = /[<>:"/\\|?*]/g;
84
87
 
85
88
  // Windows reserved device names (case-insensitive). Match either the
86
89
  // bare name or `<name>.<anything>`.
@@ -586,8 +589,9 @@ function _sanitize(input, opts) {
586
589
 
587
590
  // Strip reserved chars when policy says strip.
588
591
  if (opts.reservedCharPolicy === "strip") {
592
+ // Single global strip — RESERVED_CHARS_RE (now /g) covers the whole
593
+ // reserved class INCLUDING path separators, so no second pass is needed.
589
594
  name = name.replace(RESERVED_CHARS_RE, "_"); // allow:dynamic-regex — RESERVED_CHARS_RE is a compile-time literal
590
- name = name.replace(/[<>:"|?*]/g, "_");
591
595
  } else if (opts.reservedCharPolicy === "reject") {
592
596
  if (/[<>:"|?*]/.test(name)) {
593
597
  throw _err("filename.reserved-char", "filename contains reserved character");
@@ -107,7 +107,9 @@ function _parseCacheControl(value) {
107
107
  else { k = p.slice(0, eq).trim(); v = p.slice(eq + 1).trim(); }
108
108
  // Strip surrounding quotes from value.
109
109
  if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
110
- v = v.slice(1, v.length - 1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
110
+ // Single-pass RFC 8941 unescape (chained .replace() mis-decodes
111
+ // an escaped backslash adjacent to another escape).
112
+ v = structuredFields.unescapeSfStringBody(v.slice(1, v.length - 1));
111
113
  }
112
114
  out[k.toLowerCase()] = v;
113
115
  }
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  var nodeCrypto = require("node:crypto");
63
+ var bCrypto = require("./crypto");
63
64
  var safeUrl = require("./safe-url");
64
65
  var safeBuffer = require("./safe-buffer");
65
66
  var C = require("./constants");
@@ -406,7 +407,9 @@ function _parseSignatureInput(headerValue) {
406
407
  throw _err("BAD_HEADER",
407
408
  "httpSig: Signature-Input: unterminated quoted token");
408
409
  }
409
- var bareName = coveredRaw.slice(qStart, qEnd).replace(/\\\\/g, "\\").replace(/\\"/g, "\"");
410
+ // Single-pass RFC 8941 §3.3.3 unescape — NOT two chained .replace() passes,
411
+ // which mis-decode an escaped backslash adjacent to another escape.
412
+ var bareName = structuredFields.unescapeSfStringBody(coveredRaw.slice(qStart, qEnd));
410
413
  i2 = qEnd + 1;
411
414
  // Optional ;param=value;param=... suffix immediately following.
412
415
  var suffixStart = i2;
@@ -525,13 +528,27 @@ function verify(msg, opts) {
525
528
  if (!presented) {
526
529
  return { valid: false, reason: "content-digest-header-missing" };
527
530
  }
528
- var actual = contentDigest(m.body);
529
- // RFC 9530 allows multiple algorithms in one header (sha-512=...,
530
- // sha-256=...). For SHA3-512 specifically exact substring match
531
- // against the presented header. For peer-supplied SHA-512 / SHA-256
532
- // identifiers the operator is responsible for re-validating; this
533
- // primitive only auto-checks SHA3-512.
534
- if (presented.indexOf(actual.replace(/^sha3-512=/, "sha3-512=")) === -1) {
531
+ // contentDigest() returns the canonical structured-field form
532
+ // `sha3-512=:<base64>:`. RFC 9530 permits a multi-member header
533
+ // (e.g. `sha-256=:...:, sha3-512=:...:`); split on top-level commas and
534
+ // match the sha3-512 member EXACTLY, in constant time, rather than by an
535
+ // unanchored substring scan that could spuriously match the digest text
536
+ // buried inside another member's value or parameters. Peer-supplied
537
+ // sha-512 / sha-256 identifiers stay the operator's responsibility.
538
+ var expectedDigest = contentDigest(m.body); // "sha3-512=:<b64>:"
539
+ var matchedDigest = false;
540
+ var digestMembers = structuredFields.splitTopLevel(presented, ",");
541
+ for (var di = 0; di < digestMembers.length; di++) {
542
+ var member = digestMembers[di].trim();
543
+ var deq = member.indexOf("=");
544
+ if (deq < 1) continue;
545
+ if (member.slice(0, deq).trim().toLowerCase() !== "sha3-512") continue;
546
+ var memberCanonical = "sha3-512=" + member.slice(deq + 1).trim();
547
+ // crypto.timingSafeEqual is the length-tolerant constant-time wrapper
548
+ // (returns false for unequal lengths without leaking via a length branch).
549
+ if (bCrypto.timingSafeEqual(memberCanonical, expectedDigest)) { matchedDigest = true; break; }
550
+ }
551
+ if (!matchedDigest) {
535
552
  return { valid: false, reason: "content-digest-mismatch" };
536
553
  }
537
554
  }
@@ -38,6 +38,9 @@ var lazyRequire = require("./lazy-require");
38
38
  // scrub attribute values through the telemetry redactor before they cross the
39
39
  // OTLP egress boundary (CWE-532).
40
40
  var observability = lazyRequire(function () { return require("./observability"); });
41
+ // Lazy — network-tls is widely required; audit an insecure (cert-validation-
42
+ // disabled) outbound TLS session at honor time, same surface as connectWithEch.
43
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
41
44
 
42
45
  var _err = LogStreamError.factory;
43
46
  var _log = boot("log-stream-otlp-grpc");
@@ -215,7 +218,14 @@ function _makeClient(cfg) {
215
218
  var sessionOpts = {};
216
219
  if (cfg.ca) sessionOpts.ca = cfg.ca;
217
220
  if (cfg.servername) sessionOpts.servername = cfg.servername;
218
- if (cfg.allowInsecure) sessionOpts.rejectUnauthorized = false;
221
+ if (cfg.allowInsecure && url.protocol === "https:") {
222
+ // allowInsecure only has meaning on a TLS session. For an h2c endpoint
223
+ // (http://, cleartext HTTP/2) there is no certificate to validate and
224
+ // nothing to skip, so neither rejectUnauthorized nor the insecure-TLS
225
+ // audit applies — emitting it there would be a false security event.
226
+ sessionOpts.rejectUnauthorized = false;
227
+ networkTls().auditInsecureTls({ host: authority, source: "log-stream.otlp-grpc" });
228
+ }
219
229
  var session = http2.connect(authority, sessionOpts);
220
230
  session.on("error", function () { /* surfaced through request err */ });
221
231
  if (typeof session.unref === "function") session.unref();
@@ -409,6 +419,7 @@ module.exports = {
409
419
  create: create,
410
420
  // Exposed for layer-0 tests that verify the wire encoding without
411
421
  // standing up an HTTP/2 server.
422
+ _makeClient: _makeClient,
412
423
  _encodeAnyValue: _encodeAnyValue,
413
424
  _encodeKeyValue: _encodeKeyValue,
414
425
  _encodeLogRecord: _encodeLogRecord,
@@ -37,6 +37,9 @@ var safeAsync = require("./safe-async");
37
37
  var safeBuffer = require("./safe-buffer");
38
38
  var safeUrl = require("./safe-url");
39
39
  var { LogStreamError } = require("./framework-error");
40
+ var lazyRequire = require("./lazy-require");
41
+ // Lazy — audit a cert-validation-disabled syslog/TLS session at honor time.
42
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
40
43
 
41
44
  var _err = LogStreamError.factory;
42
45
  var log = boot("log-stream-syslog");
@@ -223,6 +226,9 @@ function create(config) {
223
226
  });
224
227
  if (cfg.ca) tlsOpts.ca = cfg.ca;
225
228
  if (cfg.servername) tlsOpts.servername = cfg.servername;
229
+ if (cfg.rejectUnauthorized === false) {
230
+ networkTls().auditInsecureTls({ host: cfg.host, port: cfg.port, source: "log-stream.syslog" });
231
+ }
226
232
  sock = nodeTls.connect(tlsOpts, onConnect);
227
233
  } else {
228
234
  sock = net.connect(connectOpts, onConnect);
@@ -464,7 +464,11 @@ function boot(name) {
464
464
  var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
465
465
  var isTty = !!(stream && stream.isTTY);
466
466
  if (isTty) {
467
- sink(prefix + String(msg));
467
+ // Raw human-readable line — escape BOTH the C0/newline (line-forging)
468
+ // and bidi (re-ordering) control classes the create() path neutralizes,
469
+ // so a hostile boot message can't inject lines or re-order the visible
470
+ // line on a TTY / syslog reader (CWE-117 / Trojan-Source CVE-2021-42574).
471
+ sink(_escapeBidiControls(_escapeC0Controls(prefix + String(msg))));
468
472
  return;
469
473
  }
470
474
  var entry = {
@@ -474,7 +478,9 @@ function boot(name) {
474
478
  component: name,
475
479
  boot: true,
476
480
  };
477
- sink(JSON.stringify(entry));
481
+ // JSON.stringify already escapes C0/newlines; bidi/format controls survive
482
+ // raw into a piped aggregator, so apply the same bidi escape create() uses.
483
+ sink(_escapeBidiControls(JSON.stringify(entry)));
478
484
  }
479
485
 
480
486
  function debug(msg, fields) {
@@ -560,6 +566,22 @@ function _escapeBidiControls(s) {
560
566
  });
561
567
  }
562
568
 
569
+ // C0 control chars (incl. CR / LF / TAB) + DEL — escaped to `\uXXXX` so a
570
+ // hostile message can't forge extra log lines on a raw (non-JSON) TTY sink
571
+ // (log-injection, CWE-117). The create() path gets this for free from
572
+ // JSON.stringify; the boot() TTY branch writes raw text and needs it
573
+ // explicitly. Pairs with _escapeBidiControls (which only covers the bidi set).
574
+ var _C0_CONTROL_RE = /[\u0000-\u001f\u007f]/g; // eslint-disable-line no-control-regex -- the C0/DEL set is what we escape
575
+
576
+ function _escapeC0Controls(s) {
577
+ if (typeof s !== "string" || s.length === 0) return s;
578
+ return s.replace(_C0_CONTROL_RE, function (ch) {
579
+ var code = ch.charCodeAt(0).toString(16);
580
+ while (code.length < 4) code = "0" + code;
581
+ return "\\u" + code;
582
+ });
583
+ }
584
+
563
585
  // runs before safeEnv on the boot path; safeEnv requires log, so log
564
586
  // can't go through safeEnv to read its own level.
565
587
  function _bootMinLevel() {
@@ -76,6 +76,8 @@ var networkDns = lazyRequire(function () { return require("./network-dns"); });
76
76
  var nodeUrl = require("node:url");
77
77
  var numericBounds = require("./numeric-bounds");
78
78
  var nodeTls = lazyRequire(function () { return require("node:tls"); });
79
+ // Lazy — audit a cert-validation-disabled SMTP/TLS session at honor time.
80
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
79
81
  var safeJson = require("./safe-json");
80
82
  var safeSchema = require("./safe-schema");
81
83
  var validateOpts = require("./validate-opts");
@@ -778,6 +780,9 @@ function smtpTransport(opts) {
778
780
  var port = opts.port || 587;
779
781
  var useImplicitTLS = port === 465 || opts.implicitTls === true;
780
782
  var rejectUnauthorized = opts.rejectUnauthorized !== false;
783
+ if (rejectUnauthorized === false) {
784
+ networkTls().auditInsecureTls({ host: opts.host, port: port, source: "mail.smtp" });
785
+ }
781
786
  var ehloName = opts.ehloName || "blamejs";
782
787
  // GHSA-c7w3-x93f-qmm8 / GHSA-vvjj-xcjg-gr5g (nodemailer CRLF-injection
783
788
  // class) — any string concatenated into an outbound SMTP wire command
@@ -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;
@@ -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();