autotel-devtools 19.1.0 → 20.1.0

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/README.md CHANGED
@@ -214,6 +214,7 @@ AUTOTEL_MAX_METRIC_COUNT=50000 # Max metrics to keep (default: 100)
214
214
  AUTOTEL_DEVTOOLS_PORT=4318 # Server port (default: 4318)
215
215
  AUTOTEL_DEVTOOLS_HOST=127.0.0.1 # Bind host (default: 127.0.0.1)
216
216
  AUTOTEL_DEVTOOLS_TITLE="My App" # Dashboard title (optional)
217
+ AUTOTEL_DEVTOOLS_SOURCE_ROOT=. # Root GET /source may read (default: cwd on a loopback bind, else off; `false` disables)
217
218
  ```
218
219
 
219
220
  ### CLI Options
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_http = require('./http-53ldVU8k.cjs');
2
+ const require_http = require('./http-D44dITGS.cjs');
3
3
  const require_listen = require('./listen-DGzVi7DE.cjs');
4
4
  let node_http = require("node:http");
5
5
  let node_fs = require("node:fs");
@@ -79,9 +79,11 @@ async function startReceiver(options) {
79
79
  host: options.host,
80
80
  verbose: true
81
81
  });
82
+ const sourceRoot = require_http.resolveSourceRoot(process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
82
83
  require_http.attachDevtoolsRoutes(httpServer, wsServer, {
83
84
  loopbackOnly,
84
- title: options.title
85
+ title: options.title,
86
+ sourceRoot
85
87
  });
86
88
  const listeners = require_listen.listenLoopbackDualStack({
87
89
  primary: httpServer,
@@ -89,7 +91,8 @@ async function startReceiver(options) {
89
91
  host: options.host,
90
92
  attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, {
91
93
  loopbackOnly,
92
- title: options.title
94
+ title: options.title,
95
+ sourceRoot
93
96
  })
94
97
  });
95
98
  const { addresses, warnings, port: boundPort } = await listeners.ready;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as DevtoolsServer, i as probePortHolder, p as hostHeaderIsLoopback, t as attachDevtoolsRoutes } from "./http-B8rl56bF.js";
2
+ import { a as probePortHolder, f as DevtoolsServer, m as hostHeaderIsLoopback, r as resolveSourceRoot, t as attachDevtoolsRoutes } from "./http-Ddn7gT0R.js";
3
3
  import { t as listenLoopbackDualStack } from "./listen-NVMbBWHw.js";
4
4
  import { createServer } from "node:http";
5
5
  import { readFileSync } from "node:fs";
@@ -79,9 +79,11 @@ async function startReceiver(options) {
79
79
  host: options.host,
80
80
  verbose: true
81
81
  });
82
+ const sourceRoot = resolveSourceRoot(process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
82
83
  attachDevtoolsRoutes(httpServer, wsServer, {
83
84
  loopbackOnly,
84
- title: options.title
85
+ title: options.title,
86
+ sourceRoot
85
87
  });
86
88
  const listeners = listenLoopbackDualStack({
87
89
  primary: httpServer,
@@ -89,7 +91,8 @@ async function startReceiver(options) {
89
91
  host: options.host,
90
92
  attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, {
91
93
  loopbackOnly,
92
- title: options.title
94
+ title: options.title,
95
+ sourceRoot
93
96
  })
94
97
  });
95
98
  const { addresses, warnings, port: boundPort } = await listeners.ready;
@@ -32,6 +32,7 @@ let ws = require("ws");
32
32
  let autotel_agents = require("autotel-agents");
33
33
  let node_fs = require("node:fs");
34
34
  let node_path = require("node:path");
35
+ node_path = __toESM(node_path, 1);
35
36
  let node_url = require("node:url");
36
37
  let protobufjs = require("protobufjs");
37
38
  protobufjs = __toESM(protobufjs, 1);
@@ -105,7 +106,7 @@ var ErrorAggregator = class {
105
106
  const exceptionEvent = span.events?.find((e) => e.name === "exception");
106
107
  const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
107
108
  const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
108
- const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
109
+ const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || span.attributes["error.stack"] || this.extractStackFromEvents(span);
109
110
  return {
110
111
  traceId: trace.traceId,
111
112
  spanId: span.spanId,
@@ -201,7 +202,7 @@ var ErrorAggregator = class {
201
202
  * Normalize error message by removing dynamic parts
202
203
  */
203
204
  normalizeMessage(message) {
204
- return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\b\d+\b/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
205
+ return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
205
206
  }
206
207
  /**
207
208
  * Normalize stack trace for display
@@ -1149,6 +1150,98 @@ async function probePortHolder(host, port, timeoutMs = 500) {
1149
1150
  }
1150
1151
  }
1151
1152
 
1153
+ //#endregion
1154
+ //#region src/server/source-file.ts
1155
+ /** Refuse to slurp something huge just because a span named it. */
1156
+ const MAX_BYTES = 2e6;
1157
+ const DISABLED = /* @__PURE__ */ new Set([
1158
+ "false",
1159
+ "0",
1160
+ "off",
1161
+ "no",
1162
+ ""
1163
+ ]);
1164
+ /**
1165
+ * Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
1166
+ *
1167
+ * Defaults **on**, at the working directory: devtools is a local tool whose
1168
+ * whole point is showing you your own code, and requiring a flag for that would
1169
+ * mean nobody ever sees the feature. The blast radius stays small because two
1170
+ * other things still hold — the receiver is bound to loopback, and nothing
1171
+ * outside this directory is reachable. Set the variable to `false` to turn it
1172
+ * off outright.
1173
+ *
1174
+ * A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
1175
+ * Origin guard does not replace it: a request with no `Origin` at all — any
1176
+ * `curl` on the network — passes. The root holds whatever else lives in the
1177
+ * project, `.env` included, so the default flips to **off** there. An explicit
1178
+ * root is still honoured: exposing it on purpose is the caller's call.
1179
+ */
1180
+ function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
1181
+ if (configured === void 0) return loopbackOnly ? cwd : void 0;
1182
+ if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
1183
+ return configured;
1184
+ }
1185
+ /**
1186
+ * Resolve `requested` against `root`, or return `null` if it escapes.
1187
+ *
1188
+ * Containment is judged on **real** paths so a symlink inside the root that
1189
+ * points outside it is rejected — lexical `..` stripping alone cannot see that.
1190
+ * The value returned is the *lexical* resolution, because the real one differs
1191
+ * from the caller's path whenever an ancestor is a symlink (on macOS both
1192
+ * `/tmp` and `/var` are), and a caller comparing paths should not have to know.
1193
+ */
1194
+ function resolveWithinRoot(root, requested) {
1195
+ const lexicalRoot = node_path.default.resolve(root);
1196
+ const realRoot = safeRealpath(lexicalRoot);
1197
+ if (realRoot === null) return null;
1198
+ const lexicalTarget = node_path.default.resolve(lexicalRoot, requested);
1199
+ const realTarget = safeRealpath(lexicalTarget);
1200
+ if (realTarget === null) return null;
1201
+ if (!isInside(realRoot, realTarget)) return null;
1202
+ return lexicalTarget;
1203
+ }
1204
+ /** True when `target` is `root` itself or sits beneath it. */
1205
+ function isInside(root, target) {
1206
+ if (target === root) return true;
1207
+ return target.startsWith(root.endsWith(node_path.default.sep) ? root : root + node_path.default.sep);
1208
+ }
1209
+ function safeRealpath(p) {
1210
+ try {
1211
+ return (0, node_fs.realpathSync)(p);
1212
+ } catch {
1213
+ return null;
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Read `context` lines either side of `line` from a file inside `root`.
1218
+ * Returns `null` when the path escapes the root, is not a readable file, or is
1219
+ * too large — the caller cannot distinguish those, which is the point.
1220
+ */
1221
+ function readSourceWindow(root, requested, line, context) {
1222
+ const resolved = resolveWithinRoot(root, requested);
1223
+ if (resolved === null) return null;
1224
+ let text;
1225
+ try {
1226
+ const stat = (0, node_fs.statSync)(resolved);
1227
+ if (!stat.isFile() || stat.size > MAX_BYTES) return null;
1228
+ text = (0, node_fs.readFileSync)(resolved, "utf8");
1229
+ } catch {
1230
+ return null;
1231
+ }
1232
+ const all = text.split("\n");
1233
+ if (all.at(-1) === "") all.pop();
1234
+ const startLine = Math.max(1, line - context);
1235
+ const endLine = Math.min(all.length, line + context);
1236
+ if (startLine > all.length) return null;
1237
+ return {
1238
+ file: node_path.default.relative(node_path.default.resolve(root), resolved),
1239
+ line,
1240
+ startLine,
1241
+ lines: all.slice(startLine - 1, endLine)
1242
+ };
1243
+ }
1244
+
1152
1245
  //#endregion
1153
1246
  //#region src/server/http.ts
1154
1247
  function sendOtlpError(res, req, e) {
@@ -1212,6 +1305,7 @@ function getWidgetJs() {
1212
1305
  }
1213
1306
  function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1214
1307
  const loopbackOnly = options.loopbackOnly ?? true;
1308
+ const sourceRoot = options.sourceRoot;
1215
1309
  const fullpageHtml = renderFullpageHtml(options.title);
1216
1310
  httpServer.on("request", async (req, res) => {
1217
1311
  if (req.headers.upgrade?.toLowerCase() === "websocket") return;
@@ -1252,6 +1346,31 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1252
1346
  res.end(DEVTOOLS_FAVICON_SVG);
1253
1347
  return;
1254
1348
  }
1349
+ if (req.method === "GET" && url.split("?")[0] === "/source") {
1350
+ if (!sourceRoot) {
1351
+ sendJson(res, 404, { error: "Not found" });
1352
+ return;
1353
+ }
1354
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1355
+ sendJson(res, 403, { error: "Forbidden" });
1356
+ return;
1357
+ }
1358
+ const query = new URL(url, "http://localhost").searchParams;
1359
+ const file = query.get("file");
1360
+ const line = Number(query.get("line"));
1361
+ const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
1362
+ if (!file || !Number.isInteger(line) || line < 1) {
1363
+ sendJson(res, 400, { error: "file and a positive integer line are required" });
1364
+ return;
1365
+ }
1366
+ const window = readSourceWindow(sourceRoot, file, line, context);
1367
+ if (window === null) {
1368
+ sendJson(res, 404, { error: "Not found" });
1369
+ return;
1370
+ }
1371
+ sendJson(res, 200, { ...window });
1372
+ return;
1373
+ }
1255
1374
  if (req.method === "GET" && url === "/healthz") {
1256
1375
  sendJson(res, 200, {
1257
1376
  ok: true,
@@ -1438,6 +1557,12 @@ Object.defineProperty(exports, 'probePortHolder', {
1438
1557
  return probePortHolder;
1439
1558
  }
1440
1559
  });
1560
+ Object.defineProperty(exports, 'resolveSourceRoot', {
1561
+ enumerable: true,
1562
+ get: function () {
1563
+ return resolveSourceRoot;
1564
+ }
1565
+ });
1441
1566
  Object.defineProperty(exports, 'resolveTelemetryLimits', {
1442
1567
  enumerable: true,
1443
1568
  get: function () {
@@ -3,8 +3,8 @@ import { t as getResourceName } from "./resource-utils-B4UVvfnH.js";
3
3
  import { createServer } from "node:http";
4
4
  import { WebSocket, WebSocketServer } from "ws";
5
5
  import { ingestAgentEvents, ingestAgentMetrics } from "autotel-agents";
6
- import { existsSync, readFileSync } from "node:fs";
7
- import { dirname, resolve } from "node:path";
6
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
7
+ import path, { dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import protobuf from "protobufjs";
10
10
 
@@ -77,7 +77,7 @@ var ErrorAggregator = class {
77
77
  const exceptionEvent = span.events?.find((e) => e.name === "exception");
78
78
  const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
79
79
  const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
80
- const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
80
+ const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || span.attributes["error.stack"] || this.extractStackFromEvents(span);
81
81
  return {
82
82
  traceId: trace.traceId,
83
83
  spanId: span.spanId,
@@ -173,7 +173,7 @@ var ErrorAggregator = class {
173
173
  * Normalize error message by removing dynamic parts
174
174
  */
175
175
  normalizeMessage(message) {
176
- return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\b\d+\b/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
176
+ return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
177
177
  }
178
178
  /**
179
179
  * Normalize stack trace for display
@@ -1121,6 +1121,98 @@ async function probePortHolder(host, port, timeoutMs = 500) {
1121
1121
  }
1122
1122
  }
1123
1123
 
1124
+ //#endregion
1125
+ //#region src/server/source-file.ts
1126
+ /** Refuse to slurp something huge just because a span named it. */
1127
+ const MAX_BYTES = 2e6;
1128
+ const DISABLED = /* @__PURE__ */ new Set([
1129
+ "false",
1130
+ "0",
1131
+ "off",
1132
+ "no",
1133
+ ""
1134
+ ]);
1135
+ /**
1136
+ * Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
1137
+ *
1138
+ * Defaults **on**, at the working directory: devtools is a local tool whose
1139
+ * whole point is showing you your own code, and requiring a flag for that would
1140
+ * mean nobody ever sees the feature. The blast radius stays small because two
1141
+ * other things still hold — the receiver is bound to loopback, and nothing
1142
+ * outside this directory is reachable. Set the variable to `false` to turn it
1143
+ * off outright.
1144
+ *
1145
+ * A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
1146
+ * Origin guard does not replace it: a request with no `Origin` at all — any
1147
+ * `curl` on the network — passes. The root holds whatever else lives in the
1148
+ * project, `.env` included, so the default flips to **off** there. An explicit
1149
+ * root is still honoured: exposing it on purpose is the caller's call.
1150
+ */
1151
+ function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
1152
+ if (configured === void 0) return loopbackOnly ? cwd : void 0;
1153
+ if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
1154
+ return configured;
1155
+ }
1156
+ /**
1157
+ * Resolve `requested` against `root`, or return `null` if it escapes.
1158
+ *
1159
+ * Containment is judged on **real** paths so a symlink inside the root that
1160
+ * points outside it is rejected — lexical `..` stripping alone cannot see that.
1161
+ * The value returned is the *lexical* resolution, because the real one differs
1162
+ * from the caller's path whenever an ancestor is a symlink (on macOS both
1163
+ * `/tmp` and `/var` are), and a caller comparing paths should not have to know.
1164
+ */
1165
+ function resolveWithinRoot(root, requested) {
1166
+ const lexicalRoot = path.resolve(root);
1167
+ const realRoot = safeRealpath(lexicalRoot);
1168
+ if (realRoot === null) return null;
1169
+ const lexicalTarget = path.resolve(lexicalRoot, requested);
1170
+ const realTarget = safeRealpath(lexicalTarget);
1171
+ if (realTarget === null) return null;
1172
+ if (!isInside(realRoot, realTarget)) return null;
1173
+ return lexicalTarget;
1174
+ }
1175
+ /** True when `target` is `root` itself or sits beneath it. */
1176
+ function isInside(root, target) {
1177
+ if (target === root) return true;
1178
+ return target.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
1179
+ }
1180
+ function safeRealpath(p) {
1181
+ try {
1182
+ return realpathSync(p);
1183
+ } catch {
1184
+ return null;
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Read `context` lines either side of `line` from a file inside `root`.
1189
+ * Returns `null` when the path escapes the root, is not a readable file, or is
1190
+ * too large — the caller cannot distinguish those, which is the point.
1191
+ */
1192
+ function readSourceWindow(root, requested, line, context) {
1193
+ const resolved = resolveWithinRoot(root, requested);
1194
+ if (resolved === null) return null;
1195
+ let text;
1196
+ try {
1197
+ const stat = statSync(resolved);
1198
+ if (!stat.isFile() || stat.size > MAX_BYTES) return null;
1199
+ text = readFileSync(resolved, "utf8");
1200
+ } catch {
1201
+ return null;
1202
+ }
1203
+ const all = text.split("\n");
1204
+ if (all.at(-1) === "") all.pop();
1205
+ const startLine = Math.max(1, line - context);
1206
+ const endLine = Math.min(all.length, line + context);
1207
+ if (startLine > all.length) return null;
1208
+ return {
1209
+ file: path.relative(path.resolve(root), resolved),
1210
+ line,
1211
+ startLine,
1212
+ lines: all.slice(startLine - 1, endLine)
1213
+ };
1214
+ }
1215
+
1124
1216
  //#endregion
1125
1217
  //#region src/server/http.ts
1126
1218
  function sendOtlpError(res, req, e) {
@@ -1184,6 +1276,7 @@ function getWidgetJs() {
1184
1276
  }
1185
1277
  function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1186
1278
  const loopbackOnly = options.loopbackOnly ?? true;
1279
+ const sourceRoot = options.sourceRoot;
1187
1280
  const fullpageHtml = renderFullpageHtml(options.title);
1188
1281
  httpServer.on("request", async (req, res) => {
1189
1282
  if (req.headers.upgrade?.toLowerCase() === "websocket") return;
@@ -1224,6 +1317,31 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
1224
1317
  res.end(DEVTOOLS_FAVICON_SVG);
1225
1318
  return;
1226
1319
  }
1320
+ if (req.method === "GET" && url.split("?")[0] === "/source") {
1321
+ if (!sourceRoot) {
1322
+ sendJson(res, 404, { error: "Not found" });
1323
+ return;
1324
+ }
1325
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
1326
+ sendJson(res, 403, { error: "Forbidden" });
1327
+ return;
1328
+ }
1329
+ const query = new URL(url, "http://localhost").searchParams;
1330
+ const file = query.get("file");
1331
+ const line = Number(query.get("line"));
1332
+ const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
1333
+ if (!file || !Number.isInteger(line) || line < 1) {
1334
+ sendJson(res, 400, { error: "file and a positive integer line are required" });
1335
+ return;
1336
+ }
1337
+ const window = readSourceWindow(sourceRoot, file, line, context);
1338
+ if (window === null) {
1339
+ sendJson(res, 404, { error: "Not found" });
1340
+ return;
1341
+ }
1342
+ sendJson(res, 200, { ...window });
1343
+ return;
1344
+ }
1227
1345
  if (req.method === "GET" && url === "/healthz") {
1228
1346
  sendJson(res, 200, {
1229
1347
  ok: true,
@@ -1296,4 +1414,4 @@ function createDevtoolsHttpServer(devtools, _options = {}) {
1296
1414
  }
1297
1415
 
1298
1416
  //#endregion
1299
- export { appendWithLimit as _, decodeOtlpLogsRequest as a, ErrorAggregator as b, isProtobufContentType as c, DevtoolsServer as d, allowSensitiveRequest as f, appendManyWithLimit as g, originIsLoopback as h, probePortHolder as i, parseOtlpLogs as l, isLoopbackHostname as m, createDevtoolsHttpServer as n, decodeOtlpMetricsRequest as o, hostHeaderIsLoopback as p, DEVTOOLS_IDENTITY as r, decodeOtlpTraceRequest as s, attachDevtoolsRoutes as t, parseOtlpTraces as u, applyTelemetryLimits as v, resolveTelemetryLimits as y };
1417
+ export { appendManyWithLimit as _, probePortHolder as a, resolveTelemetryLimits as b, decodeOtlpTraceRequest as c, parseOtlpTraces as d, DevtoolsServer as f, originIsLoopback as g, isLoopbackHostname as h, DEVTOOLS_IDENTITY as i, isProtobufContentType as l, hostHeaderIsLoopback as m, createDevtoolsHttpServer as n, decodeOtlpLogsRequest as o, allowSensitiveRequest as p, resolveSourceRoot as r, decodeOtlpMetricsRequest as s, attachDevtoolsRoutes as t, parseOtlpLogs as u, appendWithLimit as v, ErrorAggregator as x, applyTelemetryLimits as y };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_http = require('./http-53ldVU8k.cjs');
2
+ const require_http = require('./http-D44dITGS.cjs');
3
3
  const require_listen = require('./listen-DGzVi7DE.cjs');
4
4
  const require_server_exporter = require('./server/exporter.cjs');
5
5
  const require_server_log_exporter = require('./server/log-exporter.cjs');
@@ -21,12 +21,19 @@ function createDevtools(options = {}) {
21
21
  maxLogCount: options.maxLogCount,
22
22
  maxMetricCount: options.maxMetricCount
23
23
  });
24
- require_http.attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
24
+ const sourceRoot = require_http.resolveSourceRoot(options.sourceRoot === false ? "false" : options.sourceRoot ?? process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
25
+ require_http.attachDevtoolsRoutes(httpServer, wsServer, {
26
+ loopbackOnly,
27
+ sourceRoot
28
+ });
25
29
  const listeners = require_listen.listenLoopbackDualStack({
26
30
  primary: httpServer,
27
31
  port,
28
32
  host,
29
- attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
33
+ attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, {
34
+ loopbackOnly,
35
+ sourceRoot
36
+ })
30
37
  });
31
38
  if (options.verbose) listeners.ready.then(({ warnings }) => {
32
39
  for (const w of warnings) console.warn(`[autotel-devtools] ${w}`);
package/dist/index.d.cts CHANGED
@@ -12,6 +12,14 @@ interface CreateDevtoolsOptions {
12
12
  maxTraceCount?: number;
13
13
  maxLogCount?: number;
14
14
  maxMetricCount?: number;
15
+ /**
16
+ * Project root the Errors tab may read source from, so a stack frame can show
17
+ * the line that threw.
18
+ *
19
+ * Same default as the CLI: the working directory on a loopback bind, off
20
+ * otherwise. `false` disables it. See `resolveSourceRoot`.
21
+ */
22
+ sourceRoot?: string | false;
15
23
  }
16
24
  interface DevtoolsInstance {
17
25
  server: DevtoolsServer;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,14 @@ interface CreateDevtoolsOptions {
12
12
  maxTraceCount?: number;
13
13
  maxLogCount?: number;
14
14
  maxMetricCount?: number;
15
+ /**
16
+ * Project root the Errors tab may read source from, so a stack frame can show
17
+ * the line that threw.
18
+ *
19
+ * Same default as the CLI: the working directory on a loopback bind, off
20
+ * otherwise. `false` disables it. See `resolveSourceRoot`.
21
+ */
22
+ sourceRoot?: string | false;
15
23
  }
16
24
  interface DevtoolsInstance {
17
25
  server: DevtoolsServer;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b as ErrorAggregator, d as DevtoolsServer, p as hostHeaderIsLoopback, t as attachDevtoolsRoutes } from "./http-B8rl56bF.js";
1
+ import { f as DevtoolsServer, m as hostHeaderIsLoopback, r as resolveSourceRoot, t as attachDevtoolsRoutes, x as ErrorAggregator } from "./http-Ddn7gT0R.js";
2
2
  import { t as listenLoopbackDualStack } from "./listen-NVMbBWHw.js";
3
3
  import { DevtoolsSpanExporter } from "./server/exporter.js";
4
4
  import { DevtoolsLogExporter } from "./server/log-exporter.js";
@@ -20,12 +20,19 @@ function createDevtools(options = {}) {
20
20
  maxLogCount: options.maxLogCount,
21
21
  maxMetricCount: options.maxMetricCount
22
22
  });
23
- attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
23
+ const sourceRoot = resolveSourceRoot(options.sourceRoot === false ? "false" : options.sourceRoot ?? process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
24
+ attachDevtoolsRoutes(httpServer, wsServer, {
25
+ loopbackOnly,
26
+ sourceRoot
27
+ });
24
28
  const listeners = listenLoopbackDualStack({
25
29
  primary: httpServer,
26
30
  port,
27
31
  host,
28
- attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
32
+ attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, {
33
+ loopbackOnly,
34
+ sourceRoot
35
+ })
29
36
  });
30
37
  if (options.verbose) listeners.ready.then(({ warnings }) => {
31
38
  for (const w of warnings) console.warn(`[autotel-devtools] ${w}`);
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_http = require('../http-53ldVU8k.cjs');
2
+ const require_http = require('../http-D44dITGS.cjs');
3
3
  const require_server_exporter = require('./exporter.cjs');
4
4
  const require_server_log_exporter = require('./log-exporter.cjs');
5
5
  const require_server_remote_exporter = require('./remote-exporter.cjs');
@@ -16,6 +16,15 @@ interface DevtoolsRoutesOptions {
16
16
  /** Browser tab title for the fullpage UI. Without it every dashboard reads
17
17
  * `autotel-devtools`, which is unhelpful with several running at once. */
18
18
  title?: string;
19
+ /**
20
+ * Project root that `GET /source` may read from, so a stack frame can show
21
+ * the line that threw.
22
+ *
23
+ * Opt-in and absent by default: without it the route 404s and devtools never
24
+ * touches the filesystem. Reading is confined to this directory, symlinks
25
+ * included — see `resolveWithinRoot`.
26
+ */
27
+ sourceRoot?: string;
19
28
  }
20
29
  declare function attachDevtoolsRoutes(httpServer: Server, devtools: DevtoolsServer, options?: DevtoolsRoutesOptions): void;
21
30
  declare function createDevtoolsHttpServer(devtools: DevtoolsServer, _options?: HttpServerOptions): Server;
@@ -16,6 +16,15 @@ interface DevtoolsRoutesOptions {
16
16
  /** Browser tab title for the fullpage UI. Without it every dashboard reads
17
17
  * `autotel-devtools`, which is unhelpful with several running at once. */
18
18
  title?: string;
19
+ /**
20
+ * Project root that `GET /source` may read from, so a stack frame can show
21
+ * the line that threw.
22
+ *
23
+ * Opt-in and absent by default: without it the route 404s and devtools never
24
+ * touches the filesystem. Reading is confined to this directory, symlinks
25
+ * included — see `resolveWithinRoot`.
26
+ */
27
+ sourceRoot?: string;
19
28
  }
20
29
  declare function attachDevtoolsRoutes(httpServer: Server, devtools: DevtoolsServer, options?: DevtoolsRoutesOptions): void;
21
30
  declare function createDevtoolsHttpServer(devtools: DevtoolsServer, _options?: HttpServerOptions): Server;
@@ -1,4 +1,4 @@
1
- import { _ as appendWithLimit, a as decodeOtlpLogsRequest, b as ErrorAggregator, c as isProtobufContentType, d as DevtoolsServer, f as allowSensitiveRequest, g as appendManyWithLimit, h as originIsLoopback, i as probePortHolder, l as parseOtlpLogs, m as isLoopbackHostname, n as createDevtoolsHttpServer, o as decodeOtlpMetricsRequest, p as hostHeaderIsLoopback, r as DEVTOOLS_IDENTITY, s as decodeOtlpTraceRequest, t as attachDevtoolsRoutes, u as parseOtlpTraces, v as applyTelemetryLimits, y as resolveTelemetryLimits } from "../http-B8rl56bF.js";
1
+ import { _ as appendManyWithLimit, a as probePortHolder, b as resolveTelemetryLimits, c as decodeOtlpTraceRequest, d as parseOtlpTraces, f as DevtoolsServer, g as originIsLoopback, h as isLoopbackHostname, i as DEVTOOLS_IDENTITY, l as isProtobufContentType, m as hostHeaderIsLoopback, n as createDevtoolsHttpServer, o as decodeOtlpLogsRequest, p as allowSensitiveRequest, s as decodeOtlpMetricsRequest, t as attachDevtoolsRoutes, u as parseOtlpLogs, v as appendWithLimit, x as ErrorAggregator, y as applyTelemetryLimits } from "../http-Ddn7gT0R.js";
2
2
  import { DevtoolsSpanExporter } from "./exporter.js";
3
3
  import { DevtoolsLogExporter } from "./log-exporter.js";
4
4
  import { DevtoolsRemoteExporter } from "./remote-exporter.js";