@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.
@@ -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, path43, edges) {
460
- if (path43.length > best.path.length) {
461
- best = { path: [...path43], edges: [...edges] };
459
+ function step(node, path47, edges) {
460
+ if (path47.length > best.path.length) {
461
+ best = { path: [...path47], edges: [...edges] };
462
462
  }
463
- if (path43.length - 1 >= maxDepth) return;
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
- path43.push(srcId);
468
+ path47.push(srcId);
469
469
  edges.push(edge);
470
- step(srcId, path43, edges);
471
- path43.pop();
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 path43 = [originServiceId];
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
- path43.push(hop.nextService);
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: path43, edges, culprit: current };
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 path43 = [...chain.path];
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
- path43.push(loc.fileNode);
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: path43,
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: path43,
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 Provenance12, confidenceForExtracted as confidenceForExtracted10 } from "@neat.is/types";
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: Provenance12.EXTRACTED,
5900
- confidence: confidenceForExtracted10("structural"),
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 Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
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: Provenance13.EXTRACTED,
5980
- confidence: confidenceForExtracted11("structural"),
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: Provenance13.EXTRACTED,
6003
- confidence: confidenceForExtracted11("structural"),
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 Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
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: Provenance14.EXTRACTED,
6106
- confidence: confidenceForExtracted12("structural"),
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 Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
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 lineContaining(raw, needle) {
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: Provenance15.EXTRACTED,
6225
- confidence: confidenceForExtracted13("structural"),
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: Provenance15.EXTRACTED,
6294
- confidence: confidenceForExtracted13("structural"),
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
- lineContaining(raw, route)
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
- lineContaining(raw, name)
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
- lineContaining(raw, cron)
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
- lineContaining(raw, varName)
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: Provenance15.EXTRACTED,
6380
- confidence: confidenceForExtracted13("structural"),
6381
- evidence: { file: evidenceFile, line: lineContaining(raw, svc.service) }
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
- lineContaining(raw, svc.service)
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 path37 from "path";
6648
+ import path40 from "path";
6419
6649
 
6420
6650
  // src/extract/retire.ts
6421
6651
  import { existsSync as existsSync2 } from "fs";
6422
- import path36 from "path";
6423
- import { NodeType as NodeType14, Provenance as Provenance16 } from "@neat.is/types";
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 !== Provenance16.EXTRACTED) return;
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 !== Provenance16.EXTRACTED) return;
6683
+ if (edge.provenance !== Provenance17.EXTRACTED) return;
6454
6684
  const evidenceFile = edge.evidence?.file;
6455
6685
  if (!evidenceFile) return;
6456
- if (path36.isAbsolute(evidenceFile)) {
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(path36.join(base, evidenceFile)));
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 = path37.join(path37.dirname(opts.errorsPath), "rejected.ndjson");
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 EdgeType17,
6768
+ EdgeType as EdgeType20,
6539
6769
  NodeType as NodeType15,
6540
6770
  parseEdgeId,
6541
- Provenance as Provenance17
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 Provenance17.EXTRACTED:
6785
+ case Provenance18.EXTRACTED:
6556
6786
  cur.extracted = e;
6557
6787
  break;
6558
- case Provenance17.OBSERVED:
6788
+ case Provenance18.OBSERVED:
6559
6789
  cur.observed = e;
6560
6790
  break;
6561
- case Provenance17.INFERRED:
6791
+ case Provenance18.INFERRED:
6562
6792
  cur.inferred = e;
6563
6793
  break;
6564
6794
  default:
