autotel-devtools 8.0.0 → 8.1.1

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/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as DevtoolsServer, a as DevtoolsSpanExporter } from './exporter-DjLkU621.cjs';
2
- export { b as DevtoolsData, E as ErrorGroup, L as LogData, M as MetricData, S as SpanData, T as TraceData } from './exporter-DjLkU621.cjs';
1
+ import { D as DevtoolsServer, a as DevtoolsSpanExporter } from './exporter-De6p4iAD.cjs';
2
+ export { b as DevtoolsData, E as ErrorGroup, L as LogData, M as MetricData, S as SpanData, T as TraceData } from './exporter-De6p4iAD.cjs';
3
3
  import { Server } from 'node:http';
4
4
  export { DevtoolsLogExporter } from './server/log-exporter.cjs';
5
5
  export { DevtoolsRemoteExporter } from './server/remote-exporter.cjs';
6
- export { E as ErrorAggregator } from './error-aggregator-CbLiuot4.cjs';
6
+ export { E as ErrorAggregator } from './error-aggregator-D1Mr221Y.cjs';
7
7
  import '@opentelemetry/sdk-trace-base';
8
8
  import '@opentelemetry/core';
9
9
  import '@opentelemetry/sdk-logs';
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as DevtoolsServer, a as DevtoolsSpanExporter } from './exporter-DjLkU621.js';
2
- export { b as DevtoolsData, E as ErrorGroup, L as LogData, M as MetricData, S as SpanData, T as TraceData } from './exporter-DjLkU621.js';
1
+ import { D as DevtoolsServer, a as DevtoolsSpanExporter } from './exporter-De6p4iAD.js';
2
+ export { b as DevtoolsData, E as ErrorGroup, L as LogData, M as MetricData, S as SpanData, T as TraceData } from './exporter-De6p4iAD.js';
3
3
  import { Server } from 'node:http';
4
4
  export { DevtoolsLogExporter } from './server/log-exporter.js';
5
5
  export { DevtoolsRemoteExporter } from './server/remote-exporter.js';
6
- export { E as ErrorAggregator } from './error-aggregator-CAk_pt3Z.js';
6
+ export { E as ErrorAggregator } from './error-aggregator-D0Uu5r38.js';
7
7
  import '@opentelemetry/sdk-trace-base';
8
8
  import '@opentelemetry/core';
9
9
  import '@opentelemetry/sdk-logs';
package/dist/index.js CHANGED
@@ -348,6 +348,40 @@ function appendManyWithLimit(items, incoming, limit) {
348
348
  return next.length > limit ? next.slice(next.length - limit) : next;
349
349
  }
350
350
 
