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/cli.js CHANGED
@@ -346,6 +346,40 @@ function appendManyWithLimit(items, incoming, limit) {
346
346
  return next.length > limit ? next.slice(next.length - limit) : next;
347
347
  }
348
348
 
349
+ // src/server/origin-guard.ts
350
+ var LOOPBACK_IPV6 = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
351
+ function isLoopbackHostname(hostname) {
352
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
353
+ return h === "localhost" || /^127\./.test(h) || LOOPBACK_IPV6.has(h);
354
+ }
355
+ function hostnameFromHostHeader(host) {
356
+ const h = host.trim();
357
+ if (h.startsWith("[")) {
358
+ const end = h.indexOf("]");
359
+ return end > 0 ? h.slice(1, end) : h;
360
+ }
361
+ const colon = h.indexOf(":");
362
+ return colon === -1 ? h : h.slice(0, colon);
363
+ }
364
+ function hostHeaderIsLoopback(host) {
365
+ return isLoopbackHostname(hostnameFromHostHeader(host));
366
+ }
367
+ function originIsLoopback(origin) {
368
+ try {
369
+ return isLoopbackHostname(new URL(origin).hostname);
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ function allowSensitiveRequest(headers, loopbackOnly) {
375
+ const { origin, host } = headers;
376
+ if (origin && origin.length > 0 && !originIsLoopback(origin)) return false;
377
+ if (loopbackOnly && host && host.length > 0 && !hostHeaderIsLoopback(host)) {
378
+ return false;
379
+ }
380
+ return true;
381
+ }
382
+
349
383
  // src/server/server.ts
350
384
  var DevtoolsServer = class {
351
385
  wss;
@@ -365,7 +399,12 @@ var DevtoolsServer = class {
365
399
  this._port = options.port ?? 4318;
366
400
  this.onData = options.onData;
367
401
  this.httpServer = options.server ?? createServer();
368
- this.wss = new WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
402
+ const loopbackOnly = options.host == null || hostHeaderIsLoopback(options.host);
403
+ this.wss = new WebSocketServer({
404
+ server: this.httpServer,
405
+ path: options.path ?? "/ws",
406
+ verifyClient: ({ origin, req }) => allowSensitiveRequest({ origin, host: req.headers.host }, loopbackOnly)
407
+ });
369
408
  this.wss.on("error", (err) => {
370
409
  if (this.httpServer.listening) throw err;
371
410
  });
@@ -399,6 +438,7 @@ var DevtoolsServer = class {
399
438
  }
400
439
  addTrace(trace) {
401
440
  const existing = this.traces.find((t) => t.traceId === trace.traceId);
441
+ const merged = existing ?? trace;
402
442
  if (existing) {
403
443
  const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
404
444
  for (const span of trace.spans) {
@@ -410,6 +450,14 @@ var DevtoolsServer = class {
410
450
  existing.endTime = Math.max(existing.endTime, trace.endTime);
411
451
  existing.duration = existing.endTime - existing.startTime;
412
452
  if (trace.status === "ERROR") existing.status = "ERROR";
453
+ const root = existing.spans.find((s) => !s.parentSpanId);
454
+ if (root) {
455
+ existing.rootSpan = root;
456
+ const rootService = root.attributes?.["service.name"];
457
+ if (typeof rootService === "string" && rootService.length > 0) {
458
+ existing.service = rootService;
459
+ }
460
+ }
413
461
  } else {
414
462
  this.traces = appendWithLimit(
415
463
  this.traces,
@@ -418,7 +466,7 @@ var DevtoolsServer = class {
418
466
  );
419
467
  }
420
468
  this.errorAggregator.addErrorsFromTrace(trace);
421
- this.broadcast({ traces: [trace], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
469
+ this.broadcast({ traces: [merged], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
422
470
  }
423
471
  addTraces(traces) {
424
472
  for (const trace of traces) this.addTrace(trace);
@@ -970,7 +1018,8 @@ function findPackageRoot() {
970
1018
  }
971
1019
  return dir;
972
1020
  }
973
- 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>`;
1021
+ 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>';
1022
+ 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>`;
974
1023
  var cachedVersion = null;
975
1024
  function getVersion() {
976
1025
  if (cachedVersion !== null) return cachedVersion;
@@ -1004,8 +1053,10 @@ function getWidgetJs() {
1004
1053
  }
1005
1054
  return cachedWidgetJs;
1006
1055
  }
1007
- function attachDevtoolsRoutes(httpServer, devtools) {
1056
+ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1057
+ const loopbackOnly = options.loopbackOnly ?? true;
1008
1058
  httpServer.on("request", async (req, res) => {
1059
+ if (req.headers.upgrade?.toLowerCase() === "websocket") return;
1009
1060
  res.setHeader("Access-Control-Allow-Origin", "*");
1010
1061
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1011
1062
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
@@ -1028,6 +1079,15 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1028
1079
  res.end(js);
1029
1080
  return;
1030
1081
  }
1082
+ if (req.method === "GET" && (url === "/favicon.svg" || url === "/favicon.ico")) {
1083
+ res.writeHead(200, {
1084
+ "Content-Type": "image/svg+xml; charset=utf-8",
1085
+ "Cache-Control": "public, max-age=86400",
1086
+ "Content-Length": Buffer.byteLength(DEVTOOLS_FAVICON_SVG)
1087
+ });
1088
+ res.end(DEVTOOLS_FAVICON_SVG);
1089
+ return;
1090
+ }
1031
1091
  if (req.method === "GET" && url === "/healthz") {
1032
1092
  sendJson(res, 200, {
1033
1093
  ok: true,
@@ -1038,11 +1098,19 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1038
1098
  return;
1039
1099
  }
1040
1100
  if (req.method === "GET" && url === "/v1/traces") {
1101
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1102
+ sendJson(res, 403, { error: "Forbidden" });
1103
+ return;
1104
+ }
1041
1105
  const data = devtools.getCurrentData();
1042
1106
  sendJson(res, 200, { traces: data.traces, count: data.traces.length });
1043
1107
  return;
1044
1108
  }
1045
1109
  if (req.method === "DELETE" && url === "/v1/traces") {
1110
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1111
+ sendJson(res, 403, { error: "Forbidden" });
1112
+ return;
1113
+ }
1046
1114
  devtools.clearData();
1047
1115
  sendJson(res, 200, { cleared: true });
1048
1116
  return;
@@ -1283,13 +1351,14 @@ async function main() {
1283
1351
  process.exit(0);
1284
1352
  }
1285
1353
  const httpServer = createServer();
1286
- const wsServer = new DevtoolsServer({ server: httpServer, verbose: true });
1287
- attachDevtoolsRoutes(httpServer, wsServer);
1354
+ const loopbackOnly = hostHeaderIsLoopback(options.host);
1355
+ const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true });
1356
+ attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
1288
1357
  const listeners = listenLoopbackDualStack({
1289
1358
  primary: httpServer,
1290
1359
  port: options.port,
1291
1360
  host: options.host,
1292
- attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1361
+ attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
1293
1362
  });
1294
1363
  const { addresses, warnings, port: boundPort } = await listeners.ready;
1295
1364
  if (boundPort !== options.port) {