@neat.is/core 0.4.28-dev.20260712 → 0.4.28-dev.20260713
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/{chunk-5T6J3WKC.js → chunk-N7HEYLOF.js} +731 -143
- package/dist/chunk-N7HEYLOF.js.map +1 -0
- package/dist/{chunk-UJ6WBLIE.js → chunk-V2HUBS5J.js} +163 -345
- package/dist/chunk-V2HUBS5J.js.map +1 -0
- package/dist/cli.cjs +1174 -813
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +9 -9
- package/dist/index.cjs +784 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +849 -445
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +507 -91
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-5T6J3WKC.js.map +0 -1
- package/dist/chunk-UJ6WBLIE.js.map +0 -1
|
@@ -456,19 +456,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
456
456
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
457
457
|
let best = { path: [start], edges: [] };
|
|
458
458
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
459
|
-
function step(node,
|
|
460
|
-
if (
|
|
461
|
-
best = { path: [...
|
|
459
|
+
function step(node, path47, edges) {
|
|
460
|
+
if (path47.length > best.path.length) {
|
|
461
|
+
best = { path: [...path47], edges: [...edges] };
|
|
462
462
|
}
|
|
463
|
-
if (
|
|
463
|
+
if (path47.length - 1 >= maxDepth) return;
|
|
464
464
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
465
465
|
for (const [srcId, edge] of incoming) {
|
|
466
466
|
if (visited.has(srcId)) continue;
|
|
467
467
|
visited.add(srcId);
|
|
468
|
-
|
|
468
|
+
path47.push(srcId);
|
|
469
469
|
edges.push(edge);
|
|
470
|
-
step(srcId,
|
|
471
|
-
|
|
470
|
+
step(srcId, path47, edges);
|
|
471
|
+
path47.pop();
|
|
472
472
|
edges.pop();
|
|
473
473
|
visited.delete(srcId);
|
|
474
474
|
}
|
|
@@ -662,26 +662,26 @@ function dominantFailingCall(graph, serviceId4, visited) {
|
|
|
662
662
|
return best;
|
|
663
663
|
}
|
|
664
664
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
665
|
-
const
|
|
665
|
+
const path47 = [originServiceId];
|
|
666
666
|
const edges = [];
|
|
667
667
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
668
668
|
let current = originServiceId;
|
|
669
669
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
670
670
|
const hop = dominantFailingCall(graph, current, visited);
|
|
671
671
|
if (!hop) break;
|
|
672
|
-
|
|
672
|
+
path47.push(hop.nextService);
|
|
673
673
|
edges.push(hop.edge);
|
|
674
674
|
visited.add(hop.nextService);
|
|
675
675
|
current = hop.nextService;
|
|
676
676
|
}
|
|
677
677
|
if (edges.length === 0) return null;
|
|
678
|
-
return { path:
|
|
678
|
+
return { path: path47, edges, culprit: current };
|
|
679
679
|
}
|
|
680
680
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
681
681
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
682
682
|
if (!chain) return null;
|
|
683
683
|
const culprit = chain.culprit;
|
|
684
|
-
const
|
|
684
|
+
const path47 = [...chain.path];
|
|
685
685
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
686
686
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
687
687
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -689,14 +689,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
689
689
|
if (loc) {
|
|
690
690
|
let rootCauseNode = culprit;
|
|
691
691
|
if (loc.fileNode) {
|
|
692
|
-
|
|
692
|
+
path47.push(loc.fileNode);
|
|
693
693
|
edgeProvenances.push(Provenance.OBSERVED);
|
|
694
694
|
rootCauseNode = loc.fileNode;
|
|
695
695
|
}
|
|
696
696
|
return RootCauseResultSchema.parse({
|
|
697
697
|
rootCauseNode,
|
|
698
698
|
rootCauseReason: loc.rootCauseReason,
|
|
699
|
-
traversalPath:
|
|
699
|
+
traversalPath: path47,
|
|
700
700
|
edgeProvenances,
|
|
701
701
|
confidence,
|
|
702
702
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -708,7 +708,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
708
708
|
return RootCauseResultSchema.parse({
|
|
709
709
|
rootCauseNode: culprit,
|
|
710
710
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
711
|
-
traversalPath:
|
|
711
|
+
traversalPath: path47,
|
|
712
712
|
edgeProvenances,
|
|
713
713
|
confidence,
|
|
714
714
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -5803,10 +5803,10 @@ async function addCallEdges(graph, services) {
|
|
|
5803
5803
|
|
|
5804
5804
|
// src/extract/infra/docker-compose.ts
|
|
5805
5805
|
import path31 from "path";
|
|
5806
|
-
import { EdgeType as EdgeType13, Provenance as
|
|
5806
|
+
import { EdgeType as EdgeType13, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
|
|
5807
5807
|
|
|
5808
5808
|
// src/extract/infra/shared.ts
|
|
5809
|
-
import { NodeType as NodeType13, infraId as infraId7 } from "@neat.is/types";
|
|
5809
|
+
import { NodeType as NodeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10, infraId as infraId7 } from "@neat.is/types";
|
|
5810
5810
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
5811
5811
|
return {
|
|
5812
5812
|
id: infraId7(kind, name),
|
|
@@ -5830,6 +5830,39 @@ function classifyImage(image) {
|
|
|
5830
5830
|
if (last.startsWith("memcached")) return "memcached";
|
|
5831
5831
|
return "container";
|
|
5832
5832
|
}
|
|
5833
|
+
function lineContaining(raw, needle) {
|
|
5834
|
+
if (!needle) return void 0;
|
|
5835
|
+
const idx = raw.indexOf(needle);
|
|
5836
|
+
if (idx === -1) return void 0;
|
|
5837
|
+
let line = 1;
|
|
5838
|
+
for (let i = 0; i < idx; i++) if (raw[i] === "\n") line++;
|
|
5839
|
+
return line;
|
|
5840
|
+
}
|
|
5841
|
+
function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provider, evidenceFile, line) {
|
|
5842
|
+
let nodesAdded = 0;
|
|
5843
|
+
let edgesAdded = 0;
|
|
5844
|
+
const node = makeInfraNode(kind, name, provider);
|
|
5845
|
+
if (!graph.hasNode(node.id)) {
|
|
5846
|
+
graph.addNode(node.id, node);
|
|
5847
|
+
nodesAdded++;
|
|
5848
|
+
}
|
|
5849
|
+
if (node.id === anchorId) return { nodesAdded, edgesAdded };
|
|
5850
|
+
const edgeId = extractedEdgeId2(anchorId, node.id, edgeType);
|
|
5851
|
+
if (!graph.hasEdge(edgeId)) {
|
|
5852
|
+
const edge = {
|
|
5853
|
+
id: edgeId,
|
|
5854
|
+
source: anchorId,
|
|
5855
|
+
target: node.id,
|
|
5856
|
+
type: edgeType,
|
|
5857
|
+
provenance: Provenance12.EXTRACTED,
|
|
5858
|
+
confidence: confidenceForExtracted10("structural"),
|
|
5859
|
+
evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
|
|
5860
|
+
};
|
|
5861
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5862
|
+
edgesAdded++;
|
|
5863
|
+
}
|
|
5864
|
+
return { nodesAdded, edgesAdded };
|
|
5865
|
+
}
|
|
5833
5866
|
|
|
5834
5867
|
// src/extract/infra/docker-compose.ts
|
|
5835
5868
|
function dependsOnList(value) {
|
|
@@ -5896,8 +5929,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5896
5929
|
source: sourceId,
|
|
5897
5930
|
target: targetId,
|
|
5898
5931
|
type: EdgeType13.DEPENDS_ON,
|
|
5899
|
-
provenance:
|
|
5900
|
-
confidence:
|
|
5932
|
+
provenance: Provenance13.EXTRACTED,
|
|
5933
|
+
confidence: confidenceForExtracted11("structural"),
|
|
5901
5934
|
evidence: { file: evidenceFile }
|
|
5902
5935
|
};
|
|
5903
5936
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -5910,7 +5943,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5910
5943
|
// src/extract/infra/dockerfile.ts
|
|
5911
5944
|
import path32 from "path";
|
|
5912
5945
|
import { promises as fs16 } from "fs";
|
|
5913
|
-
import { EdgeType as EdgeType14, Provenance as
|
|
5946
|
+
import { EdgeType as EdgeType14, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
|
|
5914
5947
|
function readDockerfile(content) {
|
|
5915
5948
|
let image = null;
|
|
5916
5949
|
const ports = [];
|
|
@@ -5976,8 +6009,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5976
6009
|
source: fileNodeId,
|
|
5977
6010
|
target: node.id,
|
|
5978
6011
|
type: EdgeType14.RUNS_ON,
|
|
5979
|
-
provenance:
|
|
5980
|
-
confidence:
|
|
6012
|
+
provenance: Provenance14.EXTRACTED,
|
|
6013
|
+
confidence: confidenceForExtracted12("structural"),
|
|
5981
6014
|
evidence: {
|
|
5982
6015
|
file: evidenceFile,
|
|
5983
6016
|
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
@@ -5999,8 +6032,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5999
6032
|
source: fileNodeId,
|
|
6000
6033
|
target: portNode.id,
|
|
6001
6034
|
type: EdgeType14.CONNECTS_TO,
|
|
6002
|
-
provenance:
|
|
6003
|
-
confidence:
|
|
6035
|
+
provenance: Provenance14.EXTRACTED,
|
|
6036
|
+
confidence: confidenceForExtracted12("structural"),
|
|
6004
6037
|
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
6005
6038
|
};
|
|
6006
6039
|
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
@@ -6013,7 +6046,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6013
6046
|
// src/extract/infra/terraform.ts
|
|
6014
6047
|
import { promises as fs17 } from "fs";
|
|
6015
6048
|
import path33 from "path";
|
|
6016
|
-
import { EdgeType as EdgeType15, Provenance as
|
|
6049
|
+
import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
|
|
6017
6050
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
6018
6051
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
6019
6052
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
@@ -6102,8 +6135,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
6102
6135
|
source: resource.nodeId,
|
|
6103
6136
|
target: target.nodeId,
|
|
6104
6137
|
type: EdgeType15.DEPENDS_ON,
|
|
6105
|
-
provenance:
|
|
6106
|
-
confidence:
|
|
6138
|
+
provenance: Provenance15.EXTRACTED,
|
|
6139
|
+
confidence: confidenceForExtracted13("structural"),
|
|
6107
6140
|
evidence: { file: evidenceFile, line, snippet: key }
|
|
6108
6141
|
};
|
|
6109
6142
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -6173,7 +6206,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
6173
6206
|
import { promises as fs19 } from "fs";
|
|
6174
6207
|
import path35 from "path";
|
|
6175
6208
|
import { parse as parseToml2 } from "smol-toml";
|
|
6176
|
-
import { EdgeType as EdgeType16, Provenance as
|
|
6209
|
+
import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
|
|
6177
6210
|
var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
|
|
6178
6211
|
async function readWranglerConfig(dir) {
|
|
6179
6212
|
for (const filename of WRANGLER_FILENAMES) {
|
|
@@ -6185,7 +6218,7 @@ async function readWranglerConfig(dir) {
|
|
|
6185
6218
|
}
|
|
6186
6219
|
return null;
|
|
6187
6220
|
}
|
|
6188
|
-
function
|
|
6221
|
+
function lineContaining2(raw, needle) {
|
|
6189
6222
|
if (!needle) return void 0;
|
|
6190
6223
|
const idx = raw.indexOf(needle);
|
|
6191
6224
|
if (idx === -1) return void 0;
|
|
@@ -6221,8 +6254,8 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
|
|
|
6221
6254
|
source: anchorId,
|
|
6222
6255
|
target: node.id,
|
|
6223
6256
|
type: edgeType,
|
|
6224
|
-
provenance:
|
|
6225
|
-
confidence:
|
|
6257
|
+
provenance: Provenance16.EXTRACTED,
|
|
6258
|
+
confidence: confidenceForExtracted14("structural"),
|
|
6226
6259
|
evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
|
|
6227
6260
|
};
|
|
6228
6261
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -6290,8 +6323,8 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6290
6323
|
source: anchorId,
|
|
6291
6324
|
target: runtimeNode.id,
|
|
6292
6325
|
type: EdgeType16.RUNS_ON,
|
|
6293
|
-
provenance:
|
|
6294
|
-
confidence:
|
|
6326
|
+
provenance: Provenance16.EXTRACTED,
|
|
6327
|
+
confidence: confidenceForExtracted14("structural"),
|
|
6295
6328
|
evidence: {
|
|
6296
6329
|
file: evidenceFile,
|
|
6297
6330
|
...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
|
|
@@ -6309,7 +6342,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6309
6342
|
"cloudflare-route",
|
|
6310
6343
|
route,
|
|
6311
6344
|
evidenceFile,
|
|
6312
|
-
|
|
6345
|
+
lineContaining2(raw, route)
|
|
6313
6346
|
);
|
|
6314
6347
|
nodesAdded += result.nodesAdded;
|
|
6315
6348
|
edgesAdded += result.edgesAdded;
|
|
@@ -6333,7 +6366,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6333
6366
|
group.kind,
|
|
6334
6367
|
name,
|
|
6335
6368
|
evidenceFile,
|
|
6336
|
-
|
|
6369
|
+
lineContaining2(raw, name)
|
|
6337
6370
|
);
|
|
6338
6371
|
nodesAdded += result.nodesAdded;
|
|
6339
6372
|
edgesAdded += result.edgesAdded;
|
|
@@ -6347,7 +6380,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6347
6380
|
"cloudflare-cron",
|
|
6348
6381
|
cron,
|
|
6349
6382
|
evidenceFile,
|
|
6350
|
-
|
|
6383
|
+
lineContaining2(raw, cron)
|
|
6351
6384
|
);
|
|
6352
6385
|
nodesAdded += result.nodesAdded;
|
|
6353
6386
|
edgesAdded += result.edgesAdded;
|
|
@@ -6360,7 +6393,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6360
6393
|
"cloudflare-env-var",
|
|
6361
6394
|
varName,
|
|
6362
6395
|
evidenceFile,
|
|
6363
|
-
|
|
6396
|
+
lineContaining2(raw, varName)
|
|
6364
6397
|
);
|
|
6365
6398
|
nodesAdded += result.nodesAdded;
|
|
6366
6399
|
edgesAdded += result.edgesAdded;
|
|
@@ -6376,9 +6409,9 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6376
6409
|
source: anchorId,
|
|
6377
6410
|
target: target.anchorId,
|
|
6378
6411
|
type: EdgeType16.CALLS,
|
|
6379
|
-
provenance:
|
|
6380
|
-
confidence:
|
|
6381
|
-
evidence: { file: evidenceFile, line:
|
|
6412
|
+
provenance: Provenance16.EXTRACTED,
|
|
6413
|
+
confidence: confidenceForExtracted14("structural"),
|
|
6414
|
+
evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
|
|
6382
6415
|
};
|
|
6383
6416
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
6384
6417
|
edgesAdded++;
|
|
@@ -6392,15 +6425,209 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6392
6425
|
"cloudflare-service-binding",
|
|
6393
6426
|
svc.service,
|
|
6394
6427
|
evidenceFile,
|
|
6395
|
-
|
|
6428
|
+
lineContaining2(raw, svc.service)
|
|
6429
|
+
);
|
|
6430
|
+
nodesAdded += result.nodesAdded;
|
|
6431
|
+
edgesAdded += result.edgesAdded;
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
return { nodesAdded, edgesAdded };
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6437
|
+
// src/extract/infra/vercel.ts
|
|
6438
|
+
import { promises as fs20 } from "fs";
|
|
6439
|
+
import path36 from "path";
|
|
6440
|
+
import { EdgeType as EdgeType17 } from "@neat.is/types";
|
|
6441
|
+
var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
|
|
6442
|
+
async function readVercelConfig(dir) {
|
|
6443
|
+
for (const filename of VERCEL_CONFIG_FILENAMES) {
|
|
6444
|
+
const abs = path36.join(dir, filename);
|
|
6445
|
+
if (!await exists(abs)) continue;
|
|
6446
|
+
const raw = await fs20.readFile(abs, "utf8");
|
|
6447
|
+
const config = JSON.parse(maskCommentsInSource(raw));
|
|
6448
|
+
return { config, relFile: filename, raw };
|
|
6449
|
+
}
|
|
6450
|
+
return null;
|
|
6451
|
+
}
|
|
6452
|
+
async function readLinkedProjectName(dir) {
|
|
6453
|
+
const abs = path36.join(dir, ".vercel", "project.json");
|
|
6454
|
+
if (!await exists(abs)) return void 0;
|
|
6455
|
+
const parsed = JSON.parse(await fs20.readFile(abs, "utf8"));
|
|
6456
|
+
return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
|
|
6457
|
+
}
|
|
6458
|
+
function routeSource(route) {
|
|
6459
|
+
return route.source ?? route.src;
|
|
6460
|
+
}
|
|
6461
|
+
async function addVercelServices(graph, services, scanPath) {
|
|
6462
|
+
let nodesAdded = 0;
|
|
6463
|
+
let edgesAdded = 0;
|
|
6464
|
+
for (const service of services) {
|
|
6465
|
+
let read = null;
|
|
6466
|
+
let projectName;
|
|
6467
|
+
try {
|
|
6468
|
+
read = await readVercelConfig(service.dir);
|
|
6469
|
+
projectName = await readLinkedProjectName(service.dir);
|
|
6470
|
+
} catch (err) {
|
|
6471
|
+
recordExtractionError("infra vercel", path36.relative(scanPath, service.dir), err);
|
|
6472
|
+
continue;
|
|
6473
|
+
}
|
|
6474
|
+
if (!read && !projectName) continue;
|
|
6475
|
+
const serviceNode = graph.getNodeAttributes(service.node.id);
|
|
6476
|
+
if (serviceNode.platform !== "vercel" || projectName && serviceNode.platformName !== projectName) {
|
|
6477
|
+
const updated = {
|
|
6478
|
+
...serviceNode,
|
|
6479
|
+
platform: "vercel",
|
|
6480
|
+
...projectName ? { platformName: projectName } : {}
|
|
6481
|
+
};
|
|
6482
|
+
graph.replaceNodeAttributes(service.node.id, updated);
|
|
6483
|
+
}
|
|
6484
|
+
const anchorId = service.node.id;
|
|
6485
|
+
if (!read) continue;
|
|
6486
|
+
const { config, relFile, raw } = read;
|
|
6487
|
+
const evidenceFile = toPosix2(path36.relative(scanPath, path36.join(service.dir, relFile)));
|
|
6488
|
+
const add = (edgeType, kind, name) => {
|
|
6489
|
+
if (!name) return;
|
|
6490
|
+
const result = emitPlatformResourceEdge(
|
|
6491
|
+
graph,
|
|
6492
|
+
anchorId,
|
|
6493
|
+
edgeType,
|
|
6494
|
+
kind,
|
|
6495
|
+
name,
|
|
6496
|
+
"vercel",
|
|
6497
|
+
evidenceFile,
|
|
6498
|
+
lineContaining(raw, name)
|
|
6396
6499
|
);
|
|
6397
6500
|
nodesAdded += result.nodesAdded;
|
|
6398
6501
|
edgesAdded += result.edgesAdded;
|
|
6502
|
+
};
|
|
6503
|
+
add(EdgeType17.RUNS_ON, "vercel", "vercel");
|
|
6504
|
+
for (const cron of config.crons ?? []) add(EdgeType17.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
|
|
6505
|
+
for (const varName of Object.keys(config.env ?? {})) add(EdgeType17.DEPENDS_ON, "vercel-env-var", varName);
|
|
6506
|
+
for (const varName of Object.keys(config.build?.env ?? {})) add(EdgeType17.DEPENDS_ON, "vercel-env-var", varName);
|
|
6507
|
+
for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
|
|
6508
|
+
add(EdgeType17.CONNECTS_TO, "vercel-route", routeSource(route));
|
|
6399
6509
|
}
|
|
6400
6510
|
}
|
|
6401
6511
|
return { nodesAdded, edgesAdded };
|
|
6402
6512
|
}
|
|
6403
6513
|
|
|
6514
|
+
// src/extract/infra/railway.ts
|
|
6515
|
+
import { promises as fs21 } from "fs";
|
|
6516
|
+
import path37 from "path";
|
|
6517
|
+
import { parse as parseToml3 } from "smol-toml";
|
|
6518
|
+
import { EdgeType as EdgeType18 } from "@neat.is/types";
|
|
6519
|
+
var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
|
|
6520
|
+
async function readRailwayConfig(dir) {
|
|
6521
|
+
for (const filename of RAILWAY_FILENAMES) {
|
|
6522
|
+
const abs = path37.join(dir, filename);
|
|
6523
|
+
if (!await exists(abs)) continue;
|
|
6524
|
+
const raw = await fs21.readFile(abs, "utf8");
|
|
6525
|
+
const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
6526
|
+
return { config, relFile: filename, raw };
|
|
6527
|
+
}
|
|
6528
|
+
return null;
|
|
6529
|
+
}
|
|
6530
|
+
async function addRailwayServices(graph, services, scanPath) {
|
|
6531
|
+
let nodesAdded = 0;
|
|
6532
|
+
let edgesAdded = 0;
|
|
6533
|
+
for (const service of services) {
|
|
6534
|
+
let read = null;
|
|
6535
|
+
try {
|
|
6536
|
+
read = await readRailwayConfig(service.dir);
|
|
6537
|
+
} catch (err) {
|
|
6538
|
+
recordExtractionError("infra railway", path37.relative(scanPath, service.dir), err);
|
|
6539
|
+
continue;
|
|
6540
|
+
}
|
|
6541
|
+
if (!read) continue;
|
|
6542
|
+
const serviceNode = graph.getNodeAttributes(service.node.id);
|
|
6543
|
+
if (serviceNode.platform !== "railway") {
|
|
6544
|
+
graph.replaceNodeAttributes(service.node.id, { ...serviceNode, platform: "railway" });
|
|
6545
|
+
}
|
|
6546
|
+
const anchorId = service.node.id;
|
|
6547
|
+
const { config, relFile, raw } = read;
|
|
6548
|
+
const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, relFile)));
|
|
6549
|
+
const add = (edgeType, kind, name) => {
|
|
6550
|
+
if (!name) return;
|
|
6551
|
+
const result = emitPlatformResourceEdge(
|
|
6552
|
+
graph,
|
|
6553
|
+
anchorId,
|
|
6554
|
+
edgeType,
|
|
6555
|
+
kind,
|
|
6556
|
+
name,
|
|
6557
|
+
"railway",
|
|
6558
|
+
evidenceFile,
|
|
6559
|
+
lineContaining(raw, name)
|
|
6560
|
+
);
|
|
6561
|
+
nodesAdded += result.nodesAdded;
|
|
6562
|
+
edgesAdded += result.edgesAdded;
|
|
6563
|
+
};
|
|
6564
|
+
add(EdgeType18.RUNS_ON, "railway", "railway");
|
|
6565
|
+
add(EdgeType18.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
|
|
6566
|
+
add(EdgeType18.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
|
|
6567
|
+
}
|
|
6568
|
+
return { nodesAdded, edgesAdded };
|
|
6569
|
+
}
|
|
6570
|
+
|
|
6571
|
+
// src/extract/infra/supabase.ts
|
|
6572
|
+
import { promises as fs22 } from "fs";
|
|
6573
|
+
import path38 from "path";
|
|
6574
|
+
import { parse as parseToml4 } from "smol-toml";
|
|
6575
|
+
import { EdgeType as EdgeType19 } from "@neat.is/types";
|
|
6576
|
+
async function readSupabaseConfig(dir) {
|
|
6577
|
+
const relFile = path38.join("supabase", "config.toml");
|
|
6578
|
+
const abs = path38.join(dir, relFile);
|
|
6579
|
+
if (!await exists(abs)) return null;
|
|
6580
|
+
const raw = await fs22.readFile(abs, "utf8");
|
|
6581
|
+
const config = parseToml4(raw);
|
|
6582
|
+
return { config, relFile, raw };
|
|
6583
|
+
}
|
|
6584
|
+
async function addSupabaseProjects(graph, services, scanPath) {
|
|
6585
|
+
let nodesAdded = 0;
|
|
6586
|
+
let edgesAdded = 0;
|
|
6587
|
+
for (const service of services) {
|
|
6588
|
+
let read = null;
|
|
6589
|
+
try {
|
|
6590
|
+
read = await readSupabaseConfig(service.dir);
|
|
6591
|
+
} catch (err) {
|
|
6592
|
+
recordExtractionError("infra supabase", path38.relative(scanPath, service.dir), err);
|
|
6593
|
+
continue;
|
|
6594
|
+
}
|
|
6595
|
+
if (!read) continue;
|
|
6596
|
+
const { config, relFile, raw } = read;
|
|
6597
|
+
const projectId = typeof config.project_id === "string" ? config.project_id : void 0;
|
|
6598
|
+
const serviceNode = graph.getNodeAttributes(service.node.id);
|
|
6599
|
+
if (serviceNode.platform !== "supabase" || projectId && serviceNode.platformName !== projectId) {
|
|
6600
|
+
graph.replaceNodeAttributes(service.node.id, {
|
|
6601
|
+
...serviceNode,
|
|
6602
|
+
platform: "supabase",
|
|
6603
|
+
...projectId ? { platformName: projectId } : {}
|
|
6604
|
+
});
|
|
6605
|
+
}
|
|
6606
|
+
const anchorId = service.node.id;
|
|
6607
|
+
const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
|
|
6608
|
+
const add = (edgeType, kind, name) => {
|
|
6609
|
+
if (!name) return;
|
|
6610
|
+
const result = emitPlatformResourceEdge(
|
|
6611
|
+
graph,
|
|
6612
|
+
anchorId,
|
|
6613
|
+
edgeType,
|
|
6614
|
+
kind,
|
|
6615
|
+
name,
|
|
6616
|
+
"supabase",
|
|
6617
|
+
evidenceFile,
|
|
6618
|
+
lineContaining(raw, name)
|
|
6619
|
+
);
|
|
6620
|
+
nodesAdded += result.nodesAdded;
|
|
6621
|
+
edgesAdded += result.edgesAdded;
|
|
6622
|
+
};
|
|
6623
|
+
add(EdgeType19.RUNS_ON, "supabase", "supabase");
|
|
6624
|
+
for (const fn of Object.keys(config.functions ?? {})) add(EdgeType19.DEPENDS_ON, "supabase-function", fn);
|
|
6625
|
+
if (config.storage) add(EdgeType19.DEPENDS_ON, "supabase-storage", "storage");
|
|
6626
|
+
if (config.auth) add(EdgeType19.DEPENDS_ON, "supabase-auth", "auth");
|
|
6627
|
+
}
|
|
6628
|
+
return { nodesAdded, edgesAdded };
|
|
6629
|
+
}
|
|
6630
|
+
|
|
6404
6631
|
// src/extract/infra/index.ts
|
|
6405
6632
|
async function addInfra(graph, scanPath, services) {
|
|
6406
6633
|
const compose = await addComposeInfra(graph, scanPath, services);
|
|
@@ -6408,19 +6635,22 @@ async function addInfra(graph, scanPath, services) {
|
|
|
6408
6635
|
const terraform = await addTerraformResources(graph, scanPath);
|
|
6409
6636
|
const k8s = await addK8sResources(graph, scanPath);
|
|
6410
6637
|
const cloudflare = await addCloudflareWorkers(graph, services, scanPath);
|
|
6638
|
+
const vercel = await addVercelServices(graph, services, scanPath);
|
|
6639
|
+
const railway = await addRailwayServices(graph, services, scanPath);
|
|
6640
|
+
const supabase = await addSupabaseProjects(graph, services, scanPath);
|
|
6411
6641
|
return {
|
|
6412
|
-
nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded,
|
|
6413
|
-
edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded
|
|
6642
|
+
nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded + vercel.nodesAdded + railway.nodesAdded + supabase.nodesAdded,
|
|
6643
|
+
edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded + vercel.edgesAdded + railway.edgesAdded + supabase.edgesAdded
|
|
6414
6644
|
};
|
|
6415
6645
|
}
|
|
6416
6646
|
|
|
6417
6647
|
// src/extract/index.ts
|
|
6418
|
-
import
|
|
6648
|
+
import path40 from "path";
|
|
6419
6649
|
|
|
6420
6650
|
// src/extract/retire.ts
|
|
6421
6651
|
import { existsSync as existsSync2 } from "fs";
|
|
6422
|
-
import
|
|
6423
|
-
import { NodeType as NodeType14, Provenance as
|
|
6652
|
+
import path39 from "path";
|
|
6653
|
+
import { NodeType as NodeType14, Provenance as Provenance17 } from "@neat.is/types";
|
|
6424
6654
|
function dropOrphanedFileNodes(graph) {
|
|
6425
6655
|
const orphans = [];
|
|
6426
6656
|
graph.forEachNode((id, attrs) => {
|
|
@@ -6437,7 +6667,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
6437
6667
|
const toDrop = [];
|
|
6438
6668
|
graph.forEachEdge((id, attrs) => {
|
|
6439
6669
|
const edge = attrs;
|
|
6440
|
-
if (edge.provenance !==
|
|
6670
|
+
if (edge.provenance !== Provenance17.EXTRACTED) return;
|
|
6441
6671
|
if (!edge.evidence?.file) return;
|
|
6442
6672
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
6443
6673
|
});
|
|
@@ -6450,14 +6680,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
6450
6680
|
const bases = [scanPath, ...serviceDirs];
|
|
6451
6681
|
graph.forEachEdge((id, attrs) => {
|
|
6452
6682
|
const edge = attrs;
|
|
6453
|
-
if (edge.provenance !==
|
|
6683
|
+
if (edge.provenance !== Provenance17.EXTRACTED) return;
|
|
6454
6684
|
const evidenceFile = edge.evidence?.file;
|
|
6455
6685
|
if (!evidenceFile) return;
|
|
6456
|
-
if (
|
|
6686
|
+
if (path39.isAbsolute(evidenceFile)) {
|
|
6457
6687
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
6458
6688
|
return;
|
|
6459
6689
|
}
|
|
6460
|
-
const found = bases.some((base) => existsSync2(
|
|
6690
|
+
const found = bases.some((base) => existsSync2(path39.join(base, evidenceFile)));
|
|
6461
6691
|
if (!found) toDrop.push(id);
|
|
6462
6692
|
});
|
|
6463
6693
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -6499,7 +6729,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
6499
6729
|
}
|
|
6500
6730
|
const droppedEntries = drainDroppedExtracted();
|
|
6501
6731
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
6502
|
-
const rejectedPath =
|
|
6732
|
+
const rejectedPath = path40.join(path40.dirname(opts.errorsPath), "rejected.ndjson");
|
|
6503
6733
|
try {
|
|
6504
6734
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
6505
6735
|
} catch (err) {
|
|
@@ -6535,10 +6765,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
6535
6765
|
import {
|
|
6536
6766
|
databaseId as databaseId3,
|
|
6537
6767
|
DivergenceResultSchema,
|
|
6538
|
-
EdgeType as
|
|
6768
|
+
EdgeType as EdgeType20,
|
|
6539
6769
|
NodeType as NodeType15,
|
|
6540
6770
|
parseEdgeId,
|
|
6541
|
-
Provenance as
|
|
6771
|
+
Provenance as Provenance18
|
|
6542
6772
|
} from "@neat.is/types";
|
|
6543
6773
|
function bucketKey(source, target, type) {
|
|
6544
6774
|
return `${type}|${source}|${target}`;
|
|
@@ -6552,17 +6782,17 @@ function bucketEdges(graph) {
|
|
|
6552
6782
|
const key = bucketKey(e.source, e.target, e.type);
|
|
6553
6783
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
6554
6784
|
switch (provenance) {
|
|
6555
|
-
case
|
|
6785
|
+
case Provenance18.EXTRACTED:
|
|
6556
6786
|
cur.extracted = e;
|
|
6557
6787
|
break;
|
|
6558
|
-
case
|
|
6788
|
+
case Provenance18.OBSERVED:
|
|
6559
6789
|
cur.observed = e;
|
|
6560
6790
|
break;
|
|
6561
|
-
case
|
|
6791
|
+
case Provenance18.INFERRED:
|
|
6562
6792
|
cur.inferred = e;
|
|
6563
6793
|
break;
|
|
6564
6794
|
default:
|
|
6565
|
-
if (e.provenance ===
|
|
6795
|
+
if (e.provenance === Provenance18.STALE) cur.stale = e;
|
|
6566
6796
|
}
|
|
6567
6797
|
buckets.set(key, cur);
|
|
6568
6798
|
});
|
|
@@ -6596,14 +6826,14 @@ function gradedConfidence(edge) {
|
|
|
6596
6826
|
return clampConfidence(confidenceForEdge(edge));
|
|
6597
6827
|
}
|
|
6598
6828
|
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6829
|
+
EdgeType20.CALLS,
|
|
6830
|
+
EdgeType20.CONNECTS_TO,
|
|
6831
|
+
EdgeType20.PUBLISHES_TO,
|
|
6832
|
+
EdgeType20.CONSUMES_FROM
|
|
6603
6833
|
]);
|
|
6604
6834
|
function detectMissingDivergences(graph, bucket) {
|
|
6605
6835
|
const out = [];
|
|
6606
|
-
if (bucket.type ===
|
|
6836
|
+
if (bucket.type === EdgeType20.CONTAINS) return out;
|
|
6607
6837
|
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
6608
6838
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
6609
6839
|
out.push({
|
|
@@ -6644,7 +6874,7 @@ function declaredHostFor(svc) {
|
|
|
6644
6874
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
6645
6875
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6646
6876
|
const e = graph.getEdgeAttributes(edgeId);
|
|
6647
|
-
if (e.type ===
|
|
6877
|
+
if (e.type === EdgeType20.CONFIGURED_BY && e.provenance === Provenance18.EXTRACTED) {
|
|
6648
6878
|
return true;
|
|
6649
6879
|
}
|
|
6650
6880
|
}
|
|
@@ -6657,8 +6887,8 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
6657
6887
|
const out = [];
|
|
6658
6888
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6659
6889
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
6660
|
-
if (edge.type !==
|
|
6661
|
-
if (edge.provenance !==
|
|
6890
|
+
if (edge.type !== EdgeType20.CONNECTS_TO) continue;
|
|
6891
|
+
if (edge.provenance !== Provenance18.OBSERVED) continue;
|
|
6662
6892
|
const target = graph.getNodeAttributes(edge.target);
|
|
6663
6893
|
if (target.type !== NodeType15.DatabaseNode) continue;
|
|
6664
6894
|
const observedHost = target.host?.trim();
|
|
@@ -6682,8 +6912,8 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
6682
6912
|
const deps = svc.dependencies ?? {};
|
|
6683
6913
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6684
6914
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
6685
|
-
if (edge.type !==
|
|
6686
|
-
if (edge.provenance !==
|
|
6915
|
+
if (edge.type !== EdgeType20.CONNECTS_TO) continue;
|
|
6916
|
+
if (edge.provenance !== Provenance18.OBSERVED) continue;
|
|
6687
6917
|
const target = graph.getNodeAttributes(edge.target);
|
|
6688
6918
|
if (target.type !== NodeType15.DatabaseNode) continue;
|
|
6689
6919
|
for (const pair of compatPairs()) {
|
|
@@ -6805,9 +7035,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
6805
7035
|
}
|
|
6806
7036
|
|
|
6807
7037
|
// src/persist.ts
|
|
6808
|
-
import { promises as
|
|
6809
|
-
import
|
|
6810
|
-
import { Provenance as
|
|
7038
|
+
import { promises as fs23 } from "fs";
|
|
7039
|
+
import path41 from "path";
|
|
7040
|
+
import { Provenance as Provenance19, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
6811
7041
|
var SCHEMA_VERSION = 4;
|
|
6812
7042
|
function migrateV1ToV2(payload) {
|
|
6813
7043
|
const nodes = payload.graph.nodes;
|
|
@@ -6829,7 +7059,7 @@ function migrateV2ToV3(payload) {
|
|
|
6829
7059
|
for (const edge of edges) {
|
|
6830
7060
|
const attrs = edge.attributes;
|
|
6831
7061
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
6832
|
-
attrs.provenance =
|
|
7062
|
+
attrs.provenance = Provenance19.OBSERVED;
|
|
6833
7063
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
6834
7064
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
6835
7065
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -6843,7 +7073,7 @@ function migrateV2ToV3(payload) {
|
|
|
6843
7073
|
return { ...payload, schemaVersion: 3 };
|
|
6844
7074
|
}
|
|
6845
7075
|
async function ensureDir(filePath) {
|
|
6846
|
-
await
|
|
7076
|
+
await fs23.mkdir(path41.dirname(filePath), { recursive: true });
|
|
6847
7077
|
}
|
|
6848
7078
|
async function saveGraphToDisk(graph, outPath) {
|
|
6849
7079
|
await ensureDir(outPath);
|
|
@@ -6853,13 +7083,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
6853
7083
|
graph: graph.export()
|
|
6854
7084
|
};
|
|
6855
7085
|
const tmp = `${outPath}.tmp`;
|
|
6856
|
-
await
|
|
6857
|
-
await
|
|
7086
|
+
await fs23.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
7087
|
+
await fs23.rename(tmp, outPath);
|
|
6858
7088
|
}
|
|
6859
7089
|
async function loadGraphFromDisk(graph, outPath) {
|
|
6860
7090
|
let raw;
|
|
6861
7091
|
try {
|
|
6862
|
-
raw = await
|
|
7092
|
+
raw = await fs23.readFile(outPath, "utf8");
|
|
6863
7093
|
} catch (err) {
|
|
6864
7094
|
if (err.code === "ENOENT") return;
|
|
6865
7095
|
throw err;
|
|
@@ -6923,7 +7153,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
6923
7153
|
}
|
|
6924
7154
|
|
|
6925
7155
|
// src/diff.ts
|
|
6926
|
-
import { promises as
|
|
7156
|
+
import { promises as fs24 } from "fs";
|
|
6927
7157
|
async function loadSnapshotForDiff(target) {
|
|
6928
7158
|
if (/^https?:\/\//i.test(target)) {
|
|
6929
7159
|
const res = await fetch(target);
|
|
@@ -6932,7 +7162,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
6932
7162
|
}
|
|
6933
7163
|
return await res.json();
|
|
6934
7164
|
}
|
|
6935
|
-
const raw = await
|
|
7165
|
+
const raw = await fs24.readFile(target, "utf8");
|
|
6936
7166
|
return JSON.parse(raw);
|
|
6937
7167
|
}
|
|
6938
7168
|
function indexEntries(entries) {
|
|
@@ -6999,23 +7229,23 @@ function canonicalJson(value) {
|
|
|
6999
7229
|
}
|
|
7000
7230
|
|
|
7001
7231
|
// src/projects.ts
|
|
7002
|
-
import
|
|
7232
|
+
import path42 from "path";
|
|
7003
7233
|
function pathsForProject(project, baseDir) {
|
|
7004
7234
|
if (project === DEFAULT_PROJECT) {
|
|
7005
7235
|
return {
|
|
7006
|
-
snapshotPath:
|
|
7007
|
-
errorsPath:
|
|
7008
|
-
staleEventsPath:
|
|
7009
|
-
embeddingsCachePath:
|
|
7010
|
-
policyViolationsPath:
|
|
7236
|
+
snapshotPath: path42.join(baseDir, "graph.json"),
|
|
7237
|
+
errorsPath: path42.join(baseDir, "errors.ndjson"),
|
|
7238
|
+
staleEventsPath: path42.join(baseDir, "stale-events.ndjson"),
|
|
7239
|
+
embeddingsCachePath: path42.join(baseDir, "embeddings.json"),
|
|
7240
|
+
policyViolationsPath: path42.join(baseDir, "policy-violations.ndjson")
|
|
7011
7241
|
};
|
|
7012
7242
|
}
|
|
7013
7243
|
return {
|
|
7014
|
-
snapshotPath:
|
|
7015
|
-
errorsPath:
|
|
7016
|
-
staleEventsPath:
|
|
7017
|
-
embeddingsCachePath:
|
|
7018
|
-
policyViolationsPath:
|
|
7244
|
+
snapshotPath: path42.join(baseDir, `${project}.json`),
|
|
7245
|
+
errorsPath: path42.join(baseDir, `errors.${project}.ndjson`),
|
|
7246
|
+
staleEventsPath: path42.join(baseDir, `stale-events.${project}.ndjson`),
|
|
7247
|
+
embeddingsCachePath: path42.join(baseDir, `embeddings.${project}.json`),
|
|
7248
|
+
policyViolationsPath: path42.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
7019
7249
|
};
|
|
7020
7250
|
}
|
|
7021
7251
|
var Projects = class {
|
|
@@ -7054,9 +7284,9 @@ function parseExtraProjects(raw) {
|
|
|
7054
7284
|
}
|
|
7055
7285
|
|
|
7056
7286
|
// src/registry.ts
|
|
7057
|
-
import { promises as
|
|
7287
|
+
import { promises as fs25 } from "fs";
|
|
7058
7288
|
import os2 from "os";
|
|
7059
|
-
import
|
|
7289
|
+
import path43 from "path";
|
|
7060
7290
|
import {
|
|
7061
7291
|
RegistryFileSchema
|
|
7062
7292
|
} from "@neat.is/types";
|
|
@@ -7064,20 +7294,20 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
7064
7294
|
var LOCK_RETRY_MS = 50;
|
|
7065
7295
|
function neatHome() {
|
|
7066
7296
|
const override = process.env.NEAT_HOME;
|
|
7067
|
-
if (override && override.length > 0) return
|
|
7068
|
-
return
|
|
7297
|
+
if (override && override.length > 0) return path43.resolve(override);
|
|
7298
|
+
return path43.join(os2.homedir(), ".neat");
|
|
7069
7299
|
}
|
|
7070
7300
|
function registryPath() {
|
|
7071
|
-
return
|
|
7301
|
+
return path43.join(neatHome(), "projects.json");
|
|
7072
7302
|
}
|
|
7073
7303
|
function registryLockPath() {
|
|
7074
|
-
return
|
|
7304
|
+
return path43.join(neatHome(), "projects.json.lock");
|
|
7075
7305
|
}
|
|
7076
7306
|
function daemonPidPath() {
|
|
7077
|
-
return
|
|
7307
|
+
return path43.join(neatHome(), "neatd.pid");
|
|
7078
7308
|
}
|
|
7079
7309
|
function daemonsDir() {
|
|
7080
|
-
return
|
|
7310
|
+
return path43.join(neatHome(), "daemons");
|
|
7081
7311
|
}
|
|
7082
7312
|
function isFiniteInt(v) {
|
|
7083
7313
|
return typeof v === "number" && Number.isFinite(v);
|
|
@@ -7110,7 +7340,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
7110
7340
|
const dir = daemonsDir();
|
|
7111
7341
|
let names;
|
|
7112
7342
|
try {
|
|
7113
|
-
names = await
|
|
7343
|
+
names = await fs25.readdir(dir);
|
|
7114
7344
|
} catch (err) {
|
|
7115
7345
|
if (err.code === "ENOENT") return [];
|
|
7116
7346
|
throw err;
|
|
@@ -7118,10 +7348,10 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
7118
7348
|
const out = [];
|
|
7119
7349
|
for (const name of names) {
|
|
7120
7350
|
if (!name.endsWith(".json")) continue;
|
|
7121
|
-
const file =
|
|
7351
|
+
const file = path43.join(dir, name);
|
|
7122
7352
|
let raw;
|
|
7123
7353
|
try {
|
|
7124
|
-
raw = await
|
|
7354
|
+
raw = await fs25.readFile(file, "utf8");
|
|
7125
7355
|
} catch {
|
|
7126
7356
|
continue;
|
|
7127
7357
|
}
|
|
@@ -7134,7 +7364,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
7134
7364
|
return out;
|
|
7135
7365
|
}
|
|
7136
7366
|
async function removeDaemonRecord(source) {
|
|
7137
|
-
await
|
|
7367
|
+
await fs25.unlink(source).catch(() => {
|
|
7138
7368
|
});
|
|
7139
7369
|
}
|
|
7140
7370
|
async function listMachineProjects(probe = defaultDiscoveryProbe) {
|
|
@@ -7191,7 +7421,7 @@ function isPidAliveDefault(pid) {
|
|
|
7191
7421
|
}
|
|
7192
7422
|
async function readPidFile(file) {
|
|
7193
7423
|
try {
|
|
7194
|
-
const raw = await
|
|
7424
|
+
const raw = await fs25.readFile(file, "utf8");
|
|
7195
7425
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
7196
7426
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
7197
7427
|
} catch {
|
|
@@ -7239,32 +7469,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
7239
7469
|
}
|
|
7240
7470
|
}
|
|
7241
7471
|
async function normalizeProjectPath(input) {
|
|
7242
|
-
const resolved =
|
|
7472
|
+
const resolved = path43.resolve(input);
|
|
7243
7473
|
try {
|
|
7244
|
-
return await
|
|
7474
|
+
return await fs25.realpath(resolved);
|
|
7245
7475
|
} catch {
|
|
7246
7476
|
return resolved;
|
|
7247
7477
|
}
|
|
7248
7478
|
}
|
|
7249
7479
|
async function writeAtomically(target, contents) {
|
|
7250
|
-
await
|
|
7480
|
+
await fs25.mkdir(path43.dirname(target), { recursive: true });
|
|
7251
7481
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
7252
|
-
const fd = await
|
|
7482
|
+
const fd = await fs25.open(tmp, "w");
|
|
7253
7483
|
try {
|
|
7254
7484
|
await fd.writeFile(contents, "utf8");
|
|
7255
7485
|
await fd.sync();
|
|
7256
7486
|
} finally {
|
|
7257
7487
|
await fd.close();
|
|
7258
7488
|
}
|
|
7259
|
-
await
|
|
7489
|
+
await fs25.rename(tmp, target);
|
|
7260
7490
|
}
|
|
7261
7491
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
7262
7492
|
const deadline = Date.now() + timeoutMs;
|
|
7263
|
-
await
|
|
7493
|
+
await fs25.mkdir(path43.dirname(lockPath), { recursive: true });
|
|
7264
7494
|
let probedHolder = false;
|
|
7265
7495
|
while (true) {
|
|
7266
7496
|
try {
|
|
7267
|
-
const fd = await
|
|
7497
|
+
const fd = await fs25.open(lockPath, "wx");
|
|
7268
7498
|
try {
|
|
7269
7499
|
await fd.writeFile(`${process.pid}
|
|
7270
7500
|
`, "utf8");
|
|
@@ -7289,7 +7519,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
|
|
|
7289
7519
|
}
|
|
7290
7520
|
}
|
|
7291
7521
|
async function releaseLock(lockPath) {
|
|
7292
|
-
await
|
|
7522
|
+
await fs25.unlink(lockPath).catch(() => {
|
|
7293
7523
|
});
|
|
7294
7524
|
}
|
|
7295
7525
|
async function withLock(fn) {
|
|
@@ -7305,7 +7535,7 @@ async function readRegistry() {
|
|
|
7305
7535
|
const file = registryPath();
|
|
7306
7536
|
let raw;
|
|
7307
7537
|
try {
|
|
7308
|
-
raw = await
|
|
7538
|
+
raw = await fs25.readFile(file, "utf8");
|
|
7309
7539
|
} catch (err) {
|
|
7310
7540
|
if (err.code === "ENOENT") {
|
|
7311
7541
|
return { version: 1, projects: [] };
|
|
@@ -7410,7 +7640,7 @@ function pruneTtlMs() {
|
|
|
7410
7640
|
}
|
|
7411
7641
|
async function statPathStatus(p) {
|
|
7412
7642
|
try {
|
|
7413
|
-
const stat = await
|
|
7643
|
+
const stat = await fs25.stat(p);
|
|
7414
7644
|
return stat.isDirectory() ? "present" : "unknown";
|
|
7415
7645
|
} catch (err) {
|
|
7416
7646
|
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
@@ -7455,14 +7685,14 @@ import cors from "@fastify/cors";
|
|
|
7455
7685
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
7456
7686
|
|
|
7457
7687
|
// src/extend/index.ts
|
|
7458
|
-
import { promises as
|
|
7459
|
-
import
|
|
7688
|
+
import { promises as fs27 } from "fs";
|
|
7689
|
+
import path45 from "path";
|
|
7460
7690
|
import os3 from "os";
|
|
7461
7691
|
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
7462
7692
|
|
|
7463
7693
|
// src/installers/package-manager.ts
|
|
7464
|
-
import { promises as
|
|
7465
|
-
import
|
|
7694
|
+
import { promises as fs26 } from "fs";
|
|
7695
|
+
import path44 from "path";
|
|
7466
7696
|
import { spawn } from "child_process";
|
|
7467
7697
|
var LOCKFILE_PRIORITY = [
|
|
7468
7698
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -7477,29 +7707,29 @@ var LOCKFILE_PRIORITY = [
|
|
|
7477
7707
|
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
7478
7708
|
async function exists2(p) {
|
|
7479
7709
|
try {
|
|
7480
|
-
await
|
|
7710
|
+
await fs26.access(p);
|
|
7481
7711
|
return true;
|
|
7482
7712
|
} catch {
|
|
7483
7713
|
return false;
|
|
7484
7714
|
}
|
|
7485
7715
|
}
|
|
7486
7716
|
async function detectPackageManager(serviceDir) {
|
|
7487
|
-
let dir =
|
|
7717
|
+
let dir = path44.resolve(serviceDir);
|
|
7488
7718
|
const stops = /* @__PURE__ */ new Set();
|
|
7489
7719
|
for (let i = 0; i < 64; i++) {
|
|
7490
7720
|
if (stops.has(dir)) break;
|
|
7491
7721
|
stops.add(dir);
|
|
7492
7722
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
7493
|
-
const lockPath =
|
|
7723
|
+
const lockPath = path44.join(dir, candidate.lockfile);
|
|
7494
7724
|
if (await exists2(lockPath)) {
|
|
7495
7725
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
7496
7726
|
}
|
|
7497
7727
|
}
|
|
7498
|
-
const parent =
|
|
7728
|
+
const parent = path44.dirname(dir);
|
|
7499
7729
|
if (parent === dir) break;
|
|
7500
7730
|
dir = parent;
|
|
7501
7731
|
}
|
|
7502
|
-
return { pm: "npm", cwd:
|
|
7732
|
+
return { pm: "npm", cwd: path44.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
7503
7733
|
}
|
|
7504
7734
|
async function runPackageManagerInstall(cmd) {
|
|
7505
7735
|
return new Promise((resolve) => {
|
|
@@ -7541,30 +7771,30 @@ ${err.message}`
|
|
|
7541
7771
|
// src/extend/index.ts
|
|
7542
7772
|
async function fileExists2(p) {
|
|
7543
7773
|
try {
|
|
7544
|
-
await
|
|
7774
|
+
await fs27.access(p);
|
|
7545
7775
|
return true;
|
|
7546
7776
|
} catch {
|
|
7547
7777
|
return false;
|
|
7548
7778
|
}
|
|
7549
7779
|
}
|
|
7550
7780
|
async function readPackageJson(scanPath) {
|
|
7551
|
-
const pkgPath =
|
|
7552
|
-
const raw = await
|
|
7781
|
+
const pkgPath = path45.join(scanPath, "package.json");
|
|
7782
|
+
const raw = await fs27.readFile(pkgPath, "utf8");
|
|
7553
7783
|
return JSON.parse(raw);
|
|
7554
7784
|
}
|
|
7555
7785
|
async function findHookFiles(scanPath) {
|
|
7556
|
-
const entries = await
|
|
7786
|
+
const entries = await fs27.readdir(scanPath);
|
|
7557
7787
|
return entries.filter(
|
|
7558
7788
|
(e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
|
|
7559
7789
|
).sort();
|
|
7560
7790
|
}
|
|
7561
7791
|
function extendLogPath() {
|
|
7562
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
7792
|
+
return process.env.NEAT_EXTEND_LOG ?? path45.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
7563
7793
|
}
|
|
7564
7794
|
async function appendExtendLog(entry) {
|
|
7565
7795
|
const logPath = extendLogPath();
|
|
7566
|
-
await
|
|
7567
|
-
await
|
|
7796
|
+
await fs27.mkdir(path45.dirname(logPath), { recursive: true });
|
|
7797
|
+
await fs27.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
7568
7798
|
}
|
|
7569
7799
|
function splicedContent(fileContent, snippet2) {
|
|
7570
7800
|
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
@@ -7622,7 +7852,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
7622
7852
|
}
|
|
7623
7853
|
async function describeProjectInstrumentation(ctx) {
|
|
7624
7854
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
7625
|
-
const envNeat = await fileExists2(
|
|
7855
|
+
const envNeat = await fileExists2(path45.join(ctx.scanPath, ".env.neat"));
|
|
7626
7856
|
const registryInstrPackages = new Set(
|
|
7627
7857
|
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
7628
7858
|
);
|
|
@@ -7644,31 +7874,31 @@ async function applyExtension(ctx, args, options) {
|
|
|
7644
7874
|
);
|
|
7645
7875
|
}
|
|
7646
7876
|
for (const file of hookFiles) {
|
|
7647
|
-
const content = await
|
|
7877
|
+
const content = await fs27.readFile(path45.join(ctx.scanPath, file), "utf8");
|
|
7648
7878
|
if (content.includes(args.registration_snippet)) {
|
|
7649
7879
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
7650
7880
|
}
|
|
7651
7881
|
}
|
|
7652
7882
|
const primaryFile = hookFiles[0];
|
|
7653
|
-
const primaryPath =
|
|
7883
|
+
const primaryPath = path45.join(ctx.scanPath, primaryFile);
|
|
7654
7884
|
const filesTouched = [];
|
|
7655
7885
|
const depsAdded = [];
|
|
7656
|
-
const pkgPath =
|
|
7886
|
+
const pkgPath = path45.join(ctx.scanPath, "package.json");
|
|
7657
7887
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
7658
7888
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
7659
7889
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
7660
|
-
await
|
|
7890
|
+
await fs27.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
7661
7891
|
filesTouched.push("package.json");
|
|
7662
7892
|
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
7663
7893
|
}
|
|
7664
|
-
const hookContent = await
|
|
7894
|
+
const hookContent = await fs27.readFile(primaryPath, "utf8");
|
|
7665
7895
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
7666
7896
|
if (!patched) {
|
|
7667
7897
|
throw new Error(
|
|
7668
7898
|
`Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
|
|
7669
7899
|
);
|
|
7670
7900
|
}
|
|
7671
|
-
await
|
|
7901
|
+
await fs27.writeFile(primaryPath, patched, "utf8");
|
|
7672
7902
|
filesTouched.push(primaryFile);
|
|
7673
7903
|
const cmd = await detectPackageManager(ctx.scanPath);
|
|
7674
7904
|
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
@@ -7699,7 +7929,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
7699
7929
|
};
|
|
7700
7930
|
}
|
|
7701
7931
|
for (const file of hookFiles) {
|
|
7702
|
-
const content = await
|
|
7932
|
+
const content = await fs27.readFile(path45.join(ctx.scanPath, file), "utf8");
|
|
7703
7933
|
if (content.includes(args.registration_snippet)) {
|
|
7704
7934
|
return {
|
|
7705
7935
|
library: args.library,
|
|
@@ -7721,7 +7951,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
7721
7951
|
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
7722
7952
|
filesTouched.push("package.json");
|
|
7723
7953
|
}
|
|
7724
|
-
const hookContent = await
|
|
7954
|
+
const hookContent = await fs27.readFile(path45.join(ctx.scanPath, primaryFile), "utf8");
|
|
7725
7955
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
7726
7956
|
if (patched) {
|
|
7727
7957
|
filesTouched.push(primaryFile);
|
|
@@ -7736,28 +7966,28 @@ async function rollbackExtension(ctx, args) {
|
|
|
7736
7966
|
if (!await fileExists2(logPath)) {
|
|
7737
7967
|
return { undone: false, message: "no apply found for library" };
|
|
7738
7968
|
}
|
|
7739
|
-
const raw = await
|
|
7969
|
+
const raw = await fs27.readFile(logPath, "utf8");
|
|
7740
7970
|
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
7741
7971
|
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
7742
7972
|
if (!match) {
|
|
7743
7973
|
return { undone: false, message: "no apply found for library" };
|
|
7744
7974
|
}
|
|
7745
|
-
const pkgPath =
|
|
7975
|
+
const pkgPath = path45.join(ctx.scanPath, "package.json");
|
|
7746
7976
|
if (await fileExists2(pkgPath)) {
|
|
7747
7977
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
7748
7978
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
7749
7979
|
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
7750
7980
|
pkg.dependencies = rest;
|
|
7751
|
-
await
|
|
7981
|
+
await fs27.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
7752
7982
|
}
|
|
7753
7983
|
}
|
|
7754
7984
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
7755
7985
|
for (const file of hookFiles) {
|
|
7756
|
-
const filePath =
|
|
7757
|
-
const content = await
|
|
7986
|
+
const filePath = path45.join(ctx.scanPath, file);
|
|
7987
|
+
const content = await fs27.readFile(filePath, "utf8");
|
|
7758
7988
|
if (content.includes(match.registration_snippet)) {
|
|
7759
7989
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
7760
|
-
await
|
|
7990
|
+
await fs27.writeFile(filePath, filtered, "utf8");
|
|
7761
7991
|
break;
|
|
7762
7992
|
}
|
|
7763
7993
|
}
|
|
@@ -7866,6 +8096,332 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
7866
8096
|
reply.raw.on("error", closeConnection);
|
|
7867
8097
|
}
|
|
7868
8098
|
|
|
8099
|
+
// src/connectors-config.ts
|
|
8100
|
+
import os4 from "os";
|
|
8101
|
+
import path46 from "path";
|
|
8102
|
+
import { promises as fs28 } from "fs";
|
|
8103
|
+
var CONNECTORS_CONFIG_VERSION = 1;
|
|
8104
|
+
var EnvRefUnsetError = class extends Error {
|
|
8105
|
+
ref;
|
|
8106
|
+
varName;
|
|
8107
|
+
constructor(ref, varName) {
|
|
8108
|
+
super(`${ref} is unset`);
|
|
8109
|
+
this.name = "EnvRefUnsetError";
|
|
8110
|
+
this.ref = ref;
|
|
8111
|
+
this.varName = varName;
|
|
8112
|
+
}
|
|
8113
|
+
};
|
|
8114
|
+
function neatHome2() {
|
|
8115
|
+
const override = process.env.NEAT_HOME;
|
|
8116
|
+
if (override && override.length > 0) return path46.resolve(override);
|
|
8117
|
+
return path46.join(os4.homedir(), ".neat");
|
|
8118
|
+
}
|
|
8119
|
+
function connectorsConfigPath(home = neatHome2()) {
|
|
8120
|
+
return path46.join(home, "connectors.json");
|
|
8121
|
+
}
|
|
8122
|
+
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
8123
|
+
async function warnIfModeLooserThan0600(file) {
|
|
8124
|
+
if (process.platform === "win32") return;
|
|
8125
|
+
try {
|
|
8126
|
+
const stat = await fs28.stat(file);
|
|
8127
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
8128
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
8129
|
+
console.warn(
|
|
8130
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
|
|
8131
|
+
);
|
|
8132
|
+
}
|
|
8133
|
+
} catch {
|
|
8134
|
+
}
|
|
8135
|
+
}
|
|
8136
|
+
async function readConnectorsConfig(home = neatHome2()) {
|
|
8137
|
+
const file = connectorsConfigPath(home);
|
|
8138
|
+
let raw;
|
|
8139
|
+
try {
|
|
8140
|
+
raw = await fs28.readFile(file, "utf8");
|
|
8141
|
+
} catch (err) {
|
|
8142
|
+
if (err.code === "ENOENT") {
|
|
8143
|
+
return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
|
|
8144
|
+
}
|
|
8145
|
+
throw err;
|
|
8146
|
+
}
|
|
8147
|
+
await warnIfModeLooserThan0600(file);
|
|
8148
|
+
let parsed;
|
|
8149
|
+
try {
|
|
8150
|
+
parsed = JSON.parse(raw);
|
|
8151
|
+
} catch (err) {
|
|
8152
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
8153
|
+
}
|
|
8154
|
+
return validateConfig(parsed, file);
|
|
8155
|
+
}
|
|
8156
|
+
function validateConfig(parsed, file) {
|
|
8157
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
8158
|
+
throw new Error(`${file} must be a JSON object with a "connectors" array`);
|
|
8159
|
+
}
|
|
8160
|
+
const obj = parsed;
|
|
8161
|
+
const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
|
|
8162
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
8163
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
8164
|
+
}
|
|
8165
|
+
const rawConnectors = obj.connectors;
|
|
8166
|
+
if (!Array.isArray(rawConnectors)) {
|
|
8167
|
+
throw new Error(`${file}: "connectors" must be an array`);
|
|
8168
|
+
}
|
|
8169
|
+
const connectors = rawConnectors.map((entry, i) => validateEntry(entry, i, file));
|
|
8170
|
+
return { version, connectors };
|
|
8171
|
+
}
|
|
8172
|
+
function validateEntry(entry, index, file) {
|
|
8173
|
+
const where = `${file}: connectors[${index}]`;
|
|
8174
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
8175
|
+
throw new Error(`${where} must be an object`);
|
|
8176
|
+
}
|
|
8177
|
+
const e = entry;
|
|
8178
|
+
const id = e.id;
|
|
8179
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
8180
|
+
throw new Error(`${where}.id must be a non-empty string`);
|
|
8181
|
+
}
|
|
8182
|
+
const provider = e.provider;
|
|
8183
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
8184
|
+
throw new Error(`${where}.provider must be a non-empty string`);
|
|
8185
|
+
}
|
|
8186
|
+
if (e.project !== void 0 && typeof e.project !== "string") {
|
|
8187
|
+
throw new Error(`${where}.project must be a string when present`);
|
|
8188
|
+
}
|
|
8189
|
+
const credential = validateCredentialRef(e.credential, `${where}.credential`);
|
|
8190
|
+
let options;
|
|
8191
|
+
if (e.options !== void 0) {
|
|
8192
|
+
if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
|
|
8193
|
+
throw new Error(`${where}.options must be an object when present`);
|
|
8194
|
+
}
|
|
8195
|
+
options = e.options;
|
|
8196
|
+
}
|
|
8197
|
+
return {
|
|
8198
|
+
id,
|
|
8199
|
+
provider,
|
|
8200
|
+
...typeof e.project === "string" ? { project: e.project } : {},
|
|
8201
|
+
credential,
|
|
8202
|
+
...options ? { options } : {}
|
|
8203
|
+
};
|
|
8204
|
+
}
|
|
8205
|
+
function validateCredentialRef(raw, where) {
|
|
8206
|
+
if (typeof raw === "string") {
|
|
8207
|
+
if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
|
|
8208
|
+
return raw;
|
|
8209
|
+
}
|
|
8210
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
8211
|
+
const fields = raw;
|
|
8212
|
+
const keys = Object.keys(fields);
|
|
8213
|
+
if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
|
|
8214
|
+
for (const key of keys) {
|
|
8215
|
+
const v = fields[key];
|
|
8216
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
8217
|
+
throw new Error(`${where}.${key} must be a non-empty string`);
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
return fields;
|
|
8221
|
+
}
|
|
8222
|
+
throw new Error(`${where} must be a string or an object of strings`);
|
|
8223
|
+
}
|
|
8224
|
+
function resolveCredential(ref, env = process.env) {
|
|
8225
|
+
if (typeof ref === "string") {
|
|
8226
|
+
return { kind: "single", value: resolveRef(ref, env) };
|
|
8227
|
+
}
|
|
8228
|
+
const fields = {};
|
|
8229
|
+
for (const [key, value] of Object.entries(ref)) {
|
|
8230
|
+
fields[key] = resolveRef(value, env);
|
|
8231
|
+
}
|
|
8232
|
+
return { kind: "fields", fields };
|
|
8233
|
+
}
|
|
8234
|
+
function resolveRef(value, env) {
|
|
8235
|
+
if (value.length > 1 && value.startsWith("$")) {
|
|
8236
|
+
const varName = value.slice(1);
|
|
8237
|
+
const resolved = env[varName];
|
|
8238
|
+
if (resolved === void 0 || resolved.length === 0) {
|
|
8239
|
+
throw new EnvRefUnsetError(value, varName);
|
|
8240
|
+
}
|
|
8241
|
+
return resolved;
|
|
8242
|
+
}
|
|
8243
|
+
return value;
|
|
8244
|
+
}
|
|
8245
|
+
function connectorMatchesProject(entry, project) {
|
|
8246
|
+
return entry.project === void 0 || entry.project === project;
|
|
8247
|
+
}
|
|
8248
|
+
var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
|
|
8249
|
+
var CONNECTORS_LOCK_RETRY_MS = 50;
|
|
8250
|
+
function connectorsConfigLockPath(home = neatHome2()) {
|
|
8251
|
+
return path46.join(home, "connectors.json.lock");
|
|
8252
|
+
}
|
|
8253
|
+
function isEnvRef(value) {
|
|
8254
|
+
return value.length > 1 && value.startsWith("$");
|
|
8255
|
+
}
|
|
8256
|
+
function redactCredentialRef(ref) {
|
|
8257
|
+
const one = (value) => isEnvRef(value) ? value : "****";
|
|
8258
|
+
if (typeof ref === "string") return one(ref);
|
|
8259
|
+
const out = {};
|
|
8260
|
+
for (const [key, value] of Object.entries(ref)) out[key] = one(value);
|
|
8261
|
+
return out;
|
|
8262
|
+
}
|
|
8263
|
+
async function writeConfigAtomically0600(file, contents) {
|
|
8264
|
+
await fs28.mkdir(path46.dirname(file), { recursive: true });
|
|
8265
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
8266
|
+
const fd = await fs28.open(tmp, "w", 384);
|
|
8267
|
+
try {
|
|
8268
|
+
await fd.writeFile(contents, "utf8");
|
|
8269
|
+
await fd.chmod(384);
|
|
8270
|
+
await fd.sync();
|
|
8271
|
+
} finally {
|
|
8272
|
+
await fd.close();
|
|
8273
|
+
}
|
|
8274
|
+
await fs28.rename(tmp, file);
|
|
8275
|
+
}
|
|
8276
|
+
async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
|
|
8277
|
+
const deadline = Date.now() + timeoutMs;
|
|
8278
|
+
await fs28.mkdir(path46.dirname(lockPath), { recursive: true });
|
|
8279
|
+
for (; ; ) {
|
|
8280
|
+
try {
|
|
8281
|
+
const fd = await fs28.open(lockPath, "wx");
|
|
8282
|
+
try {
|
|
8283
|
+
await fd.writeFile(`${process.pid}
|
|
8284
|
+
`, "utf8");
|
|
8285
|
+
} finally {
|
|
8286
|
+
await fd.close();
|
|
8287
|
+
}
|
|
8288
|
+
return;
|
|
8289
|
+
} catch (err) {
|
|
8290
|
+
if (err.code !== "EEXIST") throw err;
|
|
8291
|
+
if (Date.now() >= deadline) {
|
|
8292
|
+
throw new Error(
|
|
8293
|
+
`neat connector: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process may be writing the connector config; if none is, remove that file by hand.`
|
|
8294
|
+
);
|
|
8295
|
+
}
|
|
8296
|
+
await new Promise((r) => setTimeout(r, CONNECTORS_LOCK_RETRY_MS));
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
async function releaseConnectorsLock(lockPath) {
|
|
8301
|
+
await fs28.unlink(lockPath).catch(() => {
|
|
8302
|
+
});
|
|
8303
|
+
}
|
|
8304
|
+
async function withConnectorsLock(home, fn) {
|
|
8305
|
+
const lock = connectorsConfigLockPath(home);
|
|
8306
|
+
await acquireConnectorsLock(lock);
|
|
8307
|
+
try {
|
|
8308
|
+
return await fn();
|
|
8309
|
+
} finally {
|
|
8310
|
+
await releaseConnectorsLock(lock);
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
function serializeConfig(config) {
|
|
8314
|
+
return JSON.stringify(
|
|
8315
|
+
{ version: config.version ?? CONNECTORS_CONFIG_VERSION, connectors: config.connectors },
|
|
8316
|
+
null,
|
|
8317
|
+
2
|
|
8318
|
+
) + "\n";
|
|
8319
|
+
}
|
|
8320
|
+
async function upsertConnectorEntry(entry, home = neatHome2()) {
|
|
8321
|
+
const file = connectorsConfigPath(home);
|
|
8322
|
+
return withConnectorsLock(home, async () => {
|
|
8323
|
+
const config = await readConnectorsConfig(home);
|
|
8324
|
+
validateEntry(entry, config.connectors.length, file);
|
|
8325
|
+
const idx = config.connectors.findIndex((c) => c.id === entry.id);
|
|
8326
|
+
const replaced = idx >= 0;
|
|
8327
|
+
if (replaced) config.connectors[idx] = entry;
|
|
8328
|
+
else config.connectors.push(entry);
|
|
8329
|
+
await writeConfigAtomically0600(file, serializeConfig(config));
|
|
8330
|
+
return { replaced };
|
|
8331
|
+
});
|
|
8332
|
+
}
|
|
8333
|
+
async function removeConnectorEntry(id, home = neatHome2()) {
|
|
8334
|
+
const file = connectorsConfigPath(home);
|
|
8335
|
+
return withConnectorsLock(home, async () => {
|
|
8336
|
+
const config = await readConnectorsConfig(home);
|
|
8337
|
+
const idx = config.connectors.findIndex((c) => c.id === id);
|
|
8338
|
+
if (idx < 0) return void 0;
|
|
8339
|
+
const [removed] = config.connectors.splice(idx, 1);
|
|
8340
|
+
await writeConfigAtomically0600(file, serializeConfig(config));
|
|
8341
|
+
return removed;
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
function slugFragment(value) {
|
|
8345
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8346
|
+
}
|
|
8347
|
+
function autoSlugConnectorId(provider, project, existingIds) {
|
|
8348
|
+
const base = slugFragment(provider) || "connector";
|
|
8349
|
+
if (!existingIds.has(base)) return base;
|
|
8350
|
+
const projectFrag = project ? slugFragment(project) : "";
|
|
8351
|
+
if (projectFrag) {
|
|
8352
|
+
const withProject = `${base}-${projectFrag}`;
|
|
8353
|
+
if (!existingIds.has(withProject)) return withProject;
|
|
8354
|
+
}
|
|
8355
|
+
const stem = projectFrag ? `${base}-${projectFrag}` : base;
|
|
8356
|
+
for (let n = 2; ; n++) {
|
|
8357
|
+
const candidate = `${stem}-${n}`;
|
|
8358
|
+
if (!existingIds.has(candidate)) return candidate;
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
function describeCredential(ref, env = process.env) {
|
|
8362
|
+
const one = (value, field) => {
|
|
8363
|
+
if (isEnvRef(value)) {
|
|
8364
|
+
const varName = value.slice(1);
|
|
8365
|
+
const resolved = env[varName];
|
|
8366
|
+
const set = typeof resolved === "string" && resolved.length > 0;
|
|
8367
|
+
return {
|
|
8368
|
+
...field ? { field } : {},
|
|
8369
|
+
kind: "env-ref",
|
|
8370
|
+
display: value,
|
|
8371
|
+
status: set ? "set" : "unset"
|
|
8372
|
+
};
|
|
8373
|
+
}
|
|
8374
|
+
return { ...field ? { field } : {}, kind: "plaintext", display: "****" };
|
|
8375
|
+
};
|
|
8376
|
+
if (typeof ref === "string") return [one(ref)];
|
|
8377
|
+
return Object.entries(ref).map(([k, v]) => one(v, k));
|
|
8378
|
+
}
|
|
8379
|
+
|
|
8380
|
+
// src/connectors/status.ts
|
|
8381
|
+
var CONNECTOR_STALE_THRESHOLD_MS = 5 * 6e4;
|
|
8382
|
+
var records = /* @__PURE__ */ new Map();
|
|
8383
|
+
var MAX_ERROR_LEN = 200;
|
|
8384
|
+
function sanitizePollError(err) {
|
|
8385
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8386
|
+
const collapsed = msg.replace(/\s+/g, " ").trim();
|
|
8387
|
+
return collapsed.length > MAX_ERROR_LEN ? `${collapsed.slice(0, MAX_ERROR_LEN - 1)}\u2026` : collapsed;
|
|
8388
|
+
}
|
|
8389
|
+
function recordConnectorPoll(id, tick) {
|
|
8390
|
+
const prev = records.get(id);
|
|
8391
|
+
records.set(id, {
|
|
8392
|
+
lastPollAt: tick.at,
|
|
8393
|
+
lastOutcome: tick.outcome,
|
|
8394
|
+
lastError: tick.outcome === "error" ? tick.error ?? null : null,
|
|
8395
|
+
signalsLastPoll: tick.signalsLastPoll ?? 0,
|
|
8396
|
+
lastOkAt: tick.outcome === "ok" ? tick.at : prev?.lastOkAt ?? null
|
|
8397
|
+
});
|
|
8398
|
+
}
|
|
8399
|
+
function deriveState(rec, now, thresholdMs) {
|
|
8400
|
+
if (rec.lastOutcome === "error") return "error";
|
|
8401
|
+
const okAt = rec.lastOkAt ? Date.parse(rec.lastOkAt) : NaN;
|
|
8402
|
+
if (!Number.isNaN(okAt) && now - okAt > thresholdMs) return "stale";
|
|
8403
|
+
return "healthy";
|
|
8404
|
+
}
|
|
8405
|
+
function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_THRESHOLD_MS) {
|
|
8406
|
+
const rec = records.get(id);
|
|
8407
|
+
if (!rec) {
|
|
8408
|
+
return {
|
|
8409
|
+
state: "idle",
|
|
8410
|
+
lastPollAt: null,
|
|
8411
|
+
lastOutcome: null,
|
|
8412
|
+
lastError: null,
|
|
8413
|
+
signalsLastPoll: 0
|
|
8414
|
+
};
|
|
8415
|
+
}
|
|
8416
|
+
return {
|
|
8417
|
+
state: deriveState(rec, now, thresholdMs),
|
|
8418
|
+
lastPollAt: rec.lastPollAt,
|
|
8419
|
+
lastOutcome: rec.lastOutcome,
|
|
8420
|
+
lastError: rec.lastError,
|
|
8421
|
+
signalsLastPoll: rec.signalsLastPoll
|
|
8422
|
+
};
|
|
8423
|
+
}
|
|
8424
|
+
|
|
7869
8425
|
// src/api.ts
|
|
7870
8426
|
function serializeGraph(graph) {
|
|
7871
8427
|
const nodes = [];
|
|
@@ -8086,6 +8642,26 @@ function registerRoutes(scope, ctx) {
|
|
|
8086
8642
|
const sliced = filtered.slice(0, safeLimit);
|
|
8087
8643
|
return { count: sliced.length, total, logs: sliced };
|
|
8088
8644
|
});
|
|
8645
|
+
scope.get("/connectors", async (req, reply) => {
|
|
8646
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8647
|
+
if (!proj) return;
|
|
8648
|
+
let entries;
|
|
8649
|
+
try {
|
|
8650
|
+
entries = (await readConnectorsConfig(ctx.connectorsHome)).connectors;
|
|
8651
|
+
} catch (err) {
|
|
8652
|
+
return reply.code(500).send({
|
|
8653
|
+
error: "connectors.json failed to read",
|
|
8654
|
+
details: err.message
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
const connectors = entries.filter((entry) => connectorMatchesProject(entry, proj.name)).map((entry) => ({
|
|
8658
|
+
id: entry.id,
|
|
8659
|
+
provider: entry.provider,
|
|
8660
|
+
credentialRef: redactCredentialRef(entry.credential),
|
|
8661
|
+
status: getConnectorStatus(entry.id)
|
|
8662
|
+
}));
|
|
8663
|
+
return { connectors };
|
|
8664
|
+
});
|
|
8089
8665
|
const incidentHistoryHandler = async (req, reply) => {
|
|
8090
8666
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8091
8667
|
if (!proj) return;
|
|
@@ -8488,7 +9064,8 @@ async function buildApi(opts) {
|
|
|
8488
9064
|
staleEventsPathFor,
|
|
8489
9065
|
policyFilePathFor,
|
|
8490
9066
|
bootstrap: opts.bootstrap,
|
|
8491
|
-
singleProject: opts.singleProject?.name
|
|
9067
|
+
singleProject: opts.singleProject?.name,
|
|
9068
|
+
connectorsHome: opts.connectorsHome
|
|
8492
9069
|
};
|
|
8493
9070
|
app.get("/health", async () => {
|
|
8494
9071
|
const uptimeMs = Date.now() - startedAt;
|
|
@@ -8641,6 +9218,17 @@ export {
|
|
|
8641
9218
|
touchLastSeen,
|
|
8642
9219
|
removeProject,
|
|
8643
9220
|
pruneRegistry,
|
|
9221
|
+
EnvRefUnsetError,
|
|
9222
|
+
readConnectorsConfig,
|
|
9223
|
+
resolveCredential,
|
|
9224
|
+
connectorMatchesProject,
|
|
9225
|
+
isEnvRef,
|
|
9226
|
+
upsertConnectorEntry,
|
|
9227
|
+
removeConnectorEntry,
|
|
9228
|
+
autoSlugConnectorId,
|
|
9229
|
+
describeCredential,
|
|
9230
|
+
sanitizePollError,
|
|
9231
|
+
recordConnectorPoll,
|
|
8644
9232
|
buildApi
|
|
8645
9233
|
};
|
|
8646
|
-
//# sourceMappingURL=chunk-
|
|
9234
|
+
//# sourceMappingURL=chunk-N7HEYLOF.js.map
|