351
+ // src/server/origin-guard.ts
352
+ var LOOPBACK_IPV6 = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
353
+ function isLoopbackHostname(hostname) {
354
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
355
+ return h === "localhost" || /^127\./.test(h) || LOOPBACK_IPV6.has(h);
356
+ }
357
+ function hostnameFromHostHeader(host) {
358
+ const h = host.trim();
359
+ if (h.startsWith("[")) {
360
+ const end = h.indexOf("]");
361
+ return end > 0 ? h.slice(1, end) : h;
362
+ }
363
+ const colon = h.indexOf(":");
364
+ return colon === -1 ? h : h.slice(0, colon);
365
+ }
366
+ function hostHeaderIsLoopback(host) {
367
+ return isLoopbackHostname(hostnameFromHostHeader(host));
368
+ }
369
+ function originIsLoopback(origin) {
370
+ try {
371
+ return isLoopbackHostname(new URL(origin).hostname);
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
376
+ function allowSensitiveRequest(headers, loopbackOnly) {
377
+ const { origin, host } = headers;
378
+ if (origin && origin.length > 0 && !originIsLoopback(origin)) return false;
379
+ if (loopbackOnly && host && host.length > 0 && !hostHeaderIsLoopback(host)) {
380
+ return false;
381
+ }
382
+ return true;
383
+ }
384
+
351
385
  // src/server/server.ts
352
386
  var DevtoolsServer = class {
353
387
  wss;
@@ -367,7 +401,12 @@ var DevtoolsServer = class {
367
401
  this._port = options.port ?? 4318;
368
402
  this.onData = options.onData;
369
403
  this.httpServer = options.server ?? createServer();
370
- this.wss = new WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
404
+ const loopbackOnly = options.host == null || hostHeaderIsLoopback(options.host);
405
+ this.wss = new WebSocketServer({
406
+ server: this.httpServer,
407
+ path: options.path ?? "/ws",
408
+ verifyClient: ({ origin, req }) => allowSensitiveRequest({ origin, host: req.headers.host }, loopbackOnly)
409
+ });
371
410
  this.wss.on("error", (err) => {
372
411
  if (this.httpServer.listening) throw err;
373
412
  });
@@ -401,6 +440,7 @@ var DevtoolsServer = class {
401
440
  }
402
441
  addTrace(trace) {
403
442
  const existing = this.traces.find((t) => t.traceId === trace.traceId);
443
+ const merged = existing ?? trace;
404
444
  if (existing) {
405
445
  const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
406
446
  for (const span of trace.spans) {
@@ -412,6 +452,14 @@ var DevtoolsServer = class {
412
452
  existing.endTime = Math.max(existing.endTime, trace.endTime);
413
453
  existing.duration = existing.endTime - existing.startTime;
414
454
  if (trace.status === "ERROR") existing.status = "ERROR";
455
+ const root = existing.spans.find((s) => !s.parentSpanId);
456
+ if (root) {
457
+ existing.rootSpan = root;
458
+ const rootService = root.attributes?.["service.name"];
459
+ if (typeof rootService === "string" && rootService.length > 0) {
460
+ existing.service = rootService;
461
+ }
462
+ }
415
463
  } else {
416
464
  this.traces = appendWithLimit(
417
465
  this.traces,
@@ -420,7 +468,7 @@ var DevtoolsServer = class {
420
468
  );
421
469
  }
422
470
  this.errorAggregator.addErrorsFromTrace(trace);
423
- this.broadcast({ traces: [trace], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
471
+ this.broadcast({ traces: [merged], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
424
472
  }
425
473
  addTraces(traces) {
426
474
  for (const trace of traces) this.addTrace(trace);
@@ -951,7 +999,8 @@ function findPackageRoot() {
951
999
  }
952
1000
  return dir;
953
1001
  }
954
- var FULLPAGE_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>autotel-devtools</title><style>*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;width:100%;overflow:hidden}</style></head><body><script src="/widget.js?mode=fullpage"></script></body></html>`;
1002
+ var DEVTOOLS_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0f172a"/><text x="32" y="41" text-anchor="middle" font-size="32">\u{1F6F0}\uFE0F</text></svg>';
1003
+ var FULLPAGE_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>autotel-devtools</title><link rel="icon" href="/favicon.svg" type="image/svg+xml"><style>*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;width:100%;overflow:hidden}</style></head><body><script src="/widget.js?mode=fullpage"></script></body></html>`;
955
1004
  var cachedVersion = null;
956
1005
  function getVersion() {
957
1006
  if (cachedVersion !== null) return cachedVersion;
@@ -985,8 +1034,10 @@ function getWidgetJs() {
985
1034
  }
986
1035
  return cachedWidgetJs;
987
1036
  }
988
- function attachDevtoolsRoutes(httpServer, devtools) {
1037
+ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1038
+ const loopbackOnly = options.loopbackOnly ?? true;
989
1039
  httpServer.on("request", async (req, res) => {
1040
+ if (req.headers.upgrade?.toLowerCase() === "websocket") return;
990
1041
  res.setHeader("Access-Control-Allow-Origin", "*");
991
1042
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
992
1043
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
@@ -1009,6 +1060,15 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1009
1060
  res.end(js);
1010
1061
  return;
1011
1062
  }
1063
+ if (req.method === "GET" && (url === "/favicon.svg" || url === "/favicon.ico")) {
1064
+ res.writeHead(200, {
1065
+ "Content-Type": "image/svg+xml; charset=utf-8",
1066
+ "Cache-Control": "public, max-age=86400",
1067
+ "Content-Length": Buffer.byteLength(DEVTOOLS_FAVICON_SVG)
1068
+ });
1069
+ res.end(DEVTOOLS_FAVICON_SVG);
1070
+ return;
1071
+ }
1012
1072
  if (req.method === "GET" && url === "/healthz") {
1013
1073
  sendJson(res, 200, {
1014
1074
  ok: true,
@@ -1019,11 +1079,19 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1019
1079
  return;
1020
1080
  }
1021
1081
  if (req.method === "GET" && url === "/v1/traces") {
1082
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1083
+ sendJson(res, 403, { error: "Forbidden" });
1084
+ return;
1085
+ }
1022
1086
  const data = devtools.getCurrentData();
1023
1087
  sendJson(res, 200, { traces: data.traces, count: data.traces.length });
1024
1088
  return;
1025
1089
  }
1026
1090
  if (req.method === "DELETE" && url === "/v1/traces") {
1091
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1092
+ sendJson(res, 403, { error: "Forbidden" });
1093
+ return;
1094
+ }
1027
1095
  devtools.clearData();
1028
1096
  sendJson(res, 200, { cleared: true });
1029
1097
  return;
@@ -1610,21 +1678,23 @@ var DevtoolsRemoteExporter = class {
1610
1678
  function createDevtools(options = {}) {
1611
1679
  const port = options.port ?? 4318;
1612
1680
  const host = options.host ?? "127.0.0.1";
1681
+ const loopbackOnly = hostHeaderIsLoopback(host);
1613
1682
  const httpServer = createServer();
1614
1683
  const wsServer = new DevtoolsServer({
1615
1684
  server: httpServer,
1685
+ host,
1616
1686
  verbose: options.verbose,
1617
1687
  maxHistory: options.maxHistory,
1618
1688
  maxTraceCount: options.maxTraceCount,
1619
1689
  maxLogCount: options.maxLogCount,
1620
1690
  maxMetricCount: options.maxMetricCount
1621
1691
  });
1622
- attachDevtoolsRoutes(httpServer, wsServer);
1692
+ attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
1623
1693
  const listeners = listenLoopbackDualStack({
1624
1694
  primary: httpServer,
1625
1695
  port,
1626
1696
  host,
1627
- attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1697
+ attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
1628
1698
  });
1629
1699
  if (options.verbose) {
1630
1700
  listeners.ready.then(({ warnings }) => {