@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/neatd.cjs CHANGED
@@ -973,6 +973,24 @@ function isFrontierNode(graph, nodeId) {
973
973
  const attrs = graph.getNodeAttributes(nodeId);
974
974
  return attrs.type === import_types.NodeType.FrontierNode;
975
975
  }
976
+ function resolveOwningService(graph, nodeId) {
977
+ if (!graph.hasNode(nodeId)) return null;
978
+ const attrs = graph.getNodeAttributes(nodeId);
979
+ if (attrs.type === import_types.NodeType.ServiceNode) {
980
+ return { id: nodeId, svc: attrs };
981
+ }
982
+ if (attrs.type === import_types.NodeType.FileNode) {
983
+ for (const edgeId of graph.inboundEdges(nodeId)) {
984
+ const e = graph.getEdgeAttributes(edgeId);
985
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
986
+ const owner = graph.getNodeAttributes(e.source);
987
+ if (owner.type === import_types.NodeType.ServiceNode) {
988
+ return { id: e.source, svc: owner };
989
+ }
990
+ }
991
+ }
992
+ return null;
993
+ }
976
994
  function bestEdgeBySource(graph, edgeIds) {
977
995
  const best = /* @__PURE__ */ new Map();
978
996
  for (const id of edgeIds) {
@@ -1079,9 +1097,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1079
1097
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1080
1098
  if (candidatePairs.length === 0) return null;
1081
1099
  for (const id of walk.path) {
1082
- const attrs = graph.getNodeAttributes(id);
1083
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1084
- const svc = attrs;
1100
+ const owner = resolveOwningService(graph, id);
1101
+ if (!owner) continue;
1102
+ const { id: serviceId3, svc } = owner;
1085
1103
  const deps = svc.dependencies ?? {};
1086
1104
  for (const pair of candidatePairs) {
1087
1105
  const declared = deps[pair.driver];
@@ -1094,7 +1112,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1094
1112
  );
1095
1113
  if (!result.compatible) {
1096
1114
  return {
1097
- rootCauseNode: id,
1115
+ rootCauseNode: serviceId3,
1098
1116
  rootCauseReason: result.reason ?? "incompatible driver",
1099
1117
  ...result.minDriverVersion ? {
1100
1118
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1107,9 +1125,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1107
1125
  }
1108
1126
  function serviceRootCauseShape(graph, _origin, walk) {
1109
1127
  for (const id of walk.path) {
1110
- const attrs = graph.getNodeAttributes(id);
1111
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1112
- const svc = attrs;
1128
+ const owner = resolveOwningService(graph, id);
1129
+ if (!owner) continue;
1130
+ const { id: serviceId3, svc } = owner;
1113
1131
  const deps = svc.dependencies ?? {};
1114
1132
  const serviceNodeEngine = svc.nodeEngine;
1115
1133
  for (const constraint of nodeEngineConstraints()) {
@@ -1118,7 +1136,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1118
1136
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1119
1137
  if (!result.compatible && result.reason) {
1120
1138
  return {
1121
- rootCauseNode: id,
1139
+ rootCauseNode: serviceId3,
1122
1140
  rootCauseReason: result.reason,
1123
1141
  ...result.requiredNodeVersion ? {
1124
1142
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1133,7 +1151,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1133
1151
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1134
1152
  if (!result.compatible && result.reason) {
1135
1153
  return {
1136
- rootCauseNode: id,
1154
+ rootCauseNode: serviceId3,
1137
1155
  rootCauseReason: result.reason,
1138
1156
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1139
1157
  };
@@ -1142,9 +1160,15 @@ function serviceRootCauseShape(graph, _origin, walk) {
1142
1160
  }
1143
1161
  return null;
1144
1162
  }
1163
+ function fileRootCauseShape(graph, origin, walk) {
1164
+ const owner = resolveOwningService(graph, origin.id);
1165
+ if (!owner) return null;
1166
+ return serviceRootCauseShape(graph, owner.svc, walk);
1167
+ }
1145
1168
  var rootCauseShapes = {
1146
1169
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
1147
- [import_types.NodeType.ServiceNode]: serviceRootCauseShape
1170
+ [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1171
+ [import_types.NodeType.FileNode]: fileRootCauseShape
1148
1172
  };
1149
1173
  function getRootCause(graph, errorNodeId, errorEvent) {
1150
1174
  if (!graph.hasNode(errorNodeId)) return null;
@@ -1607,6 +1631,86 @@ function hostFromUrl(u) {
1607
1631
  function pickAddress(span) {
1608
1632
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1609
1633
  }
1634
+ var CODE_FILEPATH_ATTR = "code.filepath";
1635
+ var CODE_LINENO_ATTR = "code.lineno";
1636
+ var CODE_FUNCTION_ATTR = "code.function";
1637
+ function toPosix(p) {
1638
+ return p.split("\\").join("/");
1639
+ }
1640
+ function languageForExt(relPath) {
1641
+ const dot = relPath.lastIndexOf(".");
1642
+ if (dot === -1) return void 0;
1643
+ switch (relPath.slice(dot).toLowerCase()) {
1644
+ case ".py":
1645
+ return "python";
1646
+ case ".ts":
1647
+ case ".tsx":
1648
+ return "typescript";
1649
+ case ".js":
1650
+ case ".jsx":
1651
+ case ".mjs":
1652
+ case ".cjs":
1653
+ return "javascript";
1654
+ default:
1655
+ return void 0;
1656
+ }
1657
+ }
1658
+ function relPathForRuntimeFile(filepath, serviceNode) {
1659
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1660
+ const root = serviceNode?.repoPath;
1661
+ if (root && root !== "." && root.length > 0) {
1662
+ const rootPosix = toPosix(root);
1663
+ const anchor = `/${rootPosix}/`;
1664
+ const idx = p.lastIndexOf(anchor);
1665
+ if (idx !== -1) return p.slice(idx + anchor.length);
1666
+ const base = rootPosix.split("/").filter(Boolean).pop();
1667
+ if (base) {
1668
+ const baseAnchor = `/${base}/`;
1669
+ const bidx = p.lastIndexOf(baseAnchor);
1670
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1671
+ }
1672
+ }
1673
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1674
+ return p.length > 0 ? p : null;
1675
+ }
1676
+ function callSiteFromSpan(span, serviceNode) {
1677
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1678
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1679
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1680
+ if (!relPath) return null;
1681
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1682
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1683
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1684
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1685
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1686
+ }
1687
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1688
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1689
+ if (!graph.hasNode(fileNodeId)) {
1690
+ const language = languageForExt(callSite.relPath);
1691
+ const node = {
1692
+ id: fileNodeId,
1693
+ type: import_types3.NodeType.FileNode,
1694
+ service: serviceName,
1695
+ path: callSite.relPath,
1696
+ ...language ? { language } : {},
1697
+ discoveredVia: "otel"
1698
+ };
1699
+ graph.addNode(fileNodeId, node);
1700
+ }
1701
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1702
+ if (!graph.hasEdge(containsId)) {
1703
+ const edge = {
1704
+ id: containsId,
1705
+ source: serviceNodeId,
1706
+ target: fileNodeId,
1707
+ type: import_types3.EdgeType.CONTAINS,
1708
+ provenance: import_types3.Provenance.OBSERVED
1709
+ };
1710
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1711
+ }
1712
+ return fileNodeId;
1713
+ }
1610
1714
  function makeObservedEdgeId(type, source, target) {
1611
1715
  return (0, import_types3.observedEdgeId)(source, target, type);
1612
1716
  }
@@ -1849,6 +1953,9 @@ async function handleSpan(ctx, span) {
1849
1953
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1850
1954
  const isError = span.statusCode === 2;
1851
1955
  cacheSpanService(span, nowMs);
1956
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1957
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
1958
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1852
1959
  let affectedNode = sourceId;
1853
1960
  if (span.dbSystem) {
1854
1961
  const host = pickAddress(span);
@@ -1858,7 +1965,7 @@ async function handleSpan(ctx, span) {
1858
1965
  const result = upsertObservedEdge(
1859
1966
  ctx.graph,
1860
1967
  import_types3.EdgeType.CONNECTS_TO,
1861
- sourceId,
1968
+ observedSource(),
1862
1969
  targetId,
1863
1970
  ts,
1864
1971
  isError
@@ -1874,7 +1981,7 @@ async function handleSpan(ctx, span) {
1874
1981
  upsertObservedEdge(
1875
1982
  ctx.graph,
1876
1983
  import_types3.EdgeType.CALLS,
1877
- sourceId,
1984
+ observedSource(),
1878
1985
  targetId,
1879
1986
  ts,
1880
1987
  isError
@@ -1886,7 +1993,7 @@ async function handleSpan(ctx, span) {
1886
1993
  upsertObservedEdge(
1887
1994
  ctx.graph,
1888
1995
  import_types3.EdgeType.CALLS,
1889
- sourceId,
1996
+ observedSource(),
1890
1997
  frontierNodeId,
1891
1998
  ts,
1892
1999
  isError
@@ -3444,7 +3551,7 @@ async function addConfigNodes(graph, services, scanPath) {
3444
3551
 
3445
3552
  // src/extract/calls/index.ts
3446
3553
  init_cjs_shims();
3447
- var import_types14 = require("@neat.is/types");
3554
+ var import_types15 = require("@neat.is/types");
3448
3555
 
3449
3556
  // src/extract/calls/http.ts
3450
3557
  init_cjs_shims();
@@ -3452,12 +3559,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3452
3559
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3453
3560
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3454
3561
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3455
- var import_types9 = require("@neat.is/types");
3562
+ var import_types10 = require("@neat.is/types");
3456
3563
 
3457
3564
  // src/extract/calls/shared.ts
3458
3565
  init_cjs_shims();
3459
3566
  var import_node_fs13 = require("fs");
3460
3567
  var import_node_path19 = __toESM(require("path"), 1);
3568
+ var import_types9 = require("@neat.is/types");
3461
3569
  async function walkSourceFiles(dir) {
3462
3570
  const out = [];
3463
3571
  async function walk(current) {
@@ -3497,6 +3605,58 @@ function snippet(text, line) {
3497
3605
  const lines = text.split("\n");
3498
3606
  return (lines[line - 1] ?? "").trim();
3499
3607
  }
3608
+ function toPosix2(p) {
3609
+ return p.split("\\").join("/");
3610
+ }
3611
+ function languageForPath(relPath) {
3612
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3613
+ case ".py":
3614
+ return "python";
3615
+ case ".ts":
3616
+ case ".tsx":
3617
+ return "typescript";
3618
+ case ".js":
3619
+ case ".jsx":
3620
+ case ".mjs":
3621
+ case ".cjs":
3622
+ return "javascript";
3623
+ default:
3624
+ return void 0;
3625
+ }
3626
+ }
3627
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3628
+ let nodesAdded = 0;
3629
+ let edgesAdded = 0;
3630
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3631
+ if (!graph.hasNode(fileNodeId)) {
3632
+ const language = languageForPath(relPath);
3633
+ const node = {
3634
+ id: fileNodeId,
3635
+ type: import_types9.NodeType.FileNode,
3636
+ service: serviceName,
3637
+ path: relPath,
3638
+ ...language ? { language } : {},
3639
+ discoveredVia: "static"
3640
+ };
3641
+ graph.addNode(fileNodeId, node);
3642
+ nodesAdded++;
3643
+ }
3644
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3645
+ if (!graph.hasEdge(containsId)) {
3646
+ const edge = {
3647
+ id: containsId,
3648
+ source: serviceNodeId,
3649
+ target: fileNodeId,
3650
+ type: import_types9.EdgeType.CONTAINS,
3651
+ provenance: import_types9.Provenance.EXTRACTED,
3652
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3653
+ evidence: { file: relPath }
3654
+ };
3655
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3656
+ edgesAdded++;
3657
+ }
3658
+ return { fileNodeId, nodesAdded, edgesAdded };
3659
+ }
3500
3660
 
3501
3661
  // src/extract/calls/http.ts
3502
3662
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3530,16 +3690,16 @@ function callsFromSource(source, parser, knownHosts) {
3530
3690
  const tree = parser.parse(source);
3531
3691
  const literals = [];
3532
3692
  collectStringLiterals(tree.rootNode, literals);
3533
- const targets = /* @__PURE__ */ new Set();
3693
+ const out = [];
3534
3694
  for (const lit of literals) {
3535
3695
  if (isInsideJsxExternalLink(lit.node)) continue;
3536
3696
  for (const host of knownHosts) {
3537
3697
  if (urlMatchesHost(lit.text, host)) {
3538
- targets.add(host);
3698
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3539
3699
  }
3540
3700
  }
3541
3701
  }
3542
- return targets;
3702
+ return out;
3543
3703
  }
3544
3704
  function makeJsParser() {
3545
3705
  const p = new import_tree_sitter.default();
@@ -3562,71 +3722,78 @@ async function addHttpCallEdges(graph, services) {
3562
3722
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3563
3723
  hostToNodeId.set(service.pkg.name, service.node.id);
3564
3724
  }
3725
+ let nodesAdded = 0;
3565
3726
  let edgesAdded = 0;
3566
3727
  for (const service of services) {
3567
3728
  const files = await loadSourceFiles(service.dir);
3568
- const seenTargets = /* @__PURE__ */ new Map();
3729
+ const seen = /* @__PURE__ */ new Set();
3569
3730
  for (const file of files) {
3570
3731
  if (isTestPath(file.path)) continue;
3571
3732
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3572
- let targets;
3733
+ let sites;
3573
3734
  try {
3574
- targets = callsFromSource(file.content, parser, knownHosts);
3735
+ sites = callsFromSource(file.content, parser, knownHosts);
3575
3736
  } catch (err) {
3576
3737
  recordExtractionError("http call extraction", file.path, err);
3577
3738
  continue;
3578
3739
  }
3579
- for (const t of targets) {
3580
- const targetId = hostToNodeId.get(t);
3740
+ if (sites.length === 0) continue;
3741
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3742
+ for (const site of sites) {
3743
+ const targetId = hostToNodeId.get(site.host);
3581
3744
  if (!targetId || targetId === service.node.id) continue;
3582
- if (!seenTargets.has(targetId)) {
3583
- seenTargets.set(targetId, { file: file.path, host: t });
3745
+ const dedupKey = `${relFile}|${targetId}`;
3746
+ if (seen.has(dedupKey)) continue;
3747
+ seen.add(dedupKey);
3748
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3749
+ const ev = {
3750
+ file: relFile,
3751
+ line: site.line,
3752
+ snippet: snippet(file.content, site.line)
3753
+ };
3754
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3755
+ graph,
3756
+ service.pkg.name,
3757
+ service.node.id,
3758
+ relFile
3759
+ );
3760
+ nodesAdded += n;
3761
+ edgesAdded += e;
3762
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3763
+ noteExtractedDropped({
3764
+ source: fileNodeId,
3765
+ target: targetId,
3766
+ type: import_types10.EdgeType.CALLS,
3767
+ confidence,
3768
+ confidenceKind: "hostname-shape-match",
3769
+ evidence: ev
3770
+ });
3771
+ continue;
3772
+ }
3773
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3774
+ if (!graph.hasEdge(edgeId)) {
3775
+ const edge = {
3776
+ id: edgeId,
3777
+ source: fileNodeId,
3778
+ target: targetId,
3779
+ type: import_types10.EdgeType.CALLS,
3780
+ provenance: import_types10.Provenance.EXTRACTED,
3781
+ confidence,
3782
+ evidence: ev
3783
+ };
3784
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3785
+ edgesAdded++;
3584
3786
  }
3585
- }
3586
- }
3587
- for (const [targetId, evidenceFile] of seenTargets) {
3588
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3589
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3590
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3591
- const ev = {
3592
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3593
- line,
3594
- snippet: snippet(fileContent, line)
3595
- };
3596
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3597
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3598
- noteExtractedDropped({
3599
- source: service.node.id,
3600
- target: targetId,
3601
- type: import_types9.EdgeType.CALLS,
3602
- confidence,
3603
- confidenceKind: "hostname-shape-match",
3604
- evidence: ev
3605
- });
3606
- continue;
3607
- }
3608
- const edge = {
3609
- id: edgeId,
3610
- source: service.node.id,
3611
- target: targetId,
3612
- type: import_types9.EdgeType.CALLS,
3613
- provenance: import_types9.Provenance.EXTRACTED,
3614
- confidence,
3615
- evidence: ev
3616
- };
3617
- if (!graph.hasEdge(edge.id)) {
3618
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3619
- edgesAdded++;
3620
3787
  }
3621
3788
  }
3622
3789
  }
3623
- return edgesAdded;
3790
+ return { nodesAdded, edgesAdded };
3624
3791
  }
3625
3792
 
3626
3793
  // src/extract/calls/kafka.ts
3627
3794
  init_cjs_shims();
3628
3795
  var import_node_path21 = __toESM(require("path"), 1);
3629
- var import_types10 = require("@neat.is/types");
3796
+ var import_types11 = require("@neat.is/types");
3630
3797
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3631
3798
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3632
3799
  function findAll(re, text) {
@@ -3647,7 +3814,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3647
3814
  seen.add(key);
3648
3815
  const line = lineOf(file.content, topic);
3649
3816
  out.push({
3650
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3817
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3651
3818
  name: topic,
3652
3819
  kind: "kafka-topic",
3653
3820
  edgeType,
@@ -3670,7 +3837,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3670
3837
  // src/extract/calls/redis.ts
3671
3838
  init_cjs_shims();
3672
3839
  var import_node_path22 = __toESM(require("path"), 1);
3673
- var import_types11 = require("@neat.is/types");
3840
+ var import_types12 = require("@neat.is/types");
3674
3841
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3675
3842
  function redisEndpointsFromFile(file, serviceDir) {
3676
3843
  const out = [];
@@ -3683,7 +3850,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3683
3850
  seen.add(host);
3684
3851
  const line = lineOf(file.content, host);
3685
3852
  out.push({
3686
- infraId: (0, import_types11.infraId)("redis", host),
3853
+ infraId: (0, import_types12.infraId)("redis", host),
3687
3854
  name: host,
3688
3855
  kind: "redis",
3689
3856
  edgeType: "CALLS",
@@ -3704,7 +3871,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3704
3871
  // src/extract/calls/aws.ts
3705
3872
  init_cjs_shims();
3706
3873
  var import_node_path23 = __toESM(require("path"), 1);
3707
- var import_types12 = require("@neat.is/types");
3874
+ var import_types13 = require("@neat.is/types");
3708
3875
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3709
3876
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3710
3877
  function hasMarker(text, markers) {
@@ -3728,7 +3895,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3728
3895
  seen.add(key);
3729
3896
  const line = lineOf(file.content, name);
3730
3897
  out.push({
3731
- infraId: (0, import_types12.infraId)(kind, name),
3898
+ infraId: (0, import_types13.infraId)(kind, name),
3732
3899
  name,
3733
3900
  kind,
3734
3901
  edgeType: "CALLS",
@@ -3763,7 +3930,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3763
3930
  // src/extract/calls/grpc.ts
3764
3931
  init_cjs_shims();
3765
3932
  var import_node_path24 = __toESM(require("path"), 1);
3766
- var import_types13 = require("@neat.is/types");
3933
+ var import_types14 = require("@neat.is/types");
3767
3934
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3768
3935
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3769
3936
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3811,7 +3978,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3811
3978
  const { kind } = classified;
3812
3979
  const line = lineOf(file.content, m[0]);
3813
3980
  out.push({
3814
- infraId: (0, import_types13.infraId)(kind, name),
3981
+ infraId: (0, import_types14.infraId)(kind, name),
3815
3982
  name,
3816
3983
  kind,
3817
3984
  edgeType: "CALLS",
@@ -3833,11 +4000,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3833
4000
  function edgeTypeFromEndpoint(ep) {
3834
4001
  switch (ep.edgeType) {
3835
4002
  case "PUBLISHES_TO":
3836
- return import_types14.EdgeType.PUBLISHES_TO;
4003
+ return import_types15.EdgeType.PUBLISHES_TO;
3837
4004
  case "CONSUMES_FROM":
3838
- return import_types14.EdgeType.CONSUMES_FROM;
4005
+ return import_types15.EdgeType.CONSUMES_FROM;
3839
4006
  default:
3840
- return import_types14.EdgeType.CALLS;
4007
+ return import_types15.EdgeType.CALLS;
3841
4008
  }
3842
4009
  }
3843
4010
  function isAwsKind(kind) {
@@ -3864,7 +4031,7 @@ async function addExternalEndpointEdges(graph, services) {
3864
4031
  if (!graph.hasNode(ep.infraId)) {
3865
4032
  const node = {
3866
4033
  id: ep.infraId,
3867
- type: import_types14.NodeType.InfraNode,
4034
+ type: import_types15.NodeType.InfraNode,
3868
4035
  name: ep.name,
3869
4036
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3870
4037
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -3876,13 +4043,19 @@ async function addExternalEndpointEdges(graph, services) {
3876
4043
  nodesAdded++;
3877
4044
  }
3878
4045
  const edgeType = edgeTypeFromEndpoint(ep);
3879
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3880
- if (seenEdges.has(edgeId)) continue;
3881
- seenEdges.add(edgeId);
3882
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
3883
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4046
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4047
+ const relFile = toPosix2(ep.evidence.file);
4048
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4049
+ graph,
4050
+ service.pkg.name,
4051
+ service.node.id,
4052
+ relFile
4053
+ );
4054
+ nodesAdded += n;
4055
+ edgesAdded += e;
4056
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
3884
4057
  noteExtractedDropped({
3885
- source: service.node.id,
4058
+ source: fileNodeId,
3886
4059
  target: ep.infraId,
3887
4060
  type: edgeType,
3888
4061
  confidence,
@@ -3891,13 +4064,16 @@ async function addExternalEndpointEdges(graph, services) {
3891
4064
  });
3892
4065
  continue;
3893
4066
  }
4067
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4068
+ if (seenEdges.has(edgeId)) continue;
4069
+ seenEdges.add(edgeId);
3894
4070
  if (!graph.hasEdge(edgeId)) {
3895
4071
  const edge = {
3896
4072
  id: edgeId,
3897
- source: service.node.id,
4073
+ source: fileNodeId,
3898
4074
  target: ep.infraId,
3899
4075
  type: edgeType,
3900
- provenance: import_types14.Provenance.EXTRACTED,
4076
+ provenance: import_types15.Provenance.EXTRACTED,
3901
4077
  confidence,
3902
4078
  evidence: ep.evidence
3903
4079
  };
@@ -3909,11 +4085,11 @@ async function addExternalEndpointEdges(graph, services) {
3909
4085
  return { nodesAdded, edgesAdded };
3910
4086
  }
3911
4087
  async function addCallEdges(graph, services) {
3912
- const httpEdges = await addHttpCallEdges(graph, services);
4088
+ const http = await addHttpCallEdges(graph, services);
3913
4089
  const ext = await addExternalEndpointEdges(graph, services);
3914
4090
  return {
3915
- nodesAdded: ext.nodesAdded,
3916
- edgesAdded: httpEdges + ext.edgesAdded
4091
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
4092
+ edgesAdded: http.edgesAdded + ext.edgesAdded
3917
4093
  };
3918
4094
  }
3919
4095
 
@@ -3923,15 +4099,15 @@ init_cjs_shims();
3923
4099
  // src/extract/infra/docker-compose.ts
3924
4100
  init_cjs_shims();
3925
4101
  var import_node_path25 = __toESM(require("path"), 1);
3926
- var import_types16 = require("@neat.is/types");
4102
+ var import_types17 = require("@neat.is/types");
3927
4103
 
3928
4104
  // src/extract/infra/shared.ts
3929
4105
  init_cjs_shims();
3930
- var import_types15 = require("@neat.is/types");
4106
+ var import_types16 = require("@neat.is/types");
3931
4107
  function makeInfraNode(kind, name, provider = "self", extras) {
3932
4108
  return {
3933
- id: (0, import_types15.infraId)(kind, name),
3934
- type: import_types15.NodeType.InfraNode,
4109
+ id: (0, import_types16.infraId)(kind, name),
4110
+ type: import_types16.NodeType.InfraNode,
3935
4111
  name,
3936
4112
  provider,
3937
4113
  kind,
@@ -4010,15 +4186,15 @@ async function addComposeInfra(graph, scanPath, services) {
4010
4186
  for (const dep of dependsOnList(svc.depends_on)) {
4011
4187
  const targetId = composeNameToNodeId.get(dep);
4012
4188
  if (!targetId) continue;
4013
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4189
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4014
4190
  if (graph.hasEdge(edgeId)) continue;
4015
4191
  const edge = {
4016
4192
  id: edgeId,
4017
4193
  source: sourceId,
4018
4194
  target: targetId,
4019
- type: import_types16.EdgeType.DEPENDS_ON,
4020
- provenance: import_types16.Provenance.EXTRACTED,
4021
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4195
+ type: import_types17.EdgeType.DEPENDS_ON,
4196
+ provenance: import_types17.Provenance.EXTRACTED,
4197
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4022
4198
  evidence: { file: evidenceFile }
4023
4199
  };
4024
4200
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4032,7 +4208,7 @@ async function addComposeInfra(graph, scanPath, services) {
4032
4208
  init_cjs_shims();
4033
4209
  var import_node_path26 = __toESM(require("path"), 1);
4034
4210
  var import_node_fs14 = require("fs");
4035
- var import_types17 = require("@neat.is/types");
4211
+ var import_types18 = require("@neat.is/types");
4036
4212
  function runtimeImage(content) {
4037
4213
  const lines = content.split("\n");
4038
4214
  let last = null;
@@ -4071,15 +4247,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4071
4247
  graph.addNode(node.id, node);
4072
4248
  nodesAdded++;
4073
4249
  }
4074
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4250
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4075
4251
  if (!graph.hasEdge(edgeId)) {
4076
4252
  const edge = {
4077
4253
  id: edgeId,
4078
4254
  source: service.node.id,
4079
4255
  target: node.id,
4080
- type: import_types17.EdgeType.RUNS_ON,
4081
- provenance: import_types17.Provenance.EXTRACTED,
4082
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4256
+ type: import_types18.EdgeType.RUNS_ON,
4257
+ provenance: import_types18.Provenance.EXTRACTED,
4258
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4083
4259
  evidence: {
4084
4260
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4085
4261
  }
@@ -4207,13 +4383,24 @@ var import_node_path30 = __toESM(require("path"), 1);
4207
4383
  init_cjs_shims();
4208
4384
  var import_node_fs17 = require("fs");
4209
4385
  var import_node_path29 = __toESM(require("path"), 1);
4210
- var import_types18 = require("@neat.is/types");
4386
+ var import_types19 = require("@neat.is/types");
4387
+ function dropOrphanedFileNodes(graph) {
4388
+ const orphans = [];
4389
+ graph.forEachNode((id, attrs) => {
4390
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4391
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4392
+ orphans.push(id);
4393
+ }
4394
+ });
4395
+ for (const id of orphans) graph.dropNode(id);
4396
+ return orphans.length;
4397
+ }
4211
4398
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4212
4399
  const toDrop = [];
4213
4400
  const bases = [scanPath, ...serviceDirs];
4214
4401
  graph.forEachEdge((id, attrs) => {
4215
4402
  const edge = attrs;
4216
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4403
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4217
4404
  const evidenceFile = edge.evidence?.file;
4218
4405
  if (!evidenceFile) return;
4219
4406
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4224,6 +4411,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4224
4411
  if (!found) toDrop.push(id);
4225
4412
  });
4226
4413
  for (const id of toDrop) graph.dropEdge(id);
4414
+ dropOrphanedFileNodes(graph);
4227
4415
  return toDrop.length;
4228
4416
  }
4229
4417
 
@@ -4293,7 +4481,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4293
4481
  init_cjs_shims();
4294
4482
  var import_node_fs18 = require("fs");
4295
4483
  var import_node_path31 = __toESM(require("path"), 1);
4296
- var import_types19 = require("@neat.is/types");
4484
+ var import_types20 = require("@neat.is/types");
4297
4485
  var SCHEMA_VERSION = 4;
4298
4486
  function migrateV1ToV2(payload) {
4299
4487
  const nodes = payload.graph.nodes;
@@ -4315,12 +4503,12 @@ function migrateV2ToV3(payload) {
4315
4503
  for (const edge of edges) {
4316
4504
  const attrs = edge.attributes;
4317
4505
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4318
- attrs.provenance = import_types19.Provenance.OBSERVED;
4506
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4319
4507
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4320
4508
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4321
4509
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4322
4510
  if (type && source && target) {
4323
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4511
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4324
4512
  attrs.id = newId;
4325
4513
  if (edge.key) edge.key = newId;
4326
4514
  }
@@ -4458,11 +4646,11 @@ var Projects = class {
4458
4646
  init_cjs_shims();
4459
4647
  var import_fastify = __toESM(require("fastify"), 1);
4460
4648
  var import_cors = __toESM(require("@fastify/cors"), 1);
4461
- var import_types22 = require("@neat.is/types");
4649
+ var import_types23 = require("@neat.is/types");
4462
4650
 
4463
4651
  // src/divergences.ts
4464
4652
  init_cjs_shims();
4465
- var import_types20 = require("@neat.is/types");
4653
+ var import_types21 = require("@neat.is/types");
4466
4654
  function bucketKey(source, target, type) {
4467
4655
  return `${type}|${source}|${target}`;
4468
4656
  }
@@ -4470,22 +4658,22 @@ function bucketEdges(graph) {
4470
4658
  const buckets = /* @__PURE__ */ new Map();
4471
4659
  graph.forEachEdge((id, attrs) => {
4472
4660
  const e = attrs;
4473
- const parsed = (0, import_types20.parseEdgeId)(id);
4661
+ const parsed = (0, import_types21.parseEdgeId)(id);
4474
4662
  const provenance = parsed?.provenance ?? e.provenance;
4475
4663
  const key = bucketKey(e.source, e.target, e.type);
4476
4664
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4477
4665
  switch (provenance) {
4478
- case import_types20.Provenance.EXTRACTED:
4666
+ case import_types21.Provenance.EXTRACTED:
4479
4667
  cur.extracted = e;
4480
4668
  break;
4481
- case import_types20.Provenance.OBSERVED:
4669
+ case import_types21.Provenance.OBSERVED:
4482
4670
  cur.observed = e;
4483
4671
  break;
4484
- case import_types20.Provenance.INFERRED:
4672
+ case import_types21.Provenance.INFERRED:
4485
4673
  cur.inferred = e;
4486
4674
  break;
4487
4675
  default:
4488
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4676
+ if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
4489
4677
  }
4490
4678
  buckets.set(key, cur);
4491
4679
  });
@@ -4494,7 +4682,7 @@ function bucketEdges(graph) {
4494
4682
  function nodeIsFrontier(graph, nodeId) {
4495
4683
  if (!graph.hasNode(nodeId)) return false;
4496
4684
  const attrs = graph.getNodeAttributes(nodeId);
4497
- return attrs.type === import_types20.NodeType.FrontierNode;
4685
+ return attrs.type === import_types21.NodeType.FrontierNode;
4498
4686
  }
4499
4687
  function clampConfidence(n) {
4500
4688
  if (!Number.isFinite(n)) return 0;
@@ -4515,6 +4703,7 @@ function gradedConfidence(edge) {
4515
4703
  }
4516
4704
  function detectMissingDivergences(graph, bucket) {
4517
4705
  const out = [];
4706
+ if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
4518
4707
  if (bucket.extracted && !bucket.observed) {
4519
4708
  if (!nodeIsFrontier(graph, bucket.target)) {
4520
4709
  out.push({
@@ -4555,7 +4744,7 @@ function declaredHostFor(svc) {
4555
4744
  function hasExtractedConfiguredBy(graph, svcId) {
4556
4745
  for (const edgeId of graph.outboundEdges(svcId)) {
4557
4746
  const e = graph.getEdgeAttributes(edgeId);
4558
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4747
+ if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
4559
4748
  return true;
4560
4749
  }
4561
4750
  }
@@ -4568,10 +4757,10 @@ function detectHostMismatch(graph, svcId, svc) {
4568
4757
  const out = [];
4569
4758
  for (const edgeId of graph.outboundEdges(svcId)) {
4570
4759
  const edge = graph.getEdgeAttributes(edgeId);
4571
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4572
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4760
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4761
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4573
4762
  const target = graph.getNodeAttributes(edge.target);
4574
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4763
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4575
4764
  const observedHost = target.host?.trim();
4576
4765
  if (!observedHost) continue;
4577
4766
  if (observedHost === declaredHost) continue;
@@ -4593,10 +4782,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4593
4782
  const deps = svc.dependencies ?? {};
4594
4783
  for (const edgeId of graph.outboundEdges(svcId)) {
4595
4784
  const edge = graph.getEdgeAttributes(edgeId);
4596
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4597
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4785
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4786
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4598
4787
  const target = graph.getNodeAttributes(edge.target);
4599
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4788
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4600
4789
  for (const pair of compatPairs()) {
4601
4790
  if (pair.engine !== target.engine) continue;
4602
4791
  const declared = deps[pair.driver];
@@ -4657,7 +4846,7 @@ function computeDivergences(graph, opts = {}) {
4657
4846
  }
4658
4847
  graph.forEachNode((nodeId, attrs) => {
4659
4848
  const n = attrs;
4660
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4849
+ if (n.type !== import_types21.NodeType.ServiceNode) return;
4661
4850
  const svc = n;
4662
4851
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4663
4852
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4690,7 +4879,7 @@ function computeDivergences(graph, opts = {}) {
4690
4879
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4691
4880
  return a.target.localeCompare(b.target);
4692
4881
  });
4693
- return import_types20.DivergenceResultSchema.parse({
4882
+ return import_types21.DivergenceResultSchema.parse({
4694
4883
  divergences: filtered,
4695
4884
  totalAffected: filtered.length,
4696
4885
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4779,7 +4968,7 @@ init_cjs_shims();
4779
4968
  var import_node_fs20 = require("fs");
4780
4969
  var import_node_os2 = __toESM(require("os"), 1);
4781
4970
  var import_node_path33 = __toESM(require("path"), 1);
4782
- var import_types21 = require("@neat.is/types");
4971
+ var import_types22 = require("@neat.is/types");
4783
4972
  var LOCK_TIMEOUT_MS = 5e3;
4784
4973
  var LOCK_RETRY_MS = 50;
4785
4974
  function neatHome() {
@@ -4850,10 +5039,10 @@ async function readRegistry() {
4850
5039
  throw err;
4851
5040
  }
4852
5041
  const parsed = JSON.parse(raw);
4853
- return import_types21.RegistryFileSchema.parse(parsed);
5042
+ return import_types22.RegistryFileSchema.parse(parsed);
4854
5043
  }
4855
5044
  async function writeRegistry(reg) {
4856
- const validated = import_types21.RegistryFileSchema.parse(reg);
5045
+ const validated = import_types22.RegistryFileSchema.parse(reg);
4857
5046
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4858
5047
  }
4859
5048
  async function getProject(name) {
@@ -5075,11 +5264,11 @@ function registerRoutes(scope, ctx) {
5075
5264
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5076
5265
  const parsed = [];
5077
5266
  for (const c of candidates) {
5078
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5267
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5079
5268
  if (!r.success) {
5080
5269
  return reply.code(400).send({
5081
5270
  error: `unknown divergence type "${c}"`,
5082
- allowed: import_types22.DivergenceTypeSchema.options
5271
+ allowed: import_types23.DivergenceTypeSchema.options
5083
5272
  });
5084
5273
  }
5085
5274
  parsed.push(r.data);
@@ -5292,7 +5481,7 @@ function registerRoutes(scope, ctx) {
5292
5481
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5293
5482
  let violations = await log.readAll();
5294
5483
  if (req2.query.severity) {
5295
- const sev = import_types22.PolicySeveritySchema.safeParse(req2.query.severity);
5484
+ const sev = import_types23.PolicySeveritySchema.safeParse(req2.query.severity);
5296
5485
  if (!sev.success) {
5297
5486
  return reply.code(400).send({
5298
5487
  error: "invalid severity",
@@ -5309,7 +5498,7 @@ function registerRoutes(scope, ctx) {
5309
5498
  scope.post("/policies/check", async (req2, reply) => {
5310
5499
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
5311
5500
  if (!proj) return;
5312
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
5501
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
5313
5502
  if (!parsed.success) {
5314
5503
  return reply.code(400).send({
5315
5504
  error: "invalid /policies/check body",
@@ -5346,14 +5535,18 @@ function registerRoutes(scope, ctx) {
5346
5535
  async function buildApi(opts) {
5347
5536
  const app = (0, import_fastify.default)({ logger: false });
5348
5537
  await app.register(import_cors.default, { origin: true });
5538
+ const env = readAuthEnv();
5539
+ const authToken = opts.authToken ?? env.authToken;
5540
+ const trustProxy = opts.trustProxy ?? env.trustProxy;
5541
+ const publicRead = opts.publicRead ?? env.publicRead;
5349
5542
  mountBearerAuth(app, {
5350
- token: opts.authToken,
5351
- trustProxy: opts.trustProxy,
5352
- publicRead: opts.publicRead
5543
+ token: authToken,
5544
+ trustProxy,
5545
+ publicRead
5353
5546
  });
5354
5547
  app.get("/api/config", async () => ({
5355
- publicRead: opts.publicRead === true,
5356
- authProxy: opts.trustProxy === true
5548
+ publicRead: publicRead === true,
5549
+ authProxy: trustProxy === true
5357
5550
  }));
5358
5551
  const startedAt = opts.startedAt ?? Date.now();
5359
5552
  const registry = buildLegacyRegistry(opts);