6565
- if (e.provenance === Provenance17.STALE) cur.stale = e;
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
- EdgeType17.CALLS,
6600
- EdgeType17.CONNECTS_TO,
6601
- EdgeType17.PUBLISHES_TO,
6602
- EdgeType17.CONSUMES_FROM
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 === EdgeType17.CONTAINS) return out;
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 === EdgeType17.CONFIGURED_BY && e.provenance === Provenance17.EXTRACTED) {
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 !== EdgeType17.CONNECTS_TO) continue;
6661
- if (edge.provenance !== Provenance17.OBSERVED) continue;
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 !== EdgeType17.CONNECTS_TO) continue;
6686
- if (edge.provenance !== Provenance17.OBSERVED) continue;
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 fs20 } from "fs";
6809
- import path38 from "path";
6810
- import { Provenance as Provenance18, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
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 = Provenance18.OBSERVED;
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 fs20.mkdir(path38.dirname(filePath), { recursive: true });
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 fs20.writeFile(tmp, JSON.stringify(payload), "utf8");
6857
- await fs20.rename(tmp, outPath);
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 fs20.readFile(outPath, "utf8");
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 fs21 } from "fs";
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 fs21.readFile(target, "utf8");
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 path39 from "path";
7232
+ import path42 from "path";
7003
7233
  function pathsForProject(project, baseDir) {
7004
7234
  if (project === DEFAULT_PROJECT) {
7005
7235
  return {
7006
- snapshotPath: path39.join(baseDir, "graph.json"),
7007
- errorsPath: path39.join(baseDir, "errors.ndjson"),
7008
- staleEventsPath: path39.join(baseDir, "stale-events.ndjson"),
7009
- embeddingsCachePath: path39.join(baseDir, "embeddings.json"),
7010
- policyViolationsPath: path39.join(baseDir, "policy-violations.ndjson")
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: path39.join(baseDir, `${project}.json`),
7015
- errorsPath: path39.join(baseDir, `errors.${project}.ndjson`),
7016
- staleEventsPath: path39.join(baseDir, `stale-events.${project}.ndjson`),
7017
- embeddingsCachePath: path39.join(baseDir, `embeddings.${project}.json`),
7018
- policyViolationsPath: path39.join(baseDir, `policy-violations.${project}.ndjson`)
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 fs22 } from "fs";
7287
+ import { promises as fs25 } from "fs";
7058
7288
  import os2 from "os";
7059
- import path40 from "path";
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 path40.resolve(override);
7068
- return path40.join(os2.homedir(), ".neat");
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 path40.join(neatHome(), "projects.json");
7301
+ return path43.join(neatHome(), "projects.json");
7072
7302
  }
7073
7303
  function registryLockPath() {
7074
- return path40.join(neatHome(), "projects.json.lock");
7304
+ return path43.join(neatHome(), "projects.json.lock");
7075
7305
  }
7076
7306
  function daemonPidPath() {
7077
- return path40.join(neatHome(), "neatd.pid");
7307
+ return path43.join(neatHome(), "neatd.pid");
7078
7308
  }
7079
7309
  function daemonsDir() {
7080
- return path40.join(neatHome(), "daemons");
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 fs22.readdir(dir);
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 = path40.join(dir, name);
7351
+ const file = path43.join(dir, name);
7122
7352
  let raw;
7123
7353
  try {
7124
- raw = await fs22.readFile(file, "utf8");
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 fs22.unlink(source).catch(() => {
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 fs22.readFile(file, "utf8");
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 = path40.resolve(input);
7472
+ const resolved = path43.resolve(input);
7243
7473
  try {
7244
- return await fs22.realpath(resolved);
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 fs22.mkdir(path40.dirname(target), { recursive: true });
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 fs22.open(tmp, "w");
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 fs22.rename(tmp, target);
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 fs22.mkdir(path40.dirname(lockPath), { recursive: true });
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 fs22.open(lockPath, "wx");
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 fs22.unlink(lockPath).catch(() => {
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 fs22.readFile(file, "utf8");
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 fs22.stat(p);
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 fs24 } from "fs";
7459
- import path42 from "path";
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 fs23 } from "fs";
7465
- import path41 from "path";
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 fs23.access(p);
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 = path41.resolve(serviceDir);
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 = path41.join(dir, candidate.lockfile);
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 = path41.dirname(dir);
7728
+ const parent = path44.dirname(dir);
7499
7729
  if (parent === dir) break;
7500
7730
  dir = parent;
7501
7731
  }
7502
- return { pm: "npm", cwd: path41.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
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 fs24.access(p);
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 = path42.join(scanPath, "package.json");
7552
- const raw = await fs24.readFile(pkgPath, "utf8");
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 fs24.readdir(scanPath);
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 ?? path42.join(os3.homedir(), ".neat", "extend-log.ndjson");
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 fs24.mkdir(path42.dirname(logPath), { recursive: true });
7567
- await fs24.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
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(path42.join(ctx.scanPath, ".env.neat"));
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 fs24.readFile(path42.join(ctx.scanPath, file), "utf8");
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 = path42.join(ctx.scanPath, primaryFile);
7883
+ const primaryPath = path45.join(ctx.scanPath, primaryFile);
7654
7884
  const filesTouched = [];
7655
7885
  const depsAdded = [];
7656
- const pkgPath = path42.join(ctx.scanPath, "package.json");
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 fs24.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
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 fs24.readFile(primaryPath, "utf8");
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 fs24.writeFile(primaryPath, patched, "utf8");
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 fs24.readFile(path42.join(ctx.scanPath, file), "utf8");
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 fs24.readFile(path42.join(ctx.scanPath, primaryFile), "utf8");
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 fs24.readFile(logPath, "utf8");
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 = path42.join(ctx.scanPath, "package.json");
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 fs24.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
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 = path42.join(ctx.scanPath, file);
7757
- const content = await fs24.readFile(filePath, "utf8");
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 fs24.writeFile(filePath, filtered, "utf8");
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-5T6J3WKC.js.map
9234
+ //# sourceMappingURL=chunk-N7HEYLOF.js.map