@neat.is/core 0.4.6 → 0.4.8

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.cjs CHANGED
@@ -1017,6 +1017,24 @@ function isFrontierNode(graph, nodeId) {
1017
1017
  const attrs = graph.getNodeAttributes(nodeId);
1018
1018
  return attrs.type === import_types.NodeType.FrontierNode;
1019
1019
  }
1020
+ function resolveOwningService(graph, nodeId) {
1021
+ if (!graph.hasNode(nodeId)) return null;
1022
+ const attrs = graph.getNodeAttributes(nodeId);
1023
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1024
+ return { id: nodeId, svc: attrs };
1025
+ }
1026
+ if (attrs.type === import_types.NodeType.FileNode) {
1027
+ for (const edgeId of graph.inboundEdges(nodeId)) {
1028
+ const e = graph.getEdgeAttributes(edgeId);
1029
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1030
+ const owner = graph.getNodeAttributes(e.source);
1031
+ if (owner.type === import_types.NodeType.ServiceNode) {
1032
+ return { id: e.source, svc: owner };
1033
+ }
1034
+ }
1035
+ }
1036
+ return null;
1037
+ }
1020
1038
  function bestEdgeBySource(graph, edgeIds) {
1021
1039
  const best = /* @__PURE__ */ new Map();
1022
1040
  for (const id of edgeIds) {
@@ -1123,9 +1141,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1123
1141
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1124
1142
  if (candidatePairs.length === 0) return null;
1125
1143
  for (const id of walk.path) {
1126
- const attrs = graph.getNodeAttributes(id);
1127
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1128
- const svc = attrs;
1144
+ const owner = resolveOwningService(graph, id);
1145
+ if (!owner) continue;
1146
+ const { id: serviceId3, svc } = owner;
1129
1147
  const deps = svc.dependencies ?? {};
1130
1148
  for (const pair of candidatePairs) {
1131
1149
  const declared = deps[pair.driver];
@@ -1138,7 +1156,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1138
1156
  );
1139
1157
  if (!result.compatible) {
1140
1158
  return {
1141
- rootCauseNode: id,
1159
+ rootCauseNode: serviceId3,
1142
1160
  rootCauseReason: result.reason ?? "incompatible driver",
1143
1161
  ...result.minDriverVersion ? {
1144
1162
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1151,9 +1169,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1151
1169
  }
1152
1170
  function serviceRootCauseShape(graph, _origin, walk) {
1153
1171
  for (const id of walk.path) {
1154
- const attrs = graph.getNodeAttributes(id);
1155
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1156
- const svc = attrs;
1172
+ const owner = resolveOwningService(graph, id);
1173
+ if (!owner) continue;
1174
+ const { id: serviceId3, svc } = owner;
1157
1175
  const deps = svc.dependencies ?? {};
1158
1176
  const serviceNodeEngine = svc.nodeEngine;
1159
1177
  for (const constraint of nodeEngineConstraints()) {
@@ -1162,7 +1180,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1162
1180
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1163
1181
  if (!result.compatible && result.reason) {
1164
1182
  return {
1165
- rootCauseNode: id,
1183
+ rootCauseNode: serviceId3,
1166
1184
  rootCauseReason: result.reason,
1167
1185
  ...result.requiredNodeVersion ? {
1168
1186
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1177,7 +1195,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1177
1195
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1178
1196
  if (!result.compatible && result.reason) {
1179
1197
  return {
1180
- rootCauseNode: id,
1198
+ rootCauseNode: serviceId3,
1181
1199
  rootCauseReason: result.reason,
1182
1200
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1183
1201
  };
@@ -1186,9 +1204,15 @@ function serviceRootCauseShape(graph, _origin, walk) {
1186
1204
  }
1187
1205
  return null;
1188
1206
  }
1207
+ function fileRootCauseShape(graph, origin, walk) {
1208
+ const owner = resolveOwningService(graph, origin.id);
1209
+ if (!owner) return null;
1210
+ return serviceRootCauseShape(graph, owner.svc, walk);
1211
+ }
1189
1212
  var rootCauseShapes = {
1190
1213
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
1191
- [import_types.NodeType.ServiceNode]: serviceRootCauseShape
1214
+ [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1215
+ [import_types.NodeType.FileNode]: fileRootCauseShape
1192
1216
  };
1193
1217
  function getRootCause(graph, errorNodeId, errorEvent) {
1194
1218
  if (!graph.hasNode(errorNodeId)) return null;
@@ -1673,6 +1697,86 @@ function hostFromUrl(u) {
1673
1697
  function pickAddress(span) {
1674
1698
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1675
1699
  }
1700
+ var CODE_FILEPATH_ATTR = "code.filepath";
1701
+ var CODE_LINENO_ATTR = "code.lineno";
1702
+ var CODE_FUNCTION_ATTR = "code.function";
1703
+ function toPosix(p) {
1704
+ return p.split("\\").join("/");
1705
+ }
1706
+ function languageForExt(relPath) {
1707
+ const dot = relPath.lastIndexOf(".");
1708
+ if (dot === -1) return void 0;
1709
+ switch (relPath.slice(dot).toLowerCase()) {
1710
+ case ".py":
1711
+ return "python";
1712
+ case ".ts":
1713
+ case ".tsx":
1714
+ return "typescript";
1715
+ case ".js":
1716
+ case ".jsx":
1717
+ case ".mjs":
1718
+ case ".cjs":
1719
+ return "javascript";
1720
+ default:
1721
+ return void 0;
1722
+ }
1723
+ }
1724
+ function relPathForRuntimeFile(filepath, serviceNode) {
1725
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1726
+ const root = serviceNode?.repoPath;
1727
+ if (root && root !== "." && root.length > 0) {
1728
+ const rootPosix = toPosix(root);
1729
+ const anchor = `/${rootPosix}/`;
1730
+ const idx = p.lastIndexOf(anchor);
1731
+ if (idx !== -1) return p.slice(idx + anchor.length);
1732
+ const base = rootPosix.split("/").filter(Boolean).pop();
1733
+ if (base) {
1734
+ const baseAnchor = `/${base}/`;
1735
+ const bidx = p.lastIndexOf(baseAnchor);
1736
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1737
+ }
1738
+ }
1739
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1740
+ return p.length > 0 ? p : null;
1741
+ }
1742
+ function callSiteFromSpan(span, serviceNode) {
1743
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1744
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1745
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1746
+ if (!relPath) return null;
1747
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1748
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1749
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1750
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1751
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1752
+ }
1753
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1754
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1755
+ if (!graph.hasNode(fileNodeId)) {
1756
+ const language = languageForExt(callSite.relPath);
1757
+ const node = {
1758
+ id: fileNodeId,
1759
+ type: import_types3.NodeType.FileNode,
1760
+ service: serviceName,
1761
+ path: callSite.relPath,
1762
+ ...language ? { language } : {},
1763
+ discoveredVia: "otel"
1764
+ };
1765
+ graph.addNode(fileNodeId, node);
1766
+ }
1767
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1768
+ if (!graph.hasEdge(containsId)) {
1769
+ const edge = {
1770
+ id: containsId,
1771
+ source: serviceNodeId,
1772
+ target: fileNodeId,
1773
+ type: import_types3.EdgeType.CONTAINS,
1774
+ provenance: import_types3.Provenance.OBSERVED
1775
+ };
1776
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1777
+ }
1778
+ return fileNodeId;
1779
+ }
1676
1780
  function makeObservedEdgeId(type, source, target) {
1677
1781
  return (0, import_types3.observedEdgeId)(source, target, type);
1678
1782
  }
@@ -1915,6 +2019,9 @@ async function handleSpan(ctx, span) {
1915
2019
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1916
2020
  const isError = span.statusCode === 2;
1917
2021
  cacheSpanService(span, nowMs);
2022
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2023
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
2024
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1918
2025
  let affectedNode = sourceId;
1919
2026
  if (span.dbSystem) {
1920
2027
  const host = pickAddress(span);
@@ -1924,7 +2031,7 @@ async function handleSpan(ctx, span) {
1924
2031
  const result = upsertObservedEdge(
1925
2032
  ctx.graph,
1926
2033
  import_types3.EdgeType.CONNECTS_TO,
1927
- sourceId,
2034
+ observedSource(),
1928
2035
  targetId,
1929
2036
  ts,
1930
2037
  isError
@@ -1940,7 +2047,7 @@ async function handleSpan(ctx, span) {
1940
2047
  upsertObservedEdge(
1941
2048
  ctx.graph,
1942
2049
  import_types3.EdgeType.CALLS,
1943
- sourceId,
2050
+ observedSource(),
1944
2051
  targetId,
1945
2052
  ts,
1946
2053
  isError
@@ -1952,7 +2059,7 @@ async function handleSpan(ctx, span) {
1952
2059
  upsertObservedEdge(
1953
2060
  ctx.graph,
1954
2061
  import_types3.EdgeType.CALLS,
1955
- sourceId,
2062
+ observedSource(),
1956
2063
  frontierNodeId,
1957
2064
  ts,
1958
2065
  isError
@@ -3583,7 +3690,7 @@ async function addConfigNodes(graph, services, scanPath) {
3583
3690
 
3584
3691
  // src/extract/calls/index.ts
3585
3692
  init_cjs_shims();
3586
- var import_types14 = require("@neat.is/types");
3693
+ var import_types15 = require("@neat.is/types");
3587
3694
 
3588
3695
  // src/extract/calls/http.ts
3589
3696
  init_cjs_shims();
@@ -3591,12 +3698,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3591
3698
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3592
3699
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3593
3700
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3594
- var import_types9 = require("@neat.is/types");
3701
+ var import_types10 = require("@neat.is/types");
3595
3702
 
3596
3703
  // src/extract/calls/shared.ts
3597
3704
  init_cjs_shims();
3598
3705
  var import_node_fs13 = require("fs");
3599
3706
  var import_node_path19 = __toESM(require("path"), 1);
3707
+ var import_types9 = require("@neat.is/types");
3600
3708
  async function walkSourceFiles(dir) {
3601
3709
  const out = [];
3602
3710
  async function walk(current) {
@@ -3636,6 +3744,58 @@ function snippet(text, line) {
3636
3744
  const lines = text.split("\n");
3637
3745
  return (lines[line - 1] ?? "").trim();
3638
3746
  }
3747
+ function toPosix2(p) {
3748
+ return p.split("\\").join("/");
3749
+ }
3750
+ function languageForPath(relPath) {
3751
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3752
+ case ".py":
3753
+ return "python";
3754
+ case ".ts":
3755
+ case ".tsx":
3756
+ return "typescript";
3757
+ case ".js":
3758
+ case ".jsx":
3759
+ case ".mjs":
3760
+ case ".cjs":
3761
+ return "javascript";
3762
+ default:
3763
+ return void 0;
3764
+ }
3765
+ }
3766
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3767
+ let nodesAdded = 0;
3768
+ let edgesAdded = 0;
3769
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3770
+ if (!graph.hasNode(fileNodeId)) {
3771
+ const language = languageForPath(relPath);
3772
+ const node = {
3773
+ id: fileNodeId,
3774
+ type: import_types9.NodeType.FileNode,
3775
+ service: serviceName,
3776
+ path: relPath,
3777
+ ...language ? { language } : {},
3778
+ discoveredVia: "static"
3779
+ };
3780
+ graph.addNode(fileNodeId, node);
3781
+ nodesAdded++;
3782
+ }
3783
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3784
+ if (!graph.hasEdge(containsId)) {
3785
+ const edge = {
3786
+ id: containsId,
3787
+ source: serviceNodeId,
3788
+ target: fileNodeId,
3789
+ type: import_types9.EdgeType.CONTAINS,
3790
+ provenance: import_types9.Provenance.EXTRACTED,
3791
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3792
+ evidence: { file: relPath }
3793
+ };
3794
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3795
+ edgesAdded++;
3796
+ }
3797
+ return { fileNodeId, nodesAdded, edgesAdded };
3798
+ }
3639
3799
 
3640
3800
  // src/extract/calls/http.ts
3641
3801
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3669,16 +3829,16 @@ function callsFromSource(source, parser, knownHosts) {
3669
3829
  const tree = parser.parse(source);
3670
3830
  const literals = [];
3671
3831
  collectStringLiterals(tree.rootNode, literals);
3672
- const targets = /* @__PURE__ */ new Set();
3832
+ const out = [];
3673
3833
  for (const lit of literals) {
3674
3834
  if (isInsideJsxExternalLink(lit.node)) continue;
3675
3835
  for (const host of knownHosts) {
3676
3836
  if (urlMatchesHost(lit.text, host)) {
3677
- targets.add(host);
3837
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3678
3838
  }
3679
3839
  }
3680
3840
  }
3681
- return targets;
3841
+ return out;
3682
3842
  }
3683
3843
  function makeJsParser() {
3684
3844
  const p = new import_tree_sitter.default();
@@ -3701,71 +3861,78 @@ async function addHttpCallEdges(graph, services) {
3701
3861
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3702
3862
  hostToNodeId.set(service.pkg.name, service.node.id);
3703
3863
  }
3864
+ let nodesAdded = 0;
3704
3865
  let edgesAdded = 0;
3705
3866
  for (const service of services) {
3706
3867
  const files = await loadSourceFiles(service.dir);
3707
- const seenTargets = /* @__PURE__ */ new Map();
3868
+ const seen = /* @__PURE__ */ new Set();
3708
3869
  for (const file of files) {
3709
3870
  if (isTestPath(file.path)) continue;
3710
3871
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3711
- let targets;
3872
+ let sites;
3712
3873
  try {
3713
- targets = callsFromSource(file.content, parser, knownHosts);
3874
+ sites = callsFromSource(file.content, parser, knownHosts);
3714
3875
  } catch (err) {
3715
3876
  recordExtractionError("http call extraction", file.path, err);
3716
3877
  continue;
3717
3878
  }
3718
- for (const t of targets) {
3719
- const targetId = hostToNodeId.get(t);
3879
+ if (sites.length === 0) continue;
3880
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3881
+ for (const site of sites) {
3882
+ const targetId = hostToNodeId.get(site.host);
3720
3883
  if (!targetId || targetId === service.node.id) continue;
3721
- if (!seenTargets.has(targetId)) {
3722
- seenTargets.set(targetId, { file: file.path, host: t });
3884
+ const dedupKey = `${relFile}|${targetId}`;
3885
+ if (seen.has(dedupKey)) continue;
3886
+ seen.add(dedupKey);
3887
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3888
+ const ev = {
3889
+ file: relFile,
3890
+ line: site.line,
3891
+ snippet: snippet(file.content, site.line)
3892
+ };
3893
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3894
+ graph,
3895
+ service.pkg.name,
3896
+ service.node.id,
3897
+ relFile
3898
+ );
3899
+ nodesAdded += n;
3900
+ edgesAdded += e;
3901
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3902
+ noteExtractedDropped({
3903
+ source: fileNodeId,
3904
+ target: targetId,
3905
+ type: import_types10.EdgeType.CALLS,
3906
+ confidence,
3907
+ confidenceKind: "hostname-shape-match",
3908
+ evidence: ev
3909
+ });
3910
+ continue;
3911
+ }
3912
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3913
+ if (!graph.hasEdge(edgeId)) {
3914
+ const edge = {
3915
+ id: edgeId,
3916
+ source: fileNodeId,
3917
+ target: targetId,
3918
+ type: import_types10.EdgeType.CALLS,
3919
+ provenance: import_types10.Provenance.EXTRACTED,
3920
+ confidence,
3921
+ evidence: ev
3922
+ };
3923
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3924
+ edgesAdded++;
3723
3925
  }
3724
- }
3725
- }
3726
- for (const [targetId, evidenceFile] of seenTargets) {
3727
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3728
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3729
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3730
- const ev = {
3731
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3732
- line,
3733
- snippet: snippet(fileContent, line)
3734
- };
3735
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3736
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3737
- noteExtractedDropped({
3738
- source: service.node.id,
3739
- target: targetId,
3740
- type: import_types9.EdgeType.CALLS,
3741
- confidence,
3742
- confidenceKind: "hostname-shape-match",
3743
- evidence: ev
3744
- });
3745
- continue;
3746
- }
3747
- const edge = {
3748
- id: edgeId,
3749
- source: service.node.id,
3750
- target: targetId,
3751
- type: import_types9.EdgeType.CALLS,
3752
- provenance: import_types9.Provenance.EXTRACTED,
3753
- confidence,
3754
- evidence: ev
3755
- };
3756
- if (!graph.hasEdge(edge.id)) {
3757
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3758
- edgesAdded++;
3759
3926
  }
3760
3927
  }
3761
3928
  }
3762
- return edgesAdded;
3929
+ return { nodesAdded, edgesAdded };
3763
3930
  }
3764
3931
 
3765
3932
  // src/extract/calls/kafka.ts
3766
3933
  init_cjs_shims();
3767
3934
  var import_node_path21 = __toESM(require("path"), 1);
3768
- var import_types10 = require("@neat.is/types");
3935
+ var import_types11 = require("@neat.is/types");
3769
3936
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3770
3937
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3771
3938
  function findAll(re, text) {
@@ -3786,7 +3953,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3786
3953
  seen.add(key);
3787
3954
  const line = lineOf(file.content, topic);
3788
3955
  out.push({
3789
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3956
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3790
3957
  name: topic,
3791
3958
  kind: "kafka-topic",
3792
3959
  edgeType,
@@ -3809,7 +3976,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3809
3976
  // src/extract/calls/redis.ts
3810
3977
  init_cjs_shims();
3811
3978
  var import_node_path22 = __toESM(require("path"), 1);
3812
- var import_types11 = require("@neat.is/types");
3979
+ var import_types12 = require("@neat.is/types");
3813
3980
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3814
3981
  function redisEndpointsFromFile(file, serviceDir) {
3815
3982
  const out = [];
@@ -3822,7 +3989,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3822
3989
  seen.add(host);
3823
3990
  const line = lineOf(file.content, host);
3824
3991
  out.push({
3825
- infraId: (0, import_types11.infraId)("redis", host),
3992
+ infraId: (0, import_types12.infraId)("redis", host),
3826
3993
  name: host,
3827
3994
  kind: "redis",
3828
3995
  edgeType: "CALLS",
@@ -3843,7 +4010,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3843
4010
  // src/extract/calls/aws.ts
3844
4011
  init_cjs_shims();
3845
4012
  var import_node_path23 = __toESM(require("path"), 1);
3846
- var import_types12 = require("@neat.is/types");
4013
+ var import_types13 = require("@neat.is/types");
3847
4014
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3848
4015
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3849
4016
  function hasMarker(text, markers) {
@@ -3867,7 +4034,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3867
4034
  seen.add(key);
3868
4035
  const line = lineOf(file.content, name);
3869
4036
  out.push({
3870
- infraId: (0, import_types12.infraId)(kind, name),
4037
+ infraId: (0, import_types13.infraId)(kind, name),
3871
4038
  name,
3872
4039
  kind,
3873
4040
  edgeType: "CALLS",
@@ -3902,7 +4069,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3902
4069
  // src/extract/calls/grpc.ts
3903
4070
  init_cjs_shims();
3904
4071
  var import_node_path24 = __toESM(require("path"), 1);
3905
- var import_types13 = require("@neat.is/types");
4072
+ var import_types14 = require("@neat.is/types");
3906
4073
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3907
4074
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3908
4075
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3950,7 +4117,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3950
4117
  const { kind } = classified;
3951
4118
  const line = lineOf(file.content, m[0]);
3952
4119
  out.push({
3953
- infraId: (0, import_types13.infraId)(kind, name),
4120
+ infraId: (0, import_types14.infraId)(kind, name),
3954
4121
  name,
3955
4122
  kind,
3956
4123
  edgeType: "CALLS",
@@ -3972,11 +4139,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3972
4139
  function edgeTypeFromEndpoint(ep) {
3973
4140
  switch (ep.edgeType) {
3974
4141
  case "PUBLISHES_TO":
3975
- return import_types14.EdgeType.PUBLISHES_TO;
4142
+ return import_types15.EdgeType.PUBLISHES_TO;
3976
4143
  case "CONSUMES_FROM":
3977
- return import_types14.EdgeType.CONSUMES_FROM;
4144
+ return import_types15.EdgeType.CONSUMES_FROM;
3978
4145
  default:
3979
- return import_types14.EdgeType.CALLS;
4146
+ return import_types15.EdgeType.CALLS;
3980
4147
  }
3981
4148
  }
3982
4149
  function isAwsKind(kind) {
@@ -4003,7 +4170,7 @@ async function addExternalEndpointEdges(graph, services) {
4003
4170
  if (!graph.hasNode(ep.infraId)) {
4004
4171
  const node = {
4005
4172
  id: ep.infraId,
4006
- type: import_types14.NodeType.InfraNode,
4173
+ type: import_types15.NodeType.InfraNode,
4007
4174
  name: ep.name,
4008
4175
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4009
4176
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4015,13 +4182,19 @@ async function addExternalEndpointEdges(graph, services) {
4015
4182
  nodesAdded++;
4016
4183
  }
4017
4184
  const edgeType = edgeTypeFromEndpoint(ep);
4018
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
4019
- if (seenEdges.has(edgeId)) continue;
4020
- seenEdges.add(edgeId);
4021
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
4022
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4185
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4186
+ const relFile = toPosix2(ep.evidence.file);
4187
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4188
+ graph,
4189
+ service.pkg.name,
4190
+ service.node.id,
4191
+ relFile
4192
+ );
4193
+ nodesAdded += n;
4194
+ edgesAdded += e;
4195
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4023
4196
  noteExtractedDropped({
4024
- source: service.node.id,
4197
+ source: fileNodeId,
4025
4198
  target: ep.infraId,
4026
4199
  type: edgeType,
4027
4200
  confidence,
@@ -4030,13 +4203,16 @@ async function addExternalEndpointEdges(graph, services) {
4030
4203
  });
4031
4204
  continue;
4032
4205
  }
4206
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4207
+ if (seenEdges.has(edgeId)) continue;
4208
+ seenEdges.add(edgeId);
4033
4209
  if (!graph.hasEdge(edgeId)) {
4034
4210
  const edge = {
4035
4211
  id: edgeId,
4036
- source: service.node.id,
4212
+ source: fileNodeId,
4037
4213
  target: ep.infraId,
4038
4214
  type: edgeType,
4039
- provenance: import_types14.Provenance.EXTRACTED,
4215
+ provenance: import_types15.Provenance.EXTRACTED,
4040
4216
  confidence,
4041
4217
  evidence: ep.evidence
4042
4218
  };
@@ -4048,11 +4224,11 @@ async function addExternalEndpointEdges(graph, services) {
4048
4224
  return { nodesAdded, edgesAdded };
4049
4225
  }
4050
4226
  async function addCallEdges(graph, services) {
4051
- const httpEdges = await addHttpCallEdges(graph, services);
4227
+ const http = await addHttpCallEdges(graph, services);
4052
4228
  const ext = await addExternalEndpointEdges(graph, services);
4053
4229
  return {
4054
- nodesAdded: ext.nodesAdded,
4055
- edgesAdded: httpEdges + ext.edgesAdded
4230
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
4231
+ edgesAdded: http.edgesAdded + ext.edgesAdded
4056
4232
  };
4057
4233
  }
4058
4234
 
@@ -4062,15 +4238,15 @@ init_cjs_shims();
4062
4238
  // src/extract/infra/docker-compose.ts
4063
4239
  init_cjs_shims();
4064
4240
  var import_node_path25 = __toESM(require("path"), 1);
4065
- var import_types16 = require("@neat.is/types");
4241
+ var import_types17 = require("@neat.is/types");
4066
4242
 
4067
4243
  // src/extract/infra/shared.ts
4068
4244
  init_cjs_shims();
4069
- var import_types15 = require("@neat.is/types");
4245
+ var import_types16 = require("@neat.is/types");
4070
4246
  function makeInfraNode(kind, name, provider = "self", extras) {
4071
4247
  return {
4072
- id: (0, import_types15.infraId)(kind, name),
4073
- type: import_types15.NodeType.InfraNode,
4248
+ id: (0, import_types16.infraId)(kind, name),
4249
+ type: import_types16.NodeType.InfraNode,
4074
4250
  name,
4075
4251
  provider,
4076
4252
  kind,
@@ -4149,15 +4325,15 @@ async function addComposeInfra(graph, scanPath, services) {
4149
4325
  for (const dep of dependsOnList(svc.depends_on)) {
4150
4326
  const targetId = composeNameToNodeId.get(dep);
4151
4327
  if (!targetId) continue;
4152
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4328
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4153
4329
  if (graph.hasEdge(edgeId)) continue;
4154
4330
  const edge = {
4155
4331
  id: edgeId,
4156
4332
  source: sourceId,
4157
4333
  target: targetId,
4158
- type: import_types16.EdgeType.DEPENDS_ON,
4159
- provenance: import_types16.Provenance.EXTRACTED,
4160
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4334
+ type: import_types17.EdgeType.DEPENDS_ON,
4335
+ provenance: import_types17.Provenance.EXTRACTED,
4336
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4161
4337
  evidence: { file: evidenceFile }
4162
4338
  };
4163
4339
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4171,7 +4347,7 @@ async function addComposeInfra(graph, scanPath, services) {
4171
4347
  init_cjs_shims();
4172
4348
  var import_node_path26 = __toESM(require("path"), 1);
4173
4349
  var import_node_fs14 = require("fs");
4174
- var import_types17 = require("@neat.is/types");
4350
+ var import_types18 = require("@neat.is/types");
4175
4351
  function runtimeImage(content) {
4176
4352
  const lines = content.split("\n");
4177
4353
  let last = null;
@@ -4210,15 +4386,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4210
4386
  graph.addNode(node.id, node);
4211
4387
  nodesAdded++;
4212
4388
  }
4213
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4389
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4214
4390
  if (!graph.hasEdge(edgeId)) {
4215
4391
  const edge = {
4216
4392
  id: edgeId,
4217
4393
  source: service.node.id,
4218
4394
  target: node.id,
4219
- type: import_types17.EdgeType.RUNS_ON,
4220
- provenance: import_types17.Provenance.EXTRACTED,
4221
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4395
+ type: import_types18.EdgeType.RUNS_ON,
4396
+ provenance: import_types18.Provenance.EXTRACTED,
4397
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4222
4398
  evidence: {
4223
4399
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4224
4400
  }
@@ -4346,13 +4522,24 @@ var import_node_path30 = __toESM(require("path"), 1);
4346
4522
  init_cjs_shims();
4347
4523
  var import_node_fs17 = require("fs");
4348
4524
  var import_node_path29 = __toESM(require("path"), 1);
4349
- var import_types18 = require("@neat.is/types");
4525
+ var import_types19 = require("@neat.is/types");
4526
+ function dropOrphanedFileNodes(graph) {
4527
+ const orphans = [];
4528
+ graph.forEachNode((id, attrs) => {
4529
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4530
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4531
+ orphans.push(id);
4532
+ }
4533
+ });
4534
+ for (const id of orphans) graph.dropNode(id);
4535
+ return orphans.length;
4536
+ }
4350
4537
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4351
4538
  const toDrop = [];
4352
4539
  const bases = [scanPath, ...serviceDirs];
4353
4540
  graph.forEachEdge((id, attrs) => {
4354
4541
  const edge = attrs;
4355
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4542
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4356
4543
  const evidenceFile = edge.evidence?.file;
4357
4544
  if (!evidenceFile) return;
4358
4545
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4363,6 +4550,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4363
4550
  if (!found) toDrop.push(id);
4364
4551
  });
4365
4552
  for (const id of toDrop) graph.dropEdge(id);
4553
+ dropOrphanedFileNodes(graph);
4366
4554
  return toDrop.length;
4367
4555
  }
4368
4556
 
@@ -4432,7 +4620,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4432
4620
  init_cjs_shims();
4433
4621
  var import_node_fs18 = require("fs");
4434
4622
  var import_node_path31 = __toESM(require("path"), 1);
4435
- var import_types19 = require("@neat.is/types");
4623
+ var import_types20 = require("@neat.is/types");
4436
4624
  var SCHEMA_VERSION = 4;
4437
4625
  function migrateV1ToV2(payload) {
4438
4626
  const nodes = payload.graph.nodes;
@@ -4454,12 +4642,12 @@ function migrateV2ToV3(payload) {
4454
4642
  for (const edge of edges) {
4455
4643
  const attrs = edge.attributes;
4456
4644
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4457
- attrs.provenance = import_types19.Provenance.OBSERVED;
4645
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4458
4646
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4459
4647
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4460
4648
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4461
4649
  if (type && source && target) {
4462
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4650
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4463
4651
  attrs.id = newId;
4464
4652
  if (edge.key) edge.key = newId;
4465
4653
  }
@@ -4545,11 +4733,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4545
4733
  init_cjs_shims();
4546
4734
  var import_fastify = __toESM(require("fastify"), 1);
4547
4735
  var import_cors = __toESM(require("@fastify/cors"), 1);
4548
- var import_types22 = require("@neat.is/types");
4736
+ var import_types23 = require("@neat.is/types");
4549
4737
 
4550
4738
  // src/divergences.ts
4551
4739
  init_cjs_shims();
4552
- var import_types20 = require("@neat.is/types");
4740
+ var import_types21 = require("@neat.is/types");
4553
4741
  function bucketKey(source, target, type) {
4554
4742
  return `${type}|${source}|${target}`;
4555
4743
  }
@@ -4557,22 +4745,22 @@ function bucketEdges(graph) {
4557
4745
  const buckets = /* @__PURE__ */ new Map();
4558
4746
  graph.forEachEdge((id, attrs) => {
4559
4747
  const e = attrs;
4560
- const parsed = (0, import_types20.parseEdgeId)(id);
4748
+ const parsed = (0, import_types21.parseEdgeId)(id);
4561
4749
  const provenance = parsed?.provenance ?? e.provenance;
4562
4750
  const key = bucketKey(e.source, e.target, e.type);
4563
4751
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4564
4752
  switch (provenance) {
4565
- case import_types20.Provenance.EXTRACTED:
4753
+ case import_types21.Provenance.EXTRACTED:
4566
4754
  cur.extracted = e;
4567
4755
  break;
4568
- case import_types20.Provenance.OBSERVED:
4756
+ case import_types21.Provenance.OBSERVED:
4569
4757
  cur.observed = e;
4570
4758
  break;
4571
- case import_types20.Provenance.INFERRED:
4759
+ case import_types21.Provenance.INFERRED:
4572
4760
  cur.inferred = e;
4573
4761
  break;
4574
4762
  default:
4575
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4763
+ if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
4576
4764
  }
4577
4765
  buckets.set(key, cur);
4578
4766
  });
@@ -4581,7 +4769,7 @@ function bucketEdges(graph) {
4581
4769
  function nodeIsFrontier(graph, nodeId) {
4582
4770
  if (!graph.hasNode(nodeId)) return false;
4583
4771
  const attrs = graph.getNodeAttributes(nodeId);
4584
- return attrs.type === import_types20.NodeType.FrontierNode;
4772
+ return attrs.type === import_types21.NodeType.FrontierNode;
4585
4773
  }
4586
4774
  function clampConfidence(n) {
4587
4775
  if (!Number.isFinite(n)) return 0;
@@ -4602,6 +4790,7 @@ function gradedConfidence(edge) {
4602
4790
  }
4603
4791
  function detectMissingDivergences(graph, bucket) {
4604
4792
  const out = [];
4793
+ if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
4605
4794
  if (bucket.extracted && !bucket.observed) {
4606
4795
  if (!nodeIsFrontier(graph, bucket.target)) {
4607
4796
  out.push({
@@ -4642,7 +4831,7 @@ function declaredHostFor(svc) {
4642
4831
  function hasExtractedConfiguredBy(graph, svcId) {
4643
4832
  for (const edgeId of graph.outboundEdges(svcId)) {
4644
4833
  const e = graph.getEdgeAttributes(edgeId);
4645
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4834
+ if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
4646
4835
  return true;
4647
4836
  }
4648
4837
  }
@@ -4655,10 +4844,10 @@ function detectHostMismatch(graph, svcId, svc) {
4655
4844
  const out = [];
4656
4845
  for (const edgeId of graph.outboundEdges(svcId)) {
4657
4846
  const edge = graph.getEdgeAttributes(edgeId);
4658
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4659
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4847
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4848
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4660
4849
  const target = graph.getNodeAttributes(edge.target);
4661
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4850
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4662
4851
  const observedHost = target.host?.trim();
4663
4852
  if (!observedHost) continue;
4664
4853
  if (observedHost === declaredHost) continue;
@@ -4680,10 +4869,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4680
4869
  const deps = svc.dependencies ?? {};
4681
4870
  for (const edgeId of graph.outboundEdges(svcId)) {
4682
4871
  const edge = graph.getEdgeAttributes(edgeId);
4683
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4684
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4872
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4873
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4685
4874
  const target = graph.getNodeAttributes(edge.target);
4686
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4875
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4687
4876
  for (const pair of compatPairs()) {
4688
4877
  if (pair.engine !== target.engine) continue;
4689
4878
  const declared = deps[pair.driver];
@@ -4744,7 +4933,7 @@ function computeDivergences(graph, opts = {}) {
4744
4933
  }
4745
4934
  graph.forEachNode((nodeId, attrs) => {
4746
4935
  const n = attrs;
4747
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4936
+ if (n.type !== import_types21.NodeType.ServiceNode) return;
4748
4937
  const svc = n;
4749
4938
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4750
4939
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4777,7 +4966,7 @@ function computeDivergences(graph, opts = {}) {
4777
4966
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4778
4967
  return a.target.localeCompare(b.target);
4779
4968
  });
4780
- return import_types20.DivergenceResultSchema.parse({
4969
+ return import_types21.DivergenceResultSchema.parse({
4781
4970
  divergences: filtered,
4782
4971
  totalAffected: filtered.length,
4783
4972
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4918,7 +5107,7 @@ init_cjs_shims();
4918
5107
  var import_node_fs20 = require("fs");
4919
5108
  var import_node_os2 = __toESM(require("os"), 1);
4920
5109
  var import_node_path33 = __toESM(require("path"), 1);
4921
- var import_types21 = require("@neat.is/types");
5110
+ var import_types22 = require("@neat.is/types");
4922
5111
  var LOCK_TIMEOUT_MS = 5e3;
4923
5112
  var LOCK_RETRY_MS = 50;
4924
5113
  function neatHome() {
@@ -4997,10 +5186,10 @@ async function readRegistry() {
4997
5186
  throw err;
4998
5187
  }
4999
5188
  const parsed = JSON.parse(raw);
5000
- return import_types21.RegistryFileSchema.parse(parsed);
5189
+ return import_types22.RegistryFileSchema.parse(parsed);
5001
5190
  }
5002
5191
  async function writeRegistry(reg) {
5003
- const validated = import_types21.RegistryFileSchema.parse(reg);
5192
+ const validated = import_types22.RegistryFileSchema.parse(reg);
5004
5193
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5005
5194
  }
5006
5195
  var ProjectNameCollisionError = class extends Error {
@@ -5272,11 +5461,11 @@ function registerRoutes(scope, ctx) {
5272
5461
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5273
5462
  const parsed = [];
5274
5463
  for (const c of candidates) {
5275
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5464
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5276
5465
  if (!r.success) {
5277
5466
  return reply.code(400).send({
5278
5467
  error: `unknown divergence type "${c}"`,
5279
- allowed: import_types22.DivergenceTypeSchema.options
5468
+ allowed: import_types23.DivergenceTypeSchema.options
5280
5469
  });
5281
5470
  }
5282
5471
  parsed.push(r.data);
@@ -5489,7 +5678,7 @@ function registerRoutes(scope, ctx) {
5489
5678
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5490
5679
  let violations = await log.readAll();
5491
5680
  if (req.query.severity) {
5492
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5681
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5493
5682
  if (!sev.success) {
5494
5683
  return reply.code(400).send({
5495
5684
  error: "invalid severity",
@@ -5506,7 +5695,7 @@ function registerRoutes(scope, ctx) {
5506
5695
  scope.post("/policies/check", async (req, reply) => {
5507
5696
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5508
5697
  if (!proj) return;
5509
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5698
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5510
5699
  if (!parsed.success) {
5511
5700
  return reply.code(400).send({
5512
5701
  error: "invalid /policies/check body",
@@ -5543,14 +5732,18 @@ function registerRoutes(scope, ctx) {
5543
5732
  async function buildApi(opts) {
5544
5733
  const app = (0, import_fastify.default)({ logger: false });
5545
5734
  await app.register(import_cors.default, { origin: true });
5735
+ const env = readAuthEnv();
5736
+ const authToken = opts.authToken ?? env.authToken;
5737
+ const trustProxy = opts.trustProxy ?? env.trustProxy;
5738
+ const publicRead = opts.publicRead ?? env.publicRead;
5546
5739
  mountBearerAuth(app, {
5547
- token: opts.authToken,
5548
- trustProxy: opts.trustProxy,
5549
- publicRead: opts.publicRead
5740
+ token: authToken,
5741
+ trustProxy,
5742
+ publicRead
5550
5743
  });
5551
5744
  app.get("/api/config", async () => ({
5552
- publicRead: opts.publicRead === true,
5553
- authProxy: opts.trustProxy === true
5745
+ publicRead: publicRead === true,
5746
+ authProxy: trustProxy === true
5554
5747
  }));
5555
5748
  const startedAt = opts.startedAt ?? Date.now();
5556
5749
  const registry = buildLegacyRegistry(opts);