@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/index.cjs CHANGED
@@ -60,8 +60,8 @@ function mountBearerAuth(app, opts) {
60
60
  ]);
61
61
  const publicRead = opts.publicRead === true;
62
62
  app.addHook("preHandler", (req, reply, done) => {
63
- const path48 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
64
- if (exactUnauthPaths.has(path48) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path48)) {
63
+ const path51 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
64
+ if (exactUnauthPaths.has(path51) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path51)) {
65
65
  done();
66
66
  return;
67
67
  }
@@ -193,8 +193,8 @@ function reshapeGrpcRequest(req) {
193
193
  };
194
194
  }
195
195
  function resolveProtoRoot() {
196
- const here = import_node_path43.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
197
- return import_node_path43.default.resolve(here, "..", "proto");
196
+ const here = import_node_path47.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
197
+ return import_node_path47.default.resolve(here, "..", "proto");
198
198
  }
199
199
  function loadTraceService() {
200
200
  const protoRoot = resolveProtoRoot();
@@ -262,13 +262,13 @@ async function startOtelGrpcReceiver(opts) {
262
262
  })
263
263
  };
264
264
  }
265
- var import_node_url, import_node_path43, import_node_crypto2, grpc, protoLoader;
265
+ var import_node_url, import_node_path47, import_node_crypto2, grpc, protoLoader;
266
266
  var init_otel_grpc = __esm({
267
267
  "src/otel-grpc.ts"() {
268
268
  "use strict";
269
269
  init_cjs_shims();
270
270
  import_node_url = require("url");
271
- import_node_path43 = __toESM(require("path"), 1);
271
+ import_node_path47 = __toESM(require("path"), 1);
272
272
  import_node_crypto2 = require("crypto");
273
273
  grpc = __toESM(require("@grpc/grpc-js"), 1);
274
274
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -361,8 +361,8 @@ function websocketChannelPathOf(attrs) {
361
361
  const v = attrs[key];
362
362
  if (typeof v === "string" && v.length > 0) {
363
363
  const q = v.indexOf("?");
364
- const path48 = q === -1 ? v : v.slice(0, q);
365
- if (path48.length > 0) return path48;
364
+ const path51 = q === -1 ? v : v.slice(0, q);
365
+ if (path51.length > 0) return path51;
366
366
  }
367
367
  }
368
368
  return void 0;
@@ -416,10 +416,10 @@ function parseOtlpRequest(body) {
416
416
  return out;
417
417
  }
418
418
  function loadProtoRoot() {
419
- const here = import_node_path44.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
420
- const protoRoot = import_node_path44.default.resolve(here, "..", "proto");
419
+ const here = import_node_path48.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
420
+ const protoRoot = import_node_path48.default.resolve(here, "..", "proto");
421
421
  const root = new import_protobufjs.default.Root();
422
- root.resolvePath = (_origin, target) => import_node_path44.default.resolve(protoRoot, target);
422
+ root.resolvePath = (_origin, target) => import_node_path48.default.resolve(protoRoot, target);
423
423
  root.loadSync(
424
424
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
425
425
  { keepCase: true }
@@ -655,12 +655,12 @@ function logSpanHandler(span) {
655
655
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
656
656
  );
657
657
  }
658
- var import_node_path44, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
658
+ var import_node_path48, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
659
659
  var init_otel = __esm({
660
660
  "src/otel.ts"() {
661
661
  "use strict";
662
662
  init_cjs_shims();
663
- import_node_path44 = __toESM(require("path"), 1);
663
+ import_node_path48 = __toESM(require("path"), 1);
664
664
  import_node_url2 = require("url");
665
665
  import_fastify2 = __toESM(require("fastify"), 1);
666
666
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1250,19 +1250,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1250
1250
  function longestIncomingWalk(graph, start, maxDepth) {
1251
1251
  let best = { path: [start], edges: [] };
1252
1252
  const visited = /* @__PURE__ */ new Set([start]);
1253
- function step(node, path48, edges) {
1254
- if (path48.length > best.path.length) {
1255
- best = { path: [...path48], edges: [...edges] };
1253
+ function step(node, path51, edges) {
1254
+ if (path51.length > best.path.length) {
1255
+ best = { path: [...path51], edges: [...edges] };
1256
1256
  }
1257
- if (path48.length - 1 >= maxDepth) return;
1257
+ if (path51.length - 1 >= maxDepth) return;
1258
1258
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1259
1259
  for (const [srcId, edge] of incoming) {
1260
1260
  if (visited.has(srcId)) continue;
1261
1261
  visited.add(srcId);
1262
- path48.push(srcId);
1262
+ path51.push(srcId);
1263
1263
  edges.push(edge);
1264
- step(srcId, path48, edges);
1265
- path48.pop();
1264
+ step(srcId, path51, edges);
1265
+ path51.pop();
1266
1266
  edges.pop();
1267
1267
  visited.delete(srcId);
1268
1268
  }
@@ -1456,26 +1456,26 @@ function dominantFailingCall(graph, serviceId5, visited) {
1456
1456
  return best;
1457
1457
  }
1458
1458
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1459
- const path48 = [originServiceId];
1459
+ const path51 = [originServiceId];
1460
1460
  const edges = [];
1461
1461
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1462
1462
  let current = originServiceId;
1463
1463
  for (let depth = 0; depth < maxDepth; depth++) {
1464
1464
  const hop = dominantFailingCall(graph, current, visited);
1465
1465
  if (!hop) break;
1466
- path48.push(hop.nextService);
1466
+ path51.push(hop.nextService);
1467
1467
  edges.push(hop.edge);
1468
1468
  visited.add(hop.nextService);
1469
1469
  current = hop.nextService;
1470
1470
  }
1471
1471
  if (edges.length === 0) return null;
1472
- return { path: path48, edges, culprit: current };
1472
+ return { path: path51, edges, culprit: current };
1473
1473
  }
1474
1474
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1475
1475
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1476
1476
  if (!chain) return null;
1477
1477
  const culprit = chain.culprit;
1478
- const path48 = [...chain.path];
1478
+ const path51 = [...chain.path];
1479
1479
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1480
1480
  const baseConfidence = confidenceFromMix(chain.edges);
1481
1481
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1483,14 +1483,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1483
1483
  if (loc) {
1484
1484
  let rootCauseNode = culprit;
1485
1485
  if (loc.fileNode) {
1486
- path48.push(loc.fileNode);
1486
+ path51.push(loc.fileNode);
1487
1487
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1488
1488
  rootCauseNode = loc.fileNode;
1489
1489
  }
1490
1490
  return import_types.RootCauseResultSchema.parse({
1491
1491
  rootCauseNode,
1492
1492
  rootCauseReason: loc.rootCauseReason,
1493
- traversalPath: path48,
1493
+ traversalPath: path51,
1494
1494
  edgeProvenances,
1495
1495
  confidence,
1496
1496
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1502,7 +1502,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1502
1502
  return import_types.RootCauseResultSchema.parse({
1503
1503
  rootCauseNode: culprit,
1504
1504
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1505
- traversalPath: path48,
1505
+ traversalPath: path51,
1506
1506
  edgeProvenances,
1507
1507
  confidence,
1508
1508
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -6498,6 +6498,39 @@ function classifyImage(image) {
6498
6498
  if (last.startsWith("memcached")) return "memcached";
6499
6499
  return "container";
6500
6500
  }
6501
+ function lineContaining(raw, needle) {
6502
+ if (!needle) return void 0;
6503
+ const idx = raw.indexOf(needle);
6504
+ if (idx === -1) return void 0;
6505
+ let line = 1;
6506
+ for (let i = 0; i < idx; i++) if (raw[i] === "\n") line++;
6507
+ return line;
6508
+ }
6509
+ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provider, evidenceFile, line) {
6510
+ let nodesAdded = 0;
6511
+ let edgesAdded = 0;
6512
+ const node = makeInfraNode(kind, name, provider);
6513
+ if (!graph.hasNode(node.id)) {
6514
+ graph.addNode(node.id, node);
6515
+ nodesAdded++;
6516
+ }
6517
+ if (node.id === anchorId) return { nodesAdded, edgesAdded };
6518
+ const edgeId = (0, import_types4.extractedEdgeId)(anchorId, node.id, edgeType);
6519
+ if (!graph.hasEdge(edgeId)) {
6520
+ const edge = {
6521
+ id: edgeId,
6522
+ source: anchorId,
6523
+ target: node.id,
6524
+ type: edgeType,
6525
+ provenance: import_types21.Provenance.EXTRACTED,
6526
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
6527
+ evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
6528
+ };
6529
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
6530
+ edgesAdded++;
6531
+ }
6532
+ return { nodesAdded, edgesAdded };
6533
+ }
6501
6534
 
6502
6535
  // src/extract/infra/docker-compose.ts
6503
6536
  function dependsOnList(value) {
@@ -6857,7 +6890,7 @@ async function readWranglerConfig(dir) {
6857
6890
  }
6858
6891
  return null;
6859
6892
  }
6860
- function lineContaining(raw, needle) {
6893
+ function lineContaining2(raw, needle) {
6861
6894
  if (!needle) return void 0;
6862
6895
  const idx = raw.indexOf(needle);
6863
6896
  if (idx === -1) return void 0;
@@ -6981,7 +7014,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6981
7014
  "cloudflare-route",
6982
7015
  route,
6983
7016
  evidenceFile,
6984
- lineContaining(raw, route)
7017
+ lineContaining2(raw, route)
6985
7018
  );
6986
7019
  nodesAdded += result.nodesAdded;
6987
7020
  edgesAdded += result.edgesAdded;
@@ -7005,7 +7038,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
7005
7038
  group.kind,
7006
7039
  name,
7007
7040
  evidenceFile,
7008
- lineContaining(raw, name)
7041
+ lineContaining2(raw, name)
7009
7042
  );
7010
7043
  nodesAdded += result.nodesAdded;
7011
7044
  edgesAdded += result.edgesAdded;
@@ -7019,7 +7052,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
7019
7052
  "cloudflare-cron",
7020
7053
  cron,
7021
7054
  evidenceFile,
7022
- lineContaining(raw, cron)
7055
+ lineContaining2(raw, cron)
7023
7056
  );
7024
7057
  nodesAdded += result.nodesAdded;
7025
7058
  edgesAdded += result.edgesAdded;
@@ -7032,7 +7065,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
7032
7065
  "cloudflare-env-var",
7033
7066
  varName,
7034
7067
  evidenceFile,
7035
- lineContaining(raw, varName)
7068
+ lineContaining2(raw, varName)
7036
7069
  );
7037
7070
  nodesAdded += result.nodesAdded;
7038
7071
  edgesAdded += result.edgesAdded;
@@ -7050,7 +7083,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
7050
7083
  type: import_types25.EdgeType.CALLS,
7051
7084
  provenance: import_types25.Provenance.EXTRACTED,
7052
7085
  confidence: (0, import_types25.confidenceForExtracted)("structural"),
7053
- evidence: { file: evidenceFile, line: lineContaining(raw, svc.service) }
7086
+ evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
7054
7087
  };
7055
7088
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
7056
7089
  edgesAdded++;
@@ -7064,11 +7097,208 @@ async function addCloudflareWorkers(graph, services, scanPath) {
7064
7097
  "cloudflare-service-binding",
7065
7098
  svc.service,
7066
7099
  evidenceFile,
7067
- lineContaining(raw, svc.service)
7100
+ lineContaining2(raw, svc.service)
7101
+ );
7102
+ nodesAdded += result.nodesAdded;
7103
+ edgesAdded += result.edgesAdded;
7104
+ }
7105
+ }
7106
+ return { nodesAdded, edgesAdded };
7107
+ }
7108
+
7109
+ // src/extract/infra/vercel.ts
7110
+ init_cjs_shims();
7111
+ var import_node_fs20 = require("fs");
7112
+ var import_node_path36 = __toESM(require("path"), 1);
7113
+ var import_types26 = require("@neat.is/types");
7114
+ var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
7115
+ async function readVercelConfig(dir) {
7116
+ for (const filename of VERCEL_CONFIG_FILENAMES) {
7117
+ const abs = import_node_path36.default.join(dir, filename);
7118
+ if (!await exists(abs)) continue;
7119
+ const raw = await import_node_fs20.promises.readFile(abs, "utf8");
7120
+ const config = JSON.parse(maskCommentsInSource(raw));
7121
+ return { config, relFile: filename, raw };
7122
+ }
7123
+ return null;
7124
+ }
7125
+ async function readLinkedProjectName(dir) {
7126
+ const abs = import_node_path36.default.join(dir, ".vercel", "project.json");
7127
+ if (!await exists(abs)) return void 0;
7128
+ const parsed = JSON.parse(await import_node_fs20.promises.readFile(abs, "utf8"));
7129
+ return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
7130
+ }
7131
+ function routeSource(route) {
7132
+ return route.source ?? route.src;
7133
+ }
7134
+ async function addVercelServices(graph, services, scanPath) {
7135
+ let nodesAdded = 0;
7136
+ let edgesAdded = 0;
7137
+ for (const service of services) {
7138
+ let read = null;
7139
+ let projectName;
7140
+ try {
7141
+ read = await readVercelConfig(service.dir);
7142
+ projectName = await readLinkedProjectName(service.dir);
7143
+ } catch (err) {
7144
+ recordExtractionError("infra vercel", import_node_path36.default.relative(scanPath, service.dir), err);
7145
+ continue;
7146
+ }
7147
+ if (!read && !projectName) continue;
7148
+ const serviceNode = graph.getNodeAttributes(service.node.id);
7149
+ if (serviceNode.platform !== "vercel" || projectName && serviceNode.platformName !== projectName) {
7150
+ const updated = {
7151
+ ...serviceNode,
7152
+ platform: "vercel",
7153
+ ...projectName ? { platformName: projectName } : {}
7154
+ };
7155
+ graph.replaceNodeAttributes(service.node.id, updated);
7156
+ }
7157
+ const anchorId = service.node.id;
7158
+ if (!read) continue;
7159
+ const { config, relFile, raw } = read;
7160
+ const evidenceFile = toPosix2(import_node_path36.default.relative(scanPath, import_node_path36.default.join(service.dir, relFile)));
7161
+ const add = (edgeType, kind, name) => {
7162
+ if (!name) return;
7163
+ const result = emitPlatformResourceEdge(
7164
+ graph,
7165
+ anchorId,
7166
+ edgeType,
7167
+ kind,
7168
+ name,
7169
+ "vercel",
7170
+ evidenceFile,
7171
+ lineContaining(raw, name)
7172
+ );
7173
+ nodesAdded += result.nodesAdded;
7174
+ edgesAdded += result.edgesAdded;
7175
+ };
7176
+ add(import_types26.EdgeType.RUNS_ON, "vercel", "vercel");
7177
+ for (const cron of config.crons ?? []) add(import_types26.EdgeType.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
7178
+ for (const varName of Object.keys(config.env ?? {})) add(import_types26.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
7179
+ for (const varName of Object.keys(config.build?.env ?? {})) add(import_types26.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
7180
+ for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
7181
+ add(import_types26.EdgeType.CONNECTS_TO, "vercel-route", routeSource(route));
7182
+ }
7183
+ }
7184
+ return { nodesAdded, edgesAdded };
7185
+ }
7186
+
7187
+ // src/extract/infra/railway.ts
7188
+ init_cjs_shims();
7189
+ var import_node_fs21 = require("fs");
7190
+ var import_node_path37 = __toESM(require("path"), 1);
7191
+ var import_smol_toml3 = require("smol-toml");
7192
+ var import_types27 = require("@neat.is/types");
7193
+ var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
7194
+ async function readRailwayConfig(dir) {
7195
+ for (const filename of RAILWAY_FILENAMES) {
7196
+ const abs = import_node_path37.default.join(dir, filename);
7197
+ if (!await exists(abs)) continue;
7198
+ const raw = await import_node_fs21.promises.readFile(abs, "utf8");
7199
+ const config = filename === "railway.toml" ? (0, import_smol_toml3.parse)(raw) : JSON.parse(maskCommentsInSource(raw));
7200
+ return { config, relFile: filename, raw };
7201
+ }
7202
+ return null;
7203
+ }
7204
+ async function addRailwayServices(graph, services, scanPath) {
7205
+ let nodesAdded = 0;
7206
+ let edgesAdded = 0;
7207
+ for (const service of services) {
7208
+ let read = null;
7209
+ try {
7210
+ read = await readRailwayConfig(service.dir);
7211
+ } catch (err) {
7212
+ recordExtractionError("infra railway", import_node_path37.default.relative(scanPath, service.dir), err);
7213
+ continue;
7214
+ }
7215
+ if (!read) continue;
7216
+ const serviceNode = graph.getNodeAttributes(service.node.id);
7217
+ if (serviceNode.platform !== "railway") {
7218
+ graph.replaceNodeAttributes(service.node.id, { ...serviceNode, platform: "railway" });
7219
+ }
7220
+ const anchorId = service.node.id;
7221
+ const { config, relFile, raw } = read;
7222
+ const evidenceFile = toPosix2(import_node_path37.default.relative(scanPath, import_node_path37.default.join(service.dir, relFile)));
7223
+ const add = (edgeType, kind, name) => {
7224
+ if (!name) return;
7225
+ const result = emitPlatformResourceEdge(
7226
+ graph,
7227
+ anchorId,
7228
+ edgeType,
7229
+ kind,
7230
+ name,
7231
+ "railway",
7232
+ evidenceFile,
7233
+ lineContaining(raw, name)
7068
7234
  );
7069
7235
  nodesAdded += result.nodesAdded;
7070
7236
  edgesAdded += result.edgesAdded;
7237
+ };
7238
+ add(import_types27.EdgeType.RUNS_ON, "railway", "railway");
7239
+ add(import_types27.EdgeType.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
7240
+ add(import_types27.EdgeType.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
7241
+ }
7242
+ return { nodesAdded, edgesAdded };
7243
+ }
7244
+
7245
+ // src/extract/infra/supabase.ts
7246
+ init_cjs_shims();
7247
+ var import_node_fs22 = require("fs");
7248
+ var import_node_path38 = __toESM(require("path"), 1);
7249
+ var import_smol_toml4 = require("smol-toml");
7250
+ var import_types28 = require("@neat.is/types");
7251
+ async function readSupabaseConfig(dir) {
7252
+ const relFile = import_node_path38.default.join("supabase", "config.toml");
7253
+ const abs = import_node_path38.default.join(dir, relFile);
7254
+ if (!await exists(abs)) return null;
7255
+ const raw = await import_node_fs22.promises.readFile(abs, "utf8");
7256
+ const config = (0, import_smol_toml4.parse)(raw);
7257
+ return { config, relFile, raw };
7258
+ }
7259
+ async function addSupabaseProjects(graph, services, scanPath) {
7260
+ let nodesAdded = 0;
7261
+ let edgesAdded = 0;
7262
+ for (const service of services) {
7263
+ let read = null;
7264
+ try {
7265
+ read = await readSupabaseConfig(service.dir);
7266
+ } catch (err) {
7267
+ recordExtractionError("infra supabase", import_node_path38.default.relative(scanPath, service.dir), err);
7268
+ continue;
7269
+ }
7270
+ if (!read) continue;
7271
+ const { config, relFile, raw } = read;
7272
+ const projectId = typeof config.project_id === "string" ? config.project_id : void 0;
7273
+ const serviceNode = graph.getNodeAttributes(service.node.id);
7274
+ if (serviceNode.platform !== "supabase" || projectId && serviceNode.platformName !== projectId) {
7275
+ graph.replaceNodeAttributes(service.node.id, {
7276
+ ...serviceNode,
7277
+ platform: "supabase",
7278
+ ...projectId ? { platformName: projectId } : {}
7279
+ });
7071
7280
  }
7281
+ const anchorId = service.node.id;
7282
+ const evidenceFile = toPosix2(import_node_path38.default.relative(scanPath, import_node_path38.default.join(service.dir, relFile)));
7283
+ const add = (edgeType, kind, name) => {
7284
+ if (!name) return;
7285
+ const result = emitPlatformResourceEdge(
7286
+ graph,
7287
+ anchorId,
7288
+ edgeType,
7289
+ kind,
7290
+ name,
7291
+ "supabase",
7292
+ evidenceFile,
7293
+ lineContaining(raw, name)
7294
+ );
7295
+ nodesAdded += result.nodesAdded;
7296
+ edgesAdded += result.edgesAdded;
7297
+ };
7298
+ add(import_types28.EdgeType.RUNS_ON, "supabase", "supabase");
7299
+ for (const fn of Object.keys(config.functions ?? {})) add(import_types28.EdgeType.DEPENDS_ON, "supabase-function", fn);
7300
+ if (config.storage) add(import_types28.EdgeType.DEPENDS_ON, "supabase-storage", "storage");
7301
+ if (config.auth) add(import_types28.EdgeType.DEPENDS_ON, "supabase-auth", "auth");
7072
7302
  }
7073
7303
  return { nodesAdded, edgesAdded };
7074
7304
  }
@@ -7080,24 +7310,27 @@ async function addInfra(graph, scanPath, services) {
7080
7310
  const terraform = await addTerraformResources(graph, scanPath);
7081
7311
  const k8s = await addK8sResources(graph, scanPath);
7082
7312
  const cloudflare = await addCloudflareWorkers(graph, services, scanPath);
7313
+ const vercel = await addVercelServices(graph, services, scanPath);
7314
+ const railway = await addRailwayServices(graph, services, scanPath);
7315
+ const supabase = await addSupabaseProjects(graph, services, scanPath);
7083
7316
  return {
7084
- nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded,
7085
- edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded
7317
+ nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded + vercel.nodesAdded + railway.nodesAdded + supabase.nodesAdded,
7318
+ edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded + vercel.edgesAdded + railway.edgesAdded + supabase.edgesAdded
7086
7319
  };
7087
7320
  }
7088
7321
 
7089
7322
  // src/extract/index.ts
7090
- var import_node_path37 = __toESM(require("path"), 1);
7323
+ var import_node_path40 = __toESM(require("path"), 1);
7091
7324
 
7092
7325
  // src/extract/retire.ts
7093
7326
  init_cjs_shims();
7094
- var import_node_fs20 = require("fs");
7095
- var import_node_path36 = __toESM(require("path"), 1);
7096
- var import_types26 = require("@neat.is/types");
7327
+ var import_node_fs23 = require("fs");
7328
+ var import_node_path39 = __toESM(require("path"), 1);
7329
+ var import_types29 = require("@neat.is/types");
7097
7330
  function dropOrphanedFileNodes(graph) {
7098
7331
  const orphans = [];
7099
7332
  graph.forEachNode((id, attrs) => {
7100
- if (attrs.type !== import_types26.NodeType.FileNode) return;
7333
+ if (attrs.type !== import_types29.NodeType.FileNode) return;
7101
7334
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
7102
7335
  orphans.push(id);
7103
7336
  }
@@ -7110,14 +7343,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
7110
7343
  const bases = [scanPath, ...serviceDirs];
7111
7344
  graph.forEachEdge((id, attrs) => {
7112
7345
  const edge = attrs;
7113
- if (edge.provenance !== import_types26.Provenance.EXTRACTED) return;
7346
+ if (edge.provenance !== import_types29.Provenance.EXTRACTED) return;
7114
7347
  const evidenceFile = edge.evidence?.file;
7115
7348
  if (!evidenceFile) return;
7116
- if (import_node_path36.default.isAbsolute(evidenceFile)) {
7117
- if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
7349
+ if (import_node_path39.default.isAbsolute(evidenceFile)) {
7350
+ if (!(0, import_node_fs23.existsSync)(evidenceFile)) toDrop.push(id);
7118
7351
  return;
7119
7352
  }
7120
- const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path36.default.join(base, evidenceFile)));
7353
+ const found = bases.some((base) => (0, import_node_fs23.existsSync)(import_node_path39.default.join(base, evidenceFile)));
7121
7354
  if (!found) toDrop.push(id);
7122
7355
  });
7123
7356
  for (const id of toDrop) graph.dropEdge(id);
@@ -7159,7 +7392,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
7159
7392
  }
7160
7393
  const droppedEntries = drainDroppedExtracted();
7161
7394
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
7162
- const rejectedPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.errorsPath), "rejected.ndjson");
7395
+ const rejectedPath = import_node_path40.default.join(import_node_path40.default.dirname(opts.errorsPath), "rejected.ndjson");
7163
7396
  try {
7164
7397
  await writeRejectedExtracted(droppedEntries, rejectedPath);
7165
7398
  } catch (err) {
@@ -7193,9 +7426,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
7193
7426
 
7194
7427
  // src/persist.ts
7195
7428
  init_cjs_shims();
7196
- var import_node_fs21 = require("fs");
7197
- var import_node_path38 = __toESM(require("path"), 1);
7198
- var import_types27 = require("@neat.is/types");
7429
+ var import_node_fs24 = require("fs");
7430
+ var import_node_path41 = __toESM(require("path"), 1);
7431
+ var import_types30 = require("@neat.is/types");
7199
7432
  var SCHEMA_VERSION = 4;
7200
7433
  function migrateV1ToV2(payload) {
7201
7434
  const nodes = payload.graph.nodes;
@@ -7217,12 +7450,12 @@ function migrateV2ToV3(payload) {
7217
7450
  for (const edge of edges) {
7218
7451
  const attrs = edge.attributes;
7219
7452
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
7220
- attrs.provenance = import_types27.Provenance.OBSERVED;
7453
+ attrs.provenance = import_types30.Provenance.OBSERVED;
7221
7454
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
7222
7455
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
7223
7456
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
7224
7457
  if (type && source && target) {
7225
- const newId = (0, import_types27.observedEdgeId)(source, target, type);
7458
+ const newId = (0, import_types30.observedEdgeId)(source, target, type);
7226
7459
  attrs.id = newId;
7227
7460
  if (edge.key) edge.key = newId;
7228
7461
  }
@@ -7231,7 +7464,7 @@ function migrateV2ToV3(payload) {
7231
7464
  return { ...payload, schemaVersion: 3 };
7232
7465
  }
7233
7466
  async function ensureDir(filePath) {
7234
- await import_node_fs21.promises.mkdir(import_node_path38.default.dirname(filePath), { recursive: true });
7467
+ await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(filePath), { recursive: true });
7235
7468
  }
7236
7469
  async function saveGraphToDisk(graph, outPath) {
7237
7470
  await ensureDir(outPath);
@@ -7241,13 +7474,13 @@ async function saveGraphToDisk(graph, outPath) {
7241
7474
  graph: graph.export()
7242
7475
  };
7243
7476
  const tmp = `${outPath}.tmp`;
7244
- await import_node_fs21.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
7245
- await import_node_fs21.promises.rename(tmp, outPath);
7477
+ await import_node_fs24.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
7478
+ await import_node_fs24.promises.rename(tmp, outPath);
7246
7479
  }
7247
7480
  async function loadGraphFromDisk(graph, outPath) {
7248
7481
  let raw;
7249
7482
  try {
7250
- raw = await import_node_fs21.promises.readFile(outPath, "utf8");
7483
+ raw = await import_node_fs24.promises.readFile(outPath, "utf8");
7251
7484
  } catch (err) {
7252
7485
  if (err.code === "ENOENT") return;
7253
7486
  throw err;
@@ -7314,19 +7547,19 @@ function startPersistLoop(graph, outPath, opts = {}) {
7314
7547
  init_cjs_shims();
7315
7548
  var import_fastify = __toESM(require("fastify"), 1);
7316
7549
  var import_cors = __toESM(require("@fastify/cors"), 1);
7317
- var import_types30 = require("@neat.is/types");
7550
+ var import_types33 = require("@neat.is/types");
7318
7551
 
7319
7552
  // src/extend/index.ts
7320
7553
  init_cjs_shims();
7321
- var import_node_fs23 = require("fs");
7322
- var import_node_path40 = __toESM(require("path"), 1);
7554
+ var import_node_fs26 = require("fs");
7555
+ var import_node_path43 = __toESM(require("path"), 1);
7323
7556
  var import_node_os2 = __toESM(require("os"), 1);
7324
7557
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
7325
7558
 
7326
7559
  // src/installers/package-manager.ts
7327
7560
  init_cjs_shims();
7328
- var import_node_fs22 = require("fs");
7329
- var import_node_path39 = __toESM(require("path"), 1);
7561
+ var import_node_fs25 = require("fs");
7562
+ var import_node_path42 = __toESM(require("path"), 1);
7330
7563
  var import_node_child_process = require("child_process");
7331
7564
  var LOCKFILE_PRIORITY = [
7332
7565
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -7341,29 +7574,29 @@ var LOCKFILE_PRIORITY = [
7341
7574
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
7342
7575
  async function exists2(p) {
7343
7576
  try {
7344
- await import_node_fs22.promises.access(p);
7577
+ await import_node_fs25.promises.access(p);
7345
7578
  return true;
7346
7579
  } catch {
7347
7580
  return false;
7348
7581
  }
7349
7582
  }
7350
7583
  async function detectPackageManager(serviceDir) {
7351
- let dir = import_node_path39.default.resolve(serviceDir);
7584
+ let dir = import_node_path42.default.resolve(serviceDir);
7352
7585
  const stops = /* @__PURE__ */ new Set();
7353
7586
  for (let i = 0; i < 64; i++) {
7354
7587
  if (stops.has(dir)) break;
7355
7588
  stops.add(dir);
7356
7589
  for (const candidate of LOCKFILE_PRIORITY) {
7357
- const lockPath = import_node_path39.default.join(dir, candidate.lockfile);
7590
+ const lockPath = import_node_path42.default.join(dir, candidate.lockfile);
7358
7591
  if (await exists2(lockPath)) {
7359
7592
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
7360
7593
  }
7361
7594
  }
7362
- const parent = import_node_path39.default.dirname(dir);
7595
+ const parent = import_node_path42.default.dirname(dir);
7363
7596
  if (parent === dir) break;
7364
7597
  dir = parent;
7365
7598
  }
7366
- return { pm: "npm", cwd: import_node_path39.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7599
+ return { pm: "npm", cwd: import_node_path42.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7367
7600
  }
7368
7601
  async function runPackageManagerInstall(cmd) {
7369
7602
  return new Promise((resolve) => {
@@ -7405,30 +7638,30 @@ ${err.message}`
7405
7638
  // src/extend/index.ts
7406
7639
  async function fileExists2(p) {
7407
7640
  try {
7408
- await import_node_fs23.promises.access(p);
7641
+ await import_node_fs26.promises.access(p);
7409
7642
  return true;
7410
7643
  } catch {
7411
7644
  return false;
7412
7645
  }
7413
7646
  }
7414
7647
  async function readPackageJson(scanPath) {
7415
- const pkgPath = import_node_path40.default.join(scanPath, "package.json");
7416
- const raw = await import_node_fs23.promises.readFile(pkgPath, "utf8");
7648
+ const pkgPath = import_node_path43.default.join(scanPath, "package.json");
7649
+ const raw = await import_node_fs26.promises.readFile(pkgPath, "utf8");
7417
7650
  return JSON.parse(raw);
7418
7651
  }
7419
7652
  async function findHookFiles(scanPath) {
7420
- const entries = await import_node_fs23.promises.readdir(scanPath);
7653
+ const entries = await import_node_fs26.promises.readdir(scanPath);
7421
7654
  return entries.filter(
7422
7655
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
7423
7656
  ).sort();
7424
7657
  }
7425
7658
  function extendLogPath() {
7426
- return process.env.NEAT_EXTEND_LOG ?? import_node_path40.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7659
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path43.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7427
7660
  }
7428
7661
  async function appendExtendLog(entry) {
7429
7662
  const logPath = extendLogPath();
7430
- await import_node_fs23.promises.mkdir(import_node_path40.default.dirname(logPath), { recursive: true });
7431
- await import_node_fs23.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
7663
+ await import_node_fs26.promises.mkdir(import_node_path43.default.dirname(logPath), { recursive: true });
7664
+ await import_node_fs26.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
7432
7665
  }
7433
7666
  function splicedContent(fileContent, snippet2) {
7434
7667
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -7486,7 +7719,7 @@ function lookupInstrumentation(library, installedVersion) {
7486
7719
  }
7487
7720
  async function describeProjectInstrumentation(ctx) {
7488
7721
  const hookFiles = await findHookFiles(ctx.scanPath);
7489
- const envNeat = await fileExists2(import_node_path40.default.join(ctx.scanPath, ".env.neat"));
7722
+ const envNeat = await fileExists2(import_node_path43.default.join(ctx.scanPath, ".env.neat"));
7490
7723
  const registryInstrPackages = new Set(
7491
7724
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
7492
7725
  );
@@ -7508,31 +7741,31 @@ async function applyExtension(ctx, args, options) {
7508
7741
  );
7509
7742
  }
7510
7743
  for (const file of hookFiles) {
7511
- const content = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7744
+ const content = await import_node_fs26.promises.readFile(import_node_path43.default.join(ctx.scanPath, file), "utf8");
7512
7745
  if (content.includes(args.registration_snippet)) {
7513
7746
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
7514
7747
  }
7515
7748
  }
7516
7749
  const primaryFile = hookFiles[0];
7517
- const primaryPath = import_node_path40.default.join(ctx.scanPath, primaryFile);
7750
+ const primaryPath = import_node_path43.default.join(ctx.scanPath, primaryFile);
7518
7751
  const filesTouched = [];
7519
7752
  const depsAdded = [];
7520
- const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7753
+ const pkgPath = import_node_path43.default.join(ctx.scanPath, "package.json");
7521
7754
  const pkg = await readPackageJson(ctx.scanPath);
7522
7755
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
7523
7756
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
7524
- await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7757
+ await import_node_fs26.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7525
7758
  filesTouched.push("package.json");
7526
7759
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
7527
7760
  }
7528
- const hookContent = await import_node_fs23.promises.readFile(primaryPath, "utf8");
7761
+ const hookContent = await import_node_fs26.promises.readFile(primaryPath, "utf8");
7529
7762
  const patched = splicedContent(hookContent, args.registration_snippet);
7530
7763
  if (!patched) {
7531
7764
  throw new Error(
7532
7765
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
7533
7766
  );
7534
7767
  }
7535
- await import_node_fs23.promises.writeFile(primaryPath, patched, "utf8");
7768
+ await import_node_fs26.promises.writeFile(primaryPath, patched, "utf8");
7536
7769
  filesTouched.push(primaryFile);
7537
7770
  const cmd = await detectPackageManager(ctx.scanPath);
7538
7771
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -7563,7 +7796,7 @@ async function dryRunExtension(ctx, args) {
7563
7796
  };
7564
7797
  }
7565
7798
  for (const file of hookFiles) {
7566
- const content = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7799
+ const content = await import_node_fs26.promises.readFile(import_node_path43.default.join(ctx.scanPath, file), "utf8");
7567
7800
  if (content.includes(args.registration_snippet)) {
7568
7801
  return {
7569
7802
  library: args.library,
@@ -7585,7 +7818,7 @@ async function dryRunExtension(ctx, args) {
7585
7818
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
7586
7819
  filesTouched.push("package.json");
7587
7820
  }
7588
- const hookContent = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, primaryFile), "utf8");
7821
+ const hookContent = await import_node_fs26.promises.readFile(import_node_path43.default.join(ctx.scanPath, primaryFile), "utf8");
7589
7822
  const patched = splicedContent(hookContent, args.registration_snippet);
7590
7823
  if (patched) {
7591
7824
  filesTouched.push(primaryFile);
@@ -7600,28 +7833,28 @@ async function rollbackExtension(ctx, args) {
7600
7833
  if (!await fileExists2(logPath)) {
7601
7834
  return { undone: false, message: "no apply found for library" };
7602
7835
  }
7603
- const raw = await import_node_fs23.promises.readFile(logPath, "utf8");
7836
+ const raw = await import_node_fs26.promises.readFile(logPath, "utf8");
7604
7837
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
7605
7838
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
7606
7839
  if (!match) {
7607
7840
  return { undone: false, message: "no apply found for library" };
7608
7841
  }
7609
- const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7842
+ const pkgPath = import_node_path43.default.join(ctx.scanPath, "package.json");
7610
7843
  if (await fileExists2(pkgPath)) {
7611
7844
  const pkg = await readPackageJson(ctx.scanPath);
7612
7845
  if (pkg.dependencies?.[match.instrumentation_package]) {
7613
7846
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
7614
7847
  pkg.dependencies = rest;
7615
- await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7848
+ await import_node_fs26.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7616
7849
  }
7617
7850
  }
7618
7851
  const hookFiles = await findHookFiles(ctx.scanPath);
7619
7852
  for (const file of hookFiles) {
7620
- const filePath = import_node_path40.default.join(ctx.scanPath, file);
7621
- const content = await import_node_fs23.promises.readFile(filePath, "utf8");
7853
+ const filePath = import_node_path43.default.join(ctx.scanPath, file);
7854
+ const content = await import_node_fs26.promises.readFile(filePath, "utf8");
7622
7855
  if (content.includes(match.registration_snippet)) {
7623
7856
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
7624
- await import_node_fs23.promises.writeFile(filePath, filtered, "utf8");
7857
+ await import_node_fs26.promises.writeFile(filePath, filtered, "utf8");
7625
7858
  break;
7626
7859
  }
7627
7860
  }
@@ -7633,7 +7866,7 @@ async function rollbackExtension(ctx, args) {
7633
7866
 
7634
7867
  // src/divergences.ts
7635
7868
  init_cjs_shims();
7636
- var import_types28 = require("@neat.is/types");
7869
+ var import_types31 = require("@neat.is/types");
7637
7870
  function bucketKey(source, target, type) {
7638
7871
  return `${type}|${source}|${target}`;
7639
7872
  }
@@ -7641,22 +7874,22 @@ function bucketEdges(graph) {
7641
7874
  const buckets2 = /* @__PURE__ */ new Map();
7642
7875
  graph.forEachEdge((id, attrs) => {
7643
7876
  const e = attrs;
7644
- const parsed = (0, import_types28.parseEdgeId)(id);
7877
+ const parsed = (0, import_types31.parseEdgeId)(id);
7645
7878
  const provenance = parsed?.provenance ?? e.provenance;
7646
7879
  const key = bucketKey(e.source, e.target, e.type);
7647
7880
  const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
7648
7881
  switch (provenance) {
7649
- case import_types28.Provenance.EXTRACTED:
7882
+ case import_types31.Provenance.EXTRACTED:
7650
7883
  cur.extracted = e;
7651
7884
  break;
7652
- case import_types28.Provenance.OBSERVED:
7885
+ case import_types31.Provenance.OBSERVED:
7653
7886
  cur.observed = e;
7654
7887
  break;
7655
- case import_types28.Provenance.INFERRED:
7888
+ case import_types31.Provenance.INFERRED:
7656
7889
  cur.inferred = e;
7657
7890
  break;
7658
7891
  default:
7659
- if (e.provenance === import_types28.Provenance.STALE) cur.stale = e;
7892
+ if (e.provenance === import_types31.Provenance.STALE) cur.stale = e;
7660
7893
  }
7661
7894
  buckets2.set(key, cur);
7662
7895
  });
@@ -7665,12 +7898,12 @@ function bucketEdges(graph) {
7665
7898
  function nodeIsFrontier(graph, nodeId) {
7666
7899
  if (!graph.hasNode(nodeId)) return false;
7667
7900
  const attrs = graph.getNodeAttributes(nodeId);
7668
- return attrs.type === import_types28.NodeType.FrontierNode;
7901
+ return attrs.type === import_types31.NodeType.FrontierNode;
7669
7902
  }
7670
7903
  function nodeIsWebsocketChannel(graph, nodeId) {
7671
7904
  if (!graph.hasNode(nodeId)) return false;
7672
7905
  const attrs = graph.getNodeAttributes(nodeId);
7673
- return attrs.type === import_types28.NodeType.WebSocketChannelNode;
7906
+ return attrs.type === import_types31.NodeType.WebSocketChannelNode;
7674
7907
  }
7675
7908
  function clampConfidence(n) {
7676
7909
  if (!Number.isFinite(n)) return 0;
@@ -7690,14 +7923,14 @@ function gradedConfidence(edge) {
7690
7923
  return clampConfidence(confidenceForEdge(edge));
7691
7924
  }
7692
7925
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
7693
- import_types28.EdgeType.CALLS,
7694
- import_types28.EdgeType.CONNECTS_TO,
7695
- import_types28.EdgeType.PUBLISHES_TO,
7696
- import_types28.EdgeType.CONSUMES_FROM
7926
+ import_types31.EdgeType.CALLS,
7927
+ import_types31.EdgeType.CONNECTS_TO,
7928
+ import_types31.EdgeType.PUBLISHES_TO,
7929
+ import_types31.EdgeType.CONSUMES_FROM
7697
7930
  ]);
7698
7931
  function detectMissingDivergences(graph, bucket) {
7699
7932
  const out = [];
7700
- if (bucket.type === import_types28.EdgeType.CONTAINS) return out;
7933
+ if (bucket.type === import_types31.EdgeType.CONTAINS) return out;
7701
7934
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
7702
7935
  if (!nodeIsFrontier(graph, bucket.target)) {
7703
7936
  out.push({
@@ -7738,7 +7971,7 @@ function declaredHostFor(svc) {
7738
7971
  function hasExtractedConfiguredBy(graph, svcId) {
7739
7972
  for (const edgeId of graph.outboundEdges(svcId)) {
7740
7973
  const e = graph.getEdgeAttributes(edgeId);
7741
- if (e.type === import_types28.EdgeType.CONFIGURED_BY && e.provenance === import_types28.Provenance.EXTRACTED) {
7974
+ if (e.type === import_types31.EdgeType.CONFIGURED_BY && e.provenance === import_types31.Provenance.EXTRACTED) {
7742
7975
  return true;
7743
7976
  }
7744
7977
  }
@@ -7751,10 +7984,10 @@ function detectHostMismatch(graph, svcId, svc) {
7751
7984
  const out = [];
7752
7985
  for (const edgeId of graph.outboundEdges(svcId)) {
7753
7986
  const edge = graph.getEdgeAttributes(edgeId);
7754
- if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7755
- if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
7987
+ if (edge.type !== import_types31.EdgeType.CONNECTS_TO) continue;
7988
+ if (edge.provenance !== import_types31.Provenance.OBSERVED) continue;
7756
7989
  const target = graph.getNodeAttributes(edge.target);
7757
- if (target.type !== import_types28.NodeType.DatabaseNode) continue;
7990
+ if (target.type !== import_types31.NodeType.DatabaseNode) continue;
7758
7991
  const observedHost = target.host?.trim();
7759
7992
  if (!observedHost) continue;
7760
7993
  if (observedHost === declaredHost) continue;
@@ -7776,10 +8009,10 @@ function detectCompatDivergences(graph, svcId, svc) {
7776
8009
  const deps = svc.dependencies ?? {};
7777
8010
  for (const edgeId of graph.outboundEdges(svcId)) {
7778
8011
  const edge = graph.getEdgeAttributes(edgeId);
7779
- if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7780
- if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
8012
+ if (edge.type !== import_types31.EdgeType.CONNECTS_TO) continue;
8013
+ if (edge.provenance !== import_types31.Provenance.OBSERVED) continue;
7781
8014
  const target = graph.getNodeAttributes(edge.target);
7782
- if (target.type !== import_types28.NodeType.DatabaseNode) continue;
8015
+ if (target.type !== import_types31.NodeType.DatabaseNode) continue;
7783
8016
  for (const pair of compatPairs()) {
7784
8017
  if (pair.engine !== target.engine) continue;
7785
8018
  const declared = deps[pair.driver];
@@ -7838,7 +8071,7 @@ function suppressHostMismatchHalves(all) {
7838
8071
  for (const d of all) {
7839
8072
  if (d.type !== "host-mismatch") continue;
7840
8073
  observedHalf.add(`${d.source}->${d.target}`);
7841
- declaredHalf.add((0, import_types28.databaseId)(d.extractedHost));
8074
+ declaredHalf.add((0, import_types31.databaseId)(d.extractedHost));
7842
8075
  }
7843
8076
  if (observedHalf.size === 0) return all;
7844
8077
  return all.filter((d) => {
@@ -7857,7 +8090,7 @@ function computeDivergences(graph, opts = {}) {
7857
8090
  }
7858
8091
  graph.forEachNode((nodeId, attrs) => {
7859
8092
  const n = attrs;
7860
- if (n.type !== import_types28.NodeType.ServiceNode) return;
8093
+ if (n.type !== import_types31.NodeType.ServiceNode) return;
7861
8094
  const svc = n;
7862
8095
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
7863
8096
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -7891,7 +8124,7 @@ function computeDivergences(graph, opts = {}) {
7891
8124
  if (a.source !== b.source) return a.source.localeCompare(b.source);
7892
8125
  return a.target.localeCompare(b.target);
7893
8126
  });
7894
- return import_types28.DivergenceResultSchema.parse({
8127
+ return import_types31.DivergenceResultSchema.parse({
7895
8128
  divergences: filtered,
7896
8129
  totalAffected: filtered.length,
7897
8130
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -7945,7 +8178,7 @@ function queryLogEntries(opts) {
7945
8178
 
7946
8179
  // src/diff.ts
7947
8180
  init_cjs_shims();
7948
- var import_node_fs24 = require("fs");
8181
+ var import_node_fs27 = require("fs");
7949
8182
  async function loadSnapshotForDiff(target) {
7950
8183
  if (/^https?:\/\//i.test(target)) {
7951
8184
  const res = await fetch(target);
@@ -7954,7 +8187,7 @@ async function loadSnapshotForDiff(target) {
7954
8187
  }
7955
8188
  return await res.json();
7956
8189
  }
7957
- const raw = await import_node_fs24.promises.readFile(target, "utf8");
8190
+ const raw = await import_node_fs27.promises.readFile(target, "utf8");
7958
8191
  return JSON.parse(raw);
7959
8192
  }
7960
8193
  function indexEntries(entries) {
@@ -8022,23 +8255,23 @@ function canonicalJson(value) {
8022
8255
 
8023
8256
  // src/projects.ts
8024
8257
  init_cjs_shims();
8025
- var import_node_path41 = __toESM(require("path"), 1);
8258
+ var import_node_path44 = __toESM(require("path"), 1);
8026
8259
  function pathsForProject(project, baseDir) {
8027
8260
  if (project === DEFAULT_PROJECT) {
8028
8261
  return {
8029
- snapshotPath: import_node_path41.default.join(baseDir, "graph.json"),
8030
- errorsPath: import_node_path41.default.join(baseDir, "errors.ndjson"),
8031
- staleEventsPath: import_node_path41.default.join(baseDir, "stale-events.ndjson"),
8032
- embeddingsCachePath: import_node_path41.default.join(baseDir, "embeddings.json"),
8033
- policyViolationsPath: import_node_path41.default.join(baseDir, "policy-violations.ndjson")
8262
+ snapshotPath: import_node_path44.default.join(baseDir, "graph.json"),
8263
+ errorsPath: import_node_path44.default.join(baseDir, "errors.ndjson"),
8264
+ staleEventsPath: import_node_path44.default.join(baseDir, "stale-events.ndjson"),
8265
+ embeddingsCachePath: import_node_path44.default.join(baseDir, "embeddings.json"),
8266
+ policyViolationsPath: import_node_path44.default.join(baseDir, "policy-violations.ndjson")
8034
8267
  };
8035
8268
  }
8036
8269
  return {
8037
- snapshotPath: import_node_path41.default.join(baseDir, `${project}.json`),
8038
- errorsPath: import_node_path41.default.join(baseDir, `errors.${project}.ndjson`),
8039
- staleEventsPath: import_node_path41.default.join(baseDir, `stale-events.${project}.ndjson`),
8040
- embeddingsCachePath: import_node_path41.default.join(baseDir, `embeddings.${project}.json`),
8041
- policyViolationsPath: import_node_path41.default.join(baseDir, `policy-violations.${project}.ndjson`)
8270
+ snapshotPath: import_node_path44.default.join(baseDir, `${project}.json`),
8271
+ errorsPath: import_node_path44.default.join(baseDir, `errors.${project}.ndjson`),
8272
+ staleEventsPath: import_node_path44.default.join(baseDir, `stale-events.${project}.ndjson`),
8273
+ embeddingsCachePath: import_node_path44.default.join(baseDir, `embeddings.${project}.json`),
8274
+ policyViolationsPath: import_node_path44.default.join(baseDir, `policy-violations.${project}.ndjson`)
8042
8275
  };
8043
8276
  }
8044
8277
  var Projects = class {
@@ -8074,25 +8307,25 @@ var Projects = class {
8074
8307
 
8075
8308
  // src/registry.ts
8076
8309
  init_cjs_shims();
8077
- var import_node_fs25 = require("fs");
8310
+ var import_node_fs28 = require("fs");
8078
8311
  var import_node_os3 = __toESM(require("os"), 1);
8079
- var import_node_path42 = __toESM(require("path"), 1);
8080
- var import_types29 = require("@neat.is/types");
8312
+ var import_node_path45 = __toESM(require("path"), 1);
8313
+ var import_types32 = require("@neat.is/types");
8081
8314
  var LOCK_TIMEOUT_MS = 5e3;
8082
8315
  var LOCK_RETRY_MS = 50;
8083
8316
  function neatHome() {
8084
8317
  const override = process.env.NEAT_HOME;
8085
- if (override && override.length > 0) return import_node_path42.default.resolve(override);
8086
- return import_node_path42.default.join(import_node_os3.default.homedir(), ".neat");
8318
+ if (override && override.length > 0) return import_node_path45.default.resolve(override);
8319
+ return import_node_path45.default.join(import_node_os3.default.homedir(), ".neat");
8087
8320
  }
8088
8321
  function registryPath() {
8089
- return import_node_path42.default.join(neatHome(), "projects.json");
8322
+ return import_node_path45.default.join(neatHome(), "projects.json");
8090
8323
  }
8091
8324
  function registryLockPath() {
8092
- return import_node_path42.default.join(neatHome(), "projects.json.lock");
8325
+ return import_node_path45.default.join(neatHome(), "projects.json.lock");
8093
8326
  }
8094
8327
  function daemonPidPath() {
8095
- return import_node_path42.default.join(neatHome(), "neatd.pid");
8328
+ return import_node_path45.default.join(neatHome(), "neatd.pid");
8096
8329
  }
8097
8330
  function isPidAliveDefault(pid) {
8098
8331
  try {
@@ -8104,7 +8337,7 @@ function isPidAliveDefault(pid) {
8104
8337
  }
8105
8338
  async function readPidFile(file) {
8106
8339
  try {
8107
- const raw = await import_node_fs25.promises.readFile(file, "utf8");
8340
+ const raw = await import_node_fs28.promises.readFile(file, "utf8");
8108
8341
  const pid = Number.parseInt(raw.trim(), 10);
8109
8342
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
8110
8343
  } catch {
@@ -8152,32 +8385,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
8152
8385
  }
8153
8386
  }
8154
8387
  async function normalizeProjectPath(input) {
8155
- const resolved = import_node_path42.default.resolve(input);
8388
+ const resolved = import_node_path45.default.resolve(input);
8156
8389
  try {
8157
- return await import_node_fs25.promises.realpath(resolved);
8390
+ return await import_node_fs28.promises.realpath(resolved);
8158
8391
  } catch {
8159
8392
  return resolved;
8160
8393
  }
8161
8394
  }
8162
8395
  async function writeAtomically(target, contents) {
8163
- await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(target), { recursive: true });
8396
+ await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(target), { recursive: true });
8164
8397
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
8165
- const fd = await import_node_fs25.promises.open(tmp, "w");
8398
+ const fd = await import_node_fs28.promises.open(tmp, "w");
8166
8399
  try {
8167
8400
  await fd.writeFile(contents, "utf8");
8168
8401
  await fd.sync();
8169
8402
  } finally {
8170
8403
  await fd.close();
8171
8404
  }
8172
- await import_node_fs25.promises.rename(tmp, target);
8405
+ await import_node_fs28.promises.rename(tmp, target);
8173
8406
  }
8174
8407
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
8175
8408
  const deadline = Date.now() + timeoutMs;
8176
- await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(lockPath), { recursive: true });
8409
+ await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(lockPath), { recursive: true });
8177
8410
  let probedHolder = false;
8178
8411
  while (true) {
8179
8412
  try {
8180
- const fd = await import_node_fs25.promises.open(lockPath, "wx");
8413
+ const fd = await import_node_fs28.promises.open(lockPath, "wx");
8181
8414
  try {
8182
8415
  await fd.writeFile(`${process.pid}
8183
8416
  `, "utf8");
@@ -8202,7 +8435,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
8202
8435
  }
8203
8436
  }
8204
8437
  async function releaseLock(lockPath) {
8205
- await import_node_fs25.promises.unlink(lockPath).catch(() => {
8438
+ await import_node_fs28.promises.unlink(lockPath).catch(() => {
8206
8439
  });
8207
8440
  }
8208
8441
  async function withLock(fn) {
@@ -8218,7 +8451,7 @@ async function readRegistry() {
8218
8451
  const file = registryPath();
8219
8452
  let raw;
8220
8453
  try {
8221
- raw = await import_node_fs25.promises.readFile(file, "utf8");
8454
+ raw = await import_node_fs28.promises.readFile(file, "utf8");
8222
8455
  } catch (err) {
8223
8456
  if (err.code === "ENOENT") {
8224
8457
  return { version: 1, projects: [] };
@@ -8226,10 +8459,10 @@ async function readRegistry() {
8226
8459
  throw err;
8227
8460
  }
8228
8461
  const parsed = JSON.parse(raw);
8229
- return import_types29.RegistryFileSchema.parse(parsed);
8462
+ return import_types32.RegistryFileSchema.parse(parsed);
8230
8463
  }
8231
8464
  async function writeRegistry(reg) {
8232
- const validated = import_types29.RegistryFileSchema.parse(reg);
8465
+ const validated = import_types32.RegistryFileSchema.parse(reg);
8233
8466
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
8234
8467
  }
8235
8468
  var ProjectNameCollisionError = class extends Error {
@@ -8323,7 +8556,7 @@ function pruneTtlMs() {
8323
8556
  }
8324
8557
  async function statPathStatus(p) {
8325
8558
  try {
8326
- const stat = await import_node_fs25.promises.stat(p);
8559
+ const stat = await import_node_fs28.promises.stat(p);
8327
8560
  return stat.isDirectory() ? "present" : "unknown";
8328
8561
  } catch (err) {
8329
8562
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -8420,6 +8653,215 @@ data: ${JSON.stringify(envelope.payload)}
8420
8653
 
8421
8654
  // src/api.ts
8422
8655
  init_auth();
8656
+
8657
+ // src/connectors-config.ts
8658
+ init_cjs_shims();
8659
+ var import_node_os4 = __toESM(require("os"), 1);
8660
+ var import_node_path46 = __toESM(require("path"), 1);
8661
+ var import_node_fs29 = require("fs");
8662
+ var CONNECTORS_CONFIG_VERSION = 1;
8663
+ var EnvRefUnsetError = class extends Error {
8664
+ ref;
8665
+ varName;
8666
+ constructor(ref, varName) {
8667
+ super(`${ref} is unset`);
8668
+ this.name = "EnvRefUnsetError";
8669
+ this.ref = ref;
8670
+ this.varName = varName;
8671
+ }
8672
+ };
8673
+ function neatHome2() {
8674
+ const override = process.env.NEAT_HOME;
8675
+ if (override && override.length > 0) return import_node_path46.default.resolve(override);
8676
+ return import_node_path46.default.join(import_node_os4.default.homedir(), ".neat");
8677
+ }
8678
+ function connectorsConfigPath(home = neatHome2()) {
8679
+ return import_node_path46.default.join(home, "connectors.json");
8680
+ }
8681
+ var MODE_MASK_LOOSER_THAN_0600 = 63;
8682
+ async function warnIfModeLooserThan0600(file) {
8683
+ if (process.platform === "win32") return;
8684
+ try {
8685
+ const stat = await import_node_fs29.promises.stat(file);
8686
+ if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
8687
+ const mode = (stat.mode & 511).toString(8).padStart(3, "0");
8688
+ console.warn(
8689
+ `[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
8690
+ );
8691
+ }
8692
+ } catch {
8693
+ }
8694
+ }
8695
+ async function readConnectorsConfig(home = neatHome2()) {
8696
+ const file = connectorsConfigPath(home);
8697
+ let raw;
8698
+ try {
8699
+ raw = await import_node_fs29.promises.readFile(file, "utf8");
8700
+ } catch (err) {
8701
+ if (err.code === "ENOENT") {
8702
+ return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
8703
+ }
8704
+ throw err;
8705
+ }
8706
+ await warnIfModeLooserThan0600(file);
8707
+ let parsed;
8708
+ try {
8709
+ parsed = JSON.parse(raw);
8710
+ } catch (err) {
8711
+ throw new Error(`${file} is not valid JSON: ${err.message}`);
8712
+ }
8713
+ return validateConfig(parsed, file);
8714
+ }
8715
+ function validateConfig(parsed, file) {
8716
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8717
+ throw new Error(`${file} must be a JSON object with a "connectors" array`);
8718
+ }
8719
+ const obj = parsed;
8720
+ const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
8721
+ if (typeof version !== "number" || !Number.isInteger(version)) {
8722
+ throw new Error(`${file}: "version" must be an integer`);
8723
+ }
8724
+ const rawConnectors = obj.connectors;
8725
+ if (!Array.isArray(rawConnectors)) {
8726
+ throw new Error(`${file}: "connectors" must be an array`);
8727
+ }
8728
+ const connectors = rawConnectors.map((entry, i) => validateEntry(entry, i, file));
8729
+ return { version, connectors };
8730
+ }
8731
+ function validateEntry(entry, index, file) {
8732
+ const where = `${file}: connectors[${index}]`;
8733
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
8734
+ throw new Error(`${where} must be an object`);
8735
+ }
8736
+ const e = entry;
8737
+ const id = e.id;
8738
+ if (typeof id !== "string" || id.length === 0) {
8739
+ throw new Error(`${where}.id must be a non-empty string`);
8740
+ }
8741
+ const provider = e.provider;
8742
+ if (typeof provider !== "string" || provider.length === 0) {
8743
+ throw new Error(`${where}.provider must be a non-empty string`);
8744
+ }
8745
+ if (e.project !== void 0 && typeof e.project !== "string") {
8746
+ throw new Error(`${where}.project must be a string when present`);
8747
+ }
8748
+ const credential = validateCredentialRef(e.credential, `${where}.credential`);
8749
+ let options;
8750
+ if (e.options !== void 0) {
8751
+ if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
8752
+ throw new Error(`${where}.options must be an object when present`);
8753
+ }
8754
+ options = e.options;
8755
+ }
8756
+ return {
8757
+ id,
8758
+ provider,
8759
+ ...typeof e.project === "string" ? { project: e.project } : {},
8760
+ credential,
8761
+ ...options ? { options } : {}
8762
+ };
8763
+ }
8764
+ function validateCredentialRef(raw, where) {
8765
+ if (typeof raw === "string") {
8766
+ if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
8767
+ return raw;
8768
+ }
8769
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
8770
+ const fields = raw;
8771
+ const keys = Object.keys(fields);
8772
+ if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
8773
+ for (const key of keys) {
8774
+ const v = fields[key];
8775
+ if (typeof v !== "string" || v.length === 0) {
8776
+ throw new Error(`${where}.${key} must be a non-empty string`);
8777
+ }
8778
+ }
8779
+ return fields;
8780
+ }
8781
+ throw new Error(`${where} must be a string or an object of strings`);
8782
+ }
8783
+ function resolveCredential(ref, env = process.env) {
8784
+ if (typeof ref === "string") {
8785
+ return { kind: "single", value: resolveRef(ref, env) };
8786
+ }
8787
+ const fields = {};
8788
+ for (const [key, value] of Object.entries(ref)) {
8789
+ fields[key] = resolveRef(value, env);
8790
+ }
8791
+ return { kind: "fields", fields };
8792
+ }
8793
+ function resolveRef(value, env) {
8794
+ if (value.length > 1 && value.startsWith("$")) {
8795
+ const varName = value.slice(1);
8796
+ const resolved = env[varName];
8797
+ if (resolved === void 0 || resolved.length === 0) {
8798
+ throw new EnvRefUnsetError(value, varName);
8799
+ }
8800
+ return resolved;
8801
+ }
8802
+ return value;
8803
+ }
8804
+ function connectorMatchesProject(entry, project) {
8805
+ return entry.project === void 0 || entry.project === project;
8806
+ }
8807
+ function isEnvRef(value) {
8808
+ return value.length > 1 && value.startsWith("$");
8809
+ }
8810
+ function redactCredentialRef(ref) {
8811
+ const one = (value) => isEnvRef(value) ? value : "****";
8812
+ if (typeof ref === "string") return one(ref);
8813
+ const out = {};
8814
+ for (const [key, value] of Object.entries(ref)) out[key] = one(value);
8815
+ return out;
8816
+ }
8817
+
8818
+ // src/connectors/status.ts
8819
+ init_cjs_shims();
8820
+ var CONNECTOR_STALE_THRESHOLD_MS = 5 * 6e4;
8821
+ var records = /* @__PURE__ */ new Map();
8822
+ var MAX_ERROR_LEN = 200;
8823
+ function sanitizePollError(err) {
8824
+ const msg = err instanceof Error ? err.message : String(err);
8825
+ const collapsed = msg.replace(/\s+/g, " ").trim();
8826
+ return collapsed.length > MAX_ERROR_LEN ? `${collapsed.slice(0, MAX_ERROR_LEN - 1)}\u2026` : collapsed;
8827
+ }
8828
+ function recordConnectorPoll(id, tick) {
8829
+ const prev = records.get(id);
8830
+ records.set(id, {
8831
+ lastPollAt: tick.at,
8832
+ lastOutcome: tick.outcome,
8833
+ lastError: tick.outcome === "error" ? tick.error ?? null : null,
8834
+ signalsLastPoll: tick.signalsLastPoll ?? 0,
8835
+ lastOkAt: tick.outcome === "ok" ? tick.at : prev?.lastOkAt ?? null
8836
+ });
8837
+ }
8838
+ function deriveState(rec, now, thresholdMs) {
8839
+ if (rec.lastOutcome === "error") return "error";
8840
+ const okAt = rec.lastOkAt ? Date.parse(rec.lastOkAt) : NaN;
8841
+ if (!Number.isNaN(okAt) && now - okAt > thresholdMs) return "stale";
8842
+ return "healthy";
8843
+ }
8844
+ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_THRESHOLD_MS) {
8845
+ const rec = records.get(id);
8846
+ if (!rec) {
8847
+ return {
8848
+ state: "idle",
8849
+ lastPollAt: null,
8850
+ lastOutcome: null,
8851
+ lastError: null,
8852
+ signalsLastPoll: 0
8853
+ };
8854
+ }
8855
+ return {
8856
+ state: deriveState(rec, now, thresholdMs),
8857
+ lastPollAt: rec.lastPollAt,
8858
+ lastOutcome: rec.lastOutcome,
8859
+ lastError: rec.lastError,
8860
+ signalsLastPoll: rec.signalsLastPoll
8861
+ };
8862
+ }
8863
+
8864
+ // src/api.ts
8423
8865
  function serializeGraph(graph) {
8424
8866
  const nodes = [];
8425
8867
  graph.forEachNode((_id, attrs) => {
@@ -8570,11 +9012,11 @@ function registerRoutes(scope, ctx) {
8570
9012
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
8571
9013
  const parsed = [];
8572
9014
  for (const c of candidates) {
8573
- const r = import_types30.DivergenceTypeSchema.safeParse(c);
9015
+ const r = import_types33.DivergenceTypeSchema.safeParse(c);
8574
9016
  if (!r.success) {
8575
9017
  return reply.code(400).send({
8576
9018
  error: `unknown divergence type "${c}"`,
8577
- allowed: import_types30.DivergenceTypeSchema.options
9019
+ allowed: import_types33.DivergenceTypeSchema.options
8578
9020
  });
8579
9021
  }
8580
9022
  parsed.push(r.data);
@@ -8639,6 +9081,26 @@ function registerRoutes(scope, ctx) {
8639
9081
  const sliced = filtered.slice(0, safeLimit);
8640
9082
  return { count: sliced.length, total, logs: sliced };
8641
9083
  });
9084
+ scope.get("/connectors", async (req, reply) => {
9085
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9086
+ if (!proj) return;
9087
+ let entries;
9088
+ try {
9089
+ entries = (await readConnectorsConfig(ctx.connectorsHome)).connectors;
9090
+ } catch (err) {
9091
+ return reply.code(500).send({
9092
+ error: "connectors.json failed to read",
9093
+ details: err.message
9094
+ });
9095
+ }
9096
+ const connectors = entries.filter((entry) => connectorMatchesProject(entry, proj.name)).map((entry) => ({
9097
+ id: entry.id,
9098
+ provider: entry.provider,
9099
+ credentialRef: redactCredentialRef(entry.credential),
9100
+ status: getConnectorStatus(entry.id)
9101
+ }));
9102
+ return { connectors };
9103
+ });
8642
9104
  const incidentHistoryHandler = async (req, reply) => {
8643
9105
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
8644
9106
  if (!proj) return;
@@ -8825,7 +9287,7 @@ function registerRoutes(scope, ctx) {
8825
9287
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
8826
9288
  let violations = await log.readAll();
8827
9289
  if (req.query.severity) {
8828
- const sev = import_types30.PolicySeveritySchema.safeParse(req.query.severity);
9290
+ const sev = import_types33.PolicySeveritySchema.safeParse(req.query.severity);
8829
9291
  if (!sev.success) {
8830
9292
  return reply.code(400).send({
8831
9293
  error: "invalid severity",
@@ -8864,7 +9326,7 @@ function registerRoutes(scope, ctx) {
8864
9326
  scope.post("/policies/check", async (req, reply) => {
8865
9327
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
8866
9328
  if (!proj) return;
8867
- const parsed = import_types30.PoliciesCheckBodySchema.safeParse(req.body ?? {});
9329
+ const parsed = import_types33.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8868
9330
  if (!parsed.success) {
8869
9331
  return reply.code(400).send({
8870
9332
  error: "invalid /policies/check body",
@@ -9041,7 +9503,8 @@ async function buildApi(opts) {
9041
9503
  staleEventsPathFor,
9042
9504
  policyFilePathFor,
9043
9505
  bootstrap: opts.bootstrap,
9044
- singleProject: opts.singleProject?.name
9506
+ singleProject: opts.singleProject?.name,
9507
+ connectorsHome: opts.connectorsHome
9045
9508
  };
9046
9509
  app.get("/health", async () => {
9047
9510
  const uptimeMs = Date.now() - startedAt;
@@ -9126,8 +9589,8 @@ init_otel_grpc();
9126
9589
 
9127
9590
  // src/daemon.ts
9128
9591
  init_cjs_shims();
9129
- var import_node_fs28 = require("fs");
9130
- var import_node_path47 = __toESM(require("path"), 1);
9592
+ var import_node_fs31 = require("fs");
9593
+ var import_node_path50 = __toESM(require("path"), 1);
9131
9594
  var import_node_module = require("module");
9132
9595
  init_otel();
9133
9596
 
@@ -9191,16 +9654,31 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
9191
9654
  let stopped = false;
9192
9655
  let since = ctx.since;
9193
9656
  const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9657
+ const connectorId = options.connectorId;
9194
9658
  const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
9195
9659
  const tick = () => {
9196
9660
  if (stopped) return;
9197
9661
  void (async () => {
9198
9662
  const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
9199
9663
  try {
9200
- await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
9664
+ const result = await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
9201
9665
  since = tickStartedAt;
9666
+ if (connectorId) {
9667
+ recordConnectorPoll(connectorId, {
9668
+ outcome: "ok",
9669
+ at: tickStartedAt,
9670
+ signalsLastPoll: result.signalCount
9671
+ });
9672
+ }
9202
9673
  } catch (err) {
9203
9674
  onError(err);
9675
+ if (connectorId) {
9676
+ recordConnectorPoll(connectorId, {
9677
+ outcome: "error",
9678
+ at: tickStartedAt,
9679
+ error: sanitizePollError(err)
9680
+ });
9681
+ }
9204
9682
  }
9205
9683
  })();
9206
9684
  };
@@ -9499,6 +9977,30 @@ function buildEdgeLogsQuery(limit) {
9499
9977
  `limit ${safeLimit}`
9500
9978
  ].join("\n");
9501
9979
  }
9980
+ function logsAllHttpFailureMessage(status2) {
9981
+ if (status2 === 401 || status2 === 403) {
9982
+ return `supabase connector: logs.all request rejected (HTTP ${status2}). Check the Management API token, its analytics read scope, and --api-project-ref.`;
9983
+ }
9984
+ if (status2 === 404) {
9985
+ return "supabase connector: logs.all project not found (HTTP 404). Check --api-project-ref.";
9986
+ }
9987
+ if (status2 === 429) {
9988
+ return "supabase connector: logs.all rate-limited (HTTP 429). The connector will retry on the next poll; reduce poll cadence if this persists.";
9989
+ }
9990
+ if (status2 === 400) {
9991
+ return "supabase connector: logs.all query rejected (HTTP 400). Provider details redacted; confirm the live log-query dialect before shipping this connector.";
9992
+ }
9993
+ return `supabase connector: logs.all request failed (HTTP ${status2}); provider details redacted.`;
9994
+ }
9995
+ function providerErrorDetails(error) {
9996
+ if (!error || typeof error === "string") return "";
9997
+ const parts = [];
9998
+ if (typeof error.code === "number") parts.push(`code ${error.code}`);
9999
+ if (typeof error.status === "string" && /^[A-Z0-9_.-]+$/.test(error.status)) {
10000
+ parts.push(`status ${error.status}`);
10001
+ }
10002
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
10003
+ }
9502
10004
  async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
9503
10005
  const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
9504
10006
  const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
@@ -9513,12 +10015,18 @@ async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl
9513
10015
  { provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
9514
10016
  );
9515
10017
  if (!res.ok) {
9516
- throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
10018
+ throw new Error(logsAllHttpFailureMessage(res.status));
10019
+ }
10020
+ let body;
10021
+ try {
10022
+ body = await res.json();
10023
+ } catch {
10024
+ throw new Error("supabase connector: logs.all returned invalid JSON; provider details redacted.");
9517
10025
  }
9518
- const body = await res.json();
9519
10026
  if (body.error) {
9520
- const message = typeof body.error === "string" ? body.error : body.error.message;
9521
- throw new Error(`supabase connector: logs.all returned an error (${message})`);
10027
+ throw new Error(
10028
+ `supabase connector: logs.all returned a provider error${providerErrorDetails(body.error)}; provider message redacted.`
10029
+ );
9522
10030
  }
9523
10031
  return body.result ?? [];
9524
10032
  }
@@ -9550,10 +10058,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
9550
10058
  // src/connectors/supabase/map.ts
9551
10059
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
9552
10060
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
9553
- function targetFromRestPath(path48) {
9554
- const rpcMatch = REST_RPC_PATH_RE.exec(path48);
10061
+ function targetFromRestPath(path51) {
10062
+ const rpcMatch = REST_RPC_PATH_RE.exec(path51);
9555
10063
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
9556
- const tableMatch = REST_TABLE_PATH_RE.exec(path48);
10064
+ const tableMatch = REST_TABLE_PATH_RE.exec(path51);
9557
10065
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
9558
10066
  return null;
9559
10067
  }
@@ -9656,19 +10164,19 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
9656
10164
 
9657
10165
  // src/connectors/supabase/resolve.ts
9658
10166
  init_cjs_shims();
9659
- var import_types32 = require("@neat.is/types");
10167
+ var import_types35 = require("@neat.is/types");
9660
10168
  function createSupabaseResolveTarget(graph, config) {
9661
10169
  return (signal, _ctx) => {
9662
10170
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
9663
10171
  return null;
9664
10172
  }
9665
- const subResourceId = (0, import_types32.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
10173
+ const subResourceId = (0, import_types35.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
9666
10174
  if (graph.hasNode(subResourceId)) {
9667
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
10175
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types35.EdgeType.CALLS };
9668
10176
  }
9669
- const projectLevelId = (0, import_types32.infraId)("supabase", config.nodeRef);
10177
+ const projectLevelId = (0, import_types35.infraId)("supabase", config.nodeRef);
9670
10178
  if (graph.hasNode(projectLevelId)) {
9671
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
10179
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types35.EdgeType.CALLS };
9672
10180
  }
9673
10181
  return null;
9674
10182
  };
@@ -9676,6 +10184,35 @@ function createSupabaseResolveTarget(graph, config) {
9676
10184
 
9677
10185
  // src/connectors/supabase/index.ts
9678
10186
  var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
10187
+ function errorCode(err) {
10188
+ const code = err?.code;
10189
+ return typeof code === "string" && code.length > 0 ? code : void 0;
10190
+ }
10191
+ function describeSupabasePostgresSurfaceFailure(projectRef, err) {
10192
+ const code = errorCode(err);
10193
+ let reason;
10194
+ switch (code) {
10195
+ case "42501":
10196
+ reason = "permission denied; grant pg_read_all_stats to the configured Postgres role";
10197
+ break;
10198
+ case "42P01":
10199
+ case "42704":
10200
+ reason = "pg_stat_statements is not enabled or visible to the configured Postgres role";
10201
+ break;
10202
+ case "28P01":
10203
+ case "28000":
10204
+ reason = "Postgres credential rejected";
10205
+ break;
10206
+ case "3D000":
10207
+ reason = "database not found";
10208
+ break;
10209
+ default: {
10210
+ const name = err instanceof Error && err.name ? err.name : "Error";
10211
+ reason = code ? `${name} ${code}` : name;
10212
+ }
10213
+ }
10214
+ return `supabase connector: pg_stat_statements surface unavailable for project ${projectRef} (${reason}); continuing with Management API log surface.`;
10215
+ }
9679
10216
  var SupabaseConnector = class {
9680
10217
  // `deps.fetchPgStatStatements` defaults to the real Postgres-backed
9681
10218
  // implementation; tests override it to exercise the "both surfaces
@@ -9707,12 +10244,18 @@ var SupabaseConnector = class {
9707
10244
  const signals = mapEdgeLogRowsToSignals(logRows);
9708
10245
  if (creds.postgresConnectionString) {
9709
10246
  const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
9710
- const statementRows = await fetchStatements(
9711
- creds.postgresConnectionString,
9712
- this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
9713
- this.config.apiProjectRef
9714
- );
9715
- signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
10247
+ try {
10248
+ const statementRows = await fetchStatements(
10249
+ creds.postgresConnectionString,
10250
+ this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
10251
+ this.config.apiProjectRef
10252
+ );
10253
+ signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
10254
+ } catch (err) {
10255
+ const summary = describeSupabasePostgresSurfaceFailure(this.config.apiProjectRef, err);
10256
+ if (this.deps.onPostgresSurfaceError) this.deps.onPostgresSurfaceError(err, summary);
10257
+ else console.warn(summary);
10258
+ }
9716
10259
  }
9717
10260
  return signals;
9718
10261
  }
@@ -9726,7 +10269,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
9726
10269
 
9727
10270
  // src/connectors/railway/index.ts
9728
10271
  init_cjs_shims();
9729
- var import_types36 = require("@neat.is/types");
10272
+ var import_types39 = require("@neat.is/types");
9730
10273
 
9731
10274
  // src/connectors/railway/client.ts
9732
10275
  init_cjs_shims();
@@ -9871,7 +10414,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
9871
10414
  const out = [];
9872
10415
  graph.forEachNode((_id, attrs) => {
9873
10416
  const node = attrs;
9874
- if (node.type !== import_types36.NodeType.RouteNode) return;
10417
+ if (node.type !== import_types39.NodeType.RouteNode) return;
9875
10418
  const route = attrs;
9876
10419
  if (route.service !== serviceName) return;
9877
10420
  out.push({
@@ -9967,12 +10510,12 @@ function createRailwayResolveTarget(config) {
9967
10510
  const serviceName = config.serviceNameById[config.serviceId];
9968
10511
  if (!serviceName) return null;
9969
10512
  if (signal.targetKind === ROUTE_TARGET_KIND) {
9970
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types36.EdgeType.CALLS };
10513
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types39.EdgeType.CALLS };
9971
10514
  }
9972
10515
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
9973
10516
  const peerName = config.serviceNameById[signal.targetName];
9974
10517
  if (!peerName) return null;
9975
- return { targetNodeId: (0, import_types36.serviceId)(peerName), serviceName, edgeType: import_types36.EdgeType.CONNECTS_TO };
10518
+ return { targetNodeId: (0, import_types39.serviceId)(peerName), serviceName, edgeType: import_types39.EdgeType.CONNECTS_TO };
9976
10519
  }
9977
10520
  return null;
9978
10521
  };
@@ -10087,9 +10630,9 @@ function parseFirebaseTargetName(targetName) {
10087
10630
  const secondSep = rest.indexOf(FIELD_SEP);
10088
10631
  if (secondSep === -1) return null;
10089
10632
  const method = rest.slice(0, secondSep);
10090
- const path48 = rest.slice(secondSep + 1);
10091
- if (!resourceName || !method || !path48) return null;
10092
- return { resourceName, method, path: path48 };
10633
+ const path51 = rest.slice(secondSep + 1);
10634
+ if (!resourceName || !method || !path51) return null;
10635
+ return { resourceName, method, path: path51 };
10093
10636
  }
10094
10637
  function resourceNameFor(type, labels) {
10095
10638
  if (!labels) return null;
@@ -10126,14 +10669,14 @@ function mapLogEntryToSignal(entry) {
10126
10669
  if (!req) return null;
10127
10670
  if (!req.requestMethod) return null;
10128
10671
  const method = req.requestMethod.toUpperCase();
10129
- const path48 = pathFromRequestUrl(req.requestUrl);
10130
- if (path48 === null) return null;
10672
+ const path51 = pathFromRequestUrl(req.requestUrl);
10673
+ if (path51 === null) return null;
10131
10674
  const timestamp = entry.timestamp;
10132
10675
  if (!timestamp) return null;
10133
10676
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
10134
10677
  return {
10135
10678
  targetKind: resourceType,
10136
- targetName: packFirebaseTargetName({ resourceName, method, path: path48 }),
10679
+ targetName: packFirebaseTargetName({ resourceName, method, path: path51 }),
10137
10680
  callCount: 1,
10138
10681
  errorCount: isError ? 1 : 0,
10139
10682
  lastObservedIso: timestamp
@@ -10150,7 +10693,7 @@ function mapLogEntriesToSignals(entries) {
10150
10693
 
10151
10694
  // src/connectors/firebase/resolve.ts
10152
10695
  init_cjs_shims();
10153
- var import_types37 = require("@neat.is/types");
10696
+ var import_types40 = require("@neat.is/types");
10154
10697
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
10155
10698
  switch (resourceType) {
10156
10699
  case "cloud_function":
@@ -10165,7 +10708,7 @@ function routeEntriesFor(graph, serviceName) {
10165
10708
  const entries = [];
10166
10709
  graph.forEachNode((_id, attrs) => {
10167
10710
  const node = attrs;
10168
- if (node.type !== import_types37.NodeType.RouteNode) return;
10711
+ if (node.type !== import_types40.NodeType.RouteNode) return;
10169
10712
  const route = attrs;
10170
10713
  if (route.service !== serviceName) return;
10171
10714
  entries.push({
@@ -10197,7 +10740,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
10197
10740
  return {
10198
10741
  targetNodeId: match.routeNodeId,
10199
10742
  serviceName,
10200
- edgeType: import_types37.EdgeType.CALLS
10743
+ edgeType: import_types40.EdgeType.CALLS
10201
10744
  };
10202
10745
  };
10203
10746
  }
@@ -10224,7 +10767,7 @@ init_cjs_shims();
10224
10767
 
10225
10768
  // src/connectors/cloudflare/connector.ts
10226
10769
  init_cjs_shims();
10227
- var import_types39 = require("@neat.is/types");
10770
+ var import_types42 = require("@neat.is/types");
10228
10771
 
10229
10772
  // src/connectors/cloudflare/client.ts
10230
10773
  init_cjs_shims();
@@ -10331,7 +10874,7 @@ function mapEventToSignal(event) {
10331
10874
  if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
10332
10875
  const statusCode = metadata?.statusCode;
10333
10876
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
10334
- const path48 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10877
+ const path51 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10335
10878
  return {
10336
10879
  targetKind: CLOUDFLARE_TARGET_KIND,
10337
10880
  targetName: scriptName,
@@ -10339,7 +10882,7 @@ function mapEventToSignal(event) {
10339
10882
  errorCount: isError ? 1 : 0,
10340
10883
  lastObservedIso: new Date(timestampMs).toISOString(),
10341
10884
  method,
10342
- ...path48 ? { path: path48 } : {},
10885
+ ...path51 ? { path: path51 } : {},
10343
10886
  ...typeof statusCode === "number" ? { statusCode } : {},
10344
10887
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
10345
10888
  };
@@ -10379,19 +10922,19 @@ function findTaggedWorkerFileNode(graph, workerName) {
10379
10922
  graph.forEachNode((id, attrs) => {
10380
10923
  if (found) return;
10381
10924
  const a = attrs;
10382
- if (a.type === import_types39.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10925
+ if (a.type === import_types42.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10383
10926
  found = id;
10384
10927
  }
10385
10928
  });
10386
10929
  return found;
10387
10930
  }
10388
- function findMatchingRouteNode(graph, serviceName, method, path48) {
10389
- const normalizedPath = normalizePathTemplate(path48);
10931
+ function findMatchingRouteNode(graph, serviceName, method, path51) {
10932
+ const normalizedPath = normalizePathTemplate(path51);
10390
10933
  let found = null;
10391
10934
  graph.forEachNode((id, attrs) => {
10392
10935
  if (found) return;
10393
10936
  const a = attrs;
10394
- if (a.type !== import_types39.NodeType.RouteNode || a.service !== serviceName) return;
10937
+ if (a.type !== import_types42.NodeType.RouteNode || a.service !== serviceName) return;
10395
10938
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
10396
10939
  const routeMethod = (a.method ?? "").toUpperCase();
10397
10940
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -10403,18 +10946,18 @@ function createCloudflareResolveTarget(config, graph) {
10403
10946
  return (signal) => {
10404
10947
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
10405
10948
  const scriptName = signal.targetName;
10406
- const { method, path: path48 } = signal;
10949
+ const { method, path: path51 } = signal;
10407
10950
  const resolveRouteGrain = (serviceName, wholeFileId) => {
10408
- if (!method || !path48) return wholeFileId;
10409
- return findMatchingRouteNode(graph, serviceName, method, path48) ?? wholeFileId;
10951
+ if (!method || !path51) return wholeFileId;
10952
+ return findMatchingRouteNode(graph, serviceName, method, path51) ?? wholeFileId;
10410
10953
  };
10411
10954
  const mapping = config.workers?.[scriptName];
10412
10955
  if (mapping) {
10413
- const wholeFileId = (0, import_types39.fileId)(mapping.service, mapping.entryFile);
10956
+ const wholeFileId = (0, import_types42.fileId)(mapping.service, mapping.entryFile);
10414
10957
  return {
10415
10958
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
10416
10959
  serviceName: mapping.service,
10417
- edgeType: import_types39.EdgeType.CALLS
10960
+ edgeType: import_types42.EdgeType.CALLS
10418
10961
  };
10419
10962
  }
10420
10963
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -10423,169 +10966,18 @@ function createCloudflareResolveTarget(config, graph) {
10423
10966
  return {
10424
10967
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
10425
10968
  serviceName: fileNode.service,
10426
- edgeType: import_types39.EdgeType.CALLS
10969
+ edgeType: import_types42.EdgeType.CALLS
10427
10970
  };
10428
10971
  }
10429
10972
  return {
10430
- targetNodeId: (0, import_types39.infraId)("cloudflare-worker", scriptName),
10973
+ targetNodeId: (0, import_types42.infraId)("cloudflare-worker", scriptName),
10431
10974
  serviceName: scriptName,
10432
- edgeType: import_types39.EdgeType.CALLS,
10975
+ edgeType: import_types42.EdgeType.CALLS,
10433
10976
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
10434
10977
  };
10435
10978
  };
10436
10979
  }
10437
10980
 
10438
- // src/connectors-config.ts
10439
- init_cjs_shims();
10440
- var import_node_os4 = __toESM(require("os"), 1);
10441
- var import_node_path45 = __toESM(require("path"), 1);
10442
- var import_node_fs26 = require("fs");
10443
- var CONNECTORS_CONFIG_VERSION = 1;
10444
- var EnvRefUnsetError = class extends Error {
10445
- ref;
10446
- varName;
10447
- constructor(ref, varName) {
10448
- super(`${ref} is unset`);
10449
- this.name = "EnvRefUnsetError";
10450
- this.ref = ref;
10451
- this.varName = varName;
10452
- }
10453
- };
10454
- function neatHome2() {
10455
- const override = process.env.NEAT_HOME;
10456
- if (override && override.length > 0) return import_node_path45.default.resolve(override);
10457
- return import_node_path45.default.join(import_node_os4.default.homedir(), ".neat");
10458
- }
10459
- function connectorsConfigPath(home = neatHome2()) {
10460
- return import_node_path45.default.join(home, "connectors.json");
10461
- }
10462
- var MODE_MASK_LOOSER_THAN_0600 = 63;
10463
- async function warnIfModeLooserThan0600(file) {
10464
- if (process.platform === "win32") return;
10465
- try {
10466
- const stat = await import_node_fs26.promises.stat(file);
10467
- if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
10468
- const mode = (stat.mode & 511).toString(8).padStart(3, "0");
10469
- console.warn(
10470
- `[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
10471
- );
10472
- }
10473
- } catch {
10474
- }
10475
- }
10476
- async function readConnectorsConfig(home = neatHome2()) {
10477
- const file = connectorsConfigPath(home);
10478
- let raw;
10479
- try {
10480
- raw = await import_node_fs26.promises.readFile(file, "utf8");
10481
- } catch (err) {
10482
- if (err.code === "ENOENT") {
10483
- return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
10484
- }
10485
- throw err;
10486
- }
10487
- await warnIfModeLooserThan0600(file);
10488
- let parsed;
10489
- try {
10490
- parsed = JSON.parse(raw);
10491
- } catch (err) {
10492
- throw new Error(`${file} is not valid JSON: ${err.message}`);
10493
- }
10494
- return validateConfig(parsed, file);
10495
- }
10496
- function validateConfig(parsed, file) {
10497
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10498
- throw new Error(`${file} must be a JSON object with a "connectors" array`);
10499
- }
10500
- const obj = parsed;
10501
- const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
10502
- if (typeof version !== "number" || !Number.isInteger(version)) {
10503
- throw new Error(`${file}: "version" must be an integer`);
10504
- }
10505
- const rawConnectors = obj.connectors;
10506
- if (!Array.isArray(rawConnectors)) {
10507
- throw new Error(`${file}: "connectors" must be an array`);
10508
- }
10509
- const connectors = rawConnectors.map((entry, i) => validateEntry(entry, i, file));
10510
- return { version, connectors };
10511
- }
10512
- function validateEntry(entry, index, file) {
10513
- const where = `${file}: connectors[${index}]`;
10514
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
10515
- throw new Error(`${where} must be an object`);
10516
- }
10517
- const e = entry;
10518
- const id = e.id;
10519
- if (typeof id !== "string" || id.length === 0) {
10520
- throw new Error(`${where}.id must be a non-empty string`);
10521
- }
10522
- const provider = e.provider;
10523
- if (typeof provider !== "string" || provider.length === 0) {
10524
- throw new Error(`${where}.provider must be a non-empty string`);
10525
- }
10526
- if (e.project !== void 0 && typeof e.project !== "string") {
10527
- throw new Error(`${where}.project must be a string when present`);
10528
- }
10529
- const credential = validateCredentialRef(e.credential, `${where}.credential`);
10530
- let options;
10531
- if (e.options !== void 0) {
10532
- if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
10533
- throw new Error(`${where}.options must be an object when present`);
10534
- }
10535
- options = e.options;
10536
- }
10537
- return {
10538
- id,
10539
- provider,
10540
- ...typeof e.project === "string" ? { project: e.project } : {},
10541
- credential,
10542
- ...options ? { options } : {}
10543
- };
10544
- }
10545
- function validateCredentialRef(raw, where) {
10546
- if (typeof raw === "string") {
10547
- if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
10548
- return raw;
10549
- }
10550
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
10551
- const fields = raw;
10552
- const keys = Object.keys(fields);
10553
- if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
10554
- for (const key of keys) {
10555
- const v = fields[key];
10556
- if (typeof v !== "string" || v.length === 0) {
10557
- throw new Error(`${where}.${key} must be a non-empty string`);
10558
- }
10559
- }
10560
- return fields;
10561
- }
10562
- throw new Error(`${where} must be a string or an object of strings`);
10563
- }
10564
- function resolveCredential(ref, env = process.env) {
10565
- if (typeof ref === "string") {
10566
- return { kind: "single", value: resolveRef(ref, env) };
10567
- }
10568
- const fields = {};
10569
- for (const [key, value] of Object.entries(ref)) {
10570
- fields[key] = resolveRef(value, env);
10571
- }
10572
- return { kind: "fields", fields };
10573
- }
10574
- function resolveRef(value, env) {
10575
- if (value.length > 1 && value.startsWith("$")) {
10576
- const varName = value.slice(1);
10577
- const resolved = env[varName];
10578
- if (resolved === void 0 || resolved.length === 0) {
10579
- throw new EnvRefUnsetError(value, varName);
10580
- }
10581
- return resolved;
10582
- }
10583
- return value;
10584
- }
10585
- function connectorMatchesProject(entry, project) {
10586
- return entry.project === void 0 || entry.project === project;
10587
- }
10588
-
10589
10981
  // src/connectors/registry.ts
10590
10982
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
10591
10983
  async function authProbe(input) {
@@ -10771,6 +11163,9 @@ function buildRegistration(entry, graph, env = process.env) {
10771
11163
  return {
10772
11164
  ok: true,
10773
11165
  registration: {
11166
+ // Carry the entry id so the daemon can key this connector's poll-status
11167
+ // records to it (ADR-136).
11168
+ id: entry.id,
10774
11169
  connector: built.connector,
10775
11170
  credentials,
10776
11171
  resolveTarget: built.resolveTarget,
@@ -10805,8 +11200,8 @@ init_auth();
10805
11200
 
10806
11201
  // src/unrouted.ts
10807
11202
  init_cjs_shims();
10808
- var import_node_fs27 = require("fs");
10809
- var import_node_path46 = __toESM(require("path"), 1);
11203
+ var import_node_fs30 = require("fs");
11204
+ var import_node_path49 = __toESM(require("path"), 1);
10810
11205
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
10811
11206
  return {
10812
11207
  timestamp: now.toISOString(),
@@ -10816,34 +11211,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
10816
11211
  };
10817
11212
  }
10818
11213
  async function appendUnroutedSpan(neatHome3, record) {
10819
- const target = import_node_path46.default.join(neatHome3, "errors.ndjson");
10820
- await import_node_fs27.promises.mkdir(neatHome3, { recursive: true });
10821
- await import_node_fs27.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
11214
+ const target = import_node_path49.default.join(neatHome3, "errors.ndjson");
11215
+ await import_node_fs30.promises.mkdir(neatHome3, { recursive: true });
11216
+ await import_node_fs30.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
10822
11217
  }
10823
11218
  function unroutedErrorsPath(neatHome3) {
10824
- return import_node_path46.default.join(neatHome3, "errors.ndjson");
11219
+ return import_node_path49.default.join(neatHome3, "errors.ndjson");
10825
11220
  }
10826
11221
 
10827
11222
  // src/daemon.ts
10828
- var import_types42 = require("@neat.is/types");
11223
+ var import_types45 = require("@neat.is/types");
10829
11224
  function daemonJsonPath(scanPath) {
10830
- return import_node_path47.default.join(scanPath, "neat-out", "daemon.json");
11225
+ return import_node_path50.default.join(scanPath, "neat-out", "daemon.json");
10831
11226
  }
10832
11227
  function daemonsDiscoveryDir(home) {
10833
11228
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
10834
- return import_node_path47.default.join(base, "daemons");
11229
+ return import_node_path50.default.join(base, "daemons");
10835
11230
  }
10836
11231
  function daemonDiscoveryPath(project, home) {
10837
- return import_node_path47.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
11232
+ return import_node_path50.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
10838
11233
  }
10839
11234
  function sanitizeDiscoveryName(project) {
10840
11235
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
10841
11236
  }
10842
11237
  function neatHomeFromEnv() {
10843
11238
  const env = process.env.NEAT_HOME;
10844
- if (env && env.length > 0) return import_node_path47.default.resolve(env);
11239
+ if (env && env.length > 0) return import_node_path50.default.resolve(env);
10845
11240
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10846
- return import_node_path47.default.join(home, ".neat");
11241
+ return import_node_path50.default.join(home, ".neat");
10847
11242
  }
10848
11243
  function resolveNeatVersion() {
10849
11244
  if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
@@ -10875,7 +11270,7 @@ async function clearDaemonRecord(record, home) {
10875
11270
  } catch {
10876
11271
  }
10877
11272
  try {
10878
- await import_node_fs28.promises.unlink(daemonDiscoveryPath(record.project, home));
11273
+ await import_node_fs31.promises.unlink(daemonDiscoveryPath(record.project, home));
10879
11274
  } catch {
10880
11275
  }
10881
11276
  }
@@ -10898,11 +11293,11 @@ function teardownSlot(slot) {
10898
11293
  }
10899
11294
  }
10900
11295
  function neatHomeFor(opts) {
10901
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path47.default.resolve(opts.neatHome);
11296
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path50.default.resolve(opts.neatHome);
10902
11297
  const env = process.env.NEAT_HOME;
10903
- if (env && env.length > 0) return import_node_path47.default.resolve(env);
11298
+ if (env && env.length > 0) return import_node_path50.default.resolve(env);
10904
11299
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10905
- return import_node_path47.default.join(home, ".neat");
11300
+ return import_node_path50.default.join(home, ".neat");
10906
11301
  }
10907
11302
  function routeSpanToProject(serviceName, projects) {
10908
11303
  if (!serviceName) return DEFAULT_PROJECT;
@@ -10946,13 +11341,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
10946
11341
  if (!serviceName) return true;
10947
11342
  if (serviceNameMatchesProject(serviceName, project)) return true;
10948
11343
  return graph.someNode(
10949
- (_id, attrs) => attrs.type === import_types42.NodeType.ServiceNode && attrs.name === serviceName
11344
+ (_id, attrs) => attrs.type === import_types45.NodeType.ServiceNode && attrs.name === serviceName
10950
11345
  );
10951
11346
  }
10952
11347
  async function bootstrapProject(entry, connectors = [], neatHome3) {
10953
- const paths = pathsForProject(entry.name, import_node_path47.default.join(entry.path, "neat-out"));
11348
+ const paths = pathsForProject(entry.name, import_node_path50.default.join(entry.path, "neat-out"));
10954
11349
  try {
10955
- const stat = await import_node_fs28.promises.stat(entry.path);
11350
+ const stat = await import_node_fs31.promises.stat(entry.path);
10956
11351
  if (!stat.isDirectory()) {
10957
11352
  throw new Error(`registered path ${entry.path} is not a directory`);
10958
11353
  }
@@ -11005,7 +11400,11 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
11005
11400
  { projectDir: entry.path, credentials: registration.credentials },
11006
11401
  graph,
11007
11402
  registration.resolveTarget,
11008
- { intervalMs: registration.intervalMs }
11403
+ // `connectorId` is threaded through so every tick lands in the
11404
+ // in-process status tracker the connector-status endpoint reads
11405
+ // (ADR-136). Undefined for a programmatic registration, which records
11406
+ // nothing.
11407
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
11009
11408
  )
11010
11409
  );
11011
11410
  const stopConnectors = () => {
@@ -11077,7 +11476,7 @@ async function startDaemon(opts = {}) {
11077
11476
  const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
11078
11477
  const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
11079
11478
  const singleProject = projectArg;
11080
- const singleProjectPath = singleProject && projectPathArg ? import_node_path47.default.resolve(projectPathArg) : null;
11479
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path50.default.resolve(projectPathArg) : null;
11081
11480
  if (singleProject && !singleProjectPath) {
11082
11481
  throw new Error(
11083
11482
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -11085,14 +11484,14 @@ async function startDaemon(opts = {}) {
11085
11484
  }
11086
11485
  if (!singleProject) {
11087
11486
  try {
11088
- await import_node_fs28.promises.access(regPath);
11487
+ await import_node_fs31.promises.access(regPath);
11089
11488
  } catch {
11090
11489
  throw new Error(
11091
11490
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
11092
11491
  );
11093
11492
  }
11094
11493
  }
11095
- const pidPath = import_node_path47.default.join(home, "neatd.pid");
11494
+ const pidPath = import_node_path50.default.join(home, "neatd.pid");
11096
11495
  await writeAtomically(pidPath, `${process.pid}
11097
11496
  `);
11098
11497
  const slots = /* @__PURE__ */ new Map();
@@ -11274,7 +11673,12 @@ async function startDaemon(opts = {}) {
11274
11673
  // only this project (the dashboard pins to it), and the daemon-wide
11275
11674
  // `/health` carries it at the top level for the spawn-reuse identity
11276
11675
  // check. Absent for the legacy multi-project daemon.
11277
- singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
11676
+ singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0,
11677
+ // ADR-136 — the connector-status endpoint reads ~/.neat/connectors.json
11678
+ // through the same resolved home the slot bootstrap read it from, so a
11679
+ // daemon given an explicit NEAT_HOME serves status for the same file it
11680
+ // polls.
11681
+ connectorsHome: home
11278
11682
  });
11279
11683
  restAddress = await restApp.listen({ port: restPort, host });
11280
11684
  console.log(
@@ -11286,7 +11690,7 @@ async function startDaemon(opts = {}) {
11286
11690
  }
11287
11691
  if (restApp) await restApp.close().catch(() => {
11288
11692
  });
11289
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11693
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11290
11694
  });
11291
11695
  throw new Error(
11292
11696
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -11412,7 +11816,7 @@ async function startDaemon(opts = {}) {
11412
11816
  });
11413
11817
  if (otlpApp) await otlpApp.close().catch(() => {
11414
11818
  });
11415
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11819
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11416
11820
  });
11417
11821
  throw new Error(
11418
11822
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -11447,7 +11851,7 @@ async function startDaemon(opts = {}) {
11447
11851
  });
11448
11852
  if (otlpApp) await otlpApp.close().catch(() => {
11449
11853
  });
11450
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11854
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11451
11855
  });
11452
11856
  throw new Error(
11453
11857
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -11494,9 +11898,9 @@ async function startDaemon(opts = {}) {
11494
11898
  let registryWatcher = null;
11495
11899
  let reloadTimer = null;
11496
11900
  if (!singleProject) try {
11497
- const regDir = import_node_path47.default.dirname(regPath);
11498
- const regBase = import_node_path47.default.basename(regPath);
11499
- registryWatcher = (0, import_node_fs28.watch)(regDir, (_eventType, filename) => {
11901
+ const regDir = import_node_path50.default.dirname(regPath);
11902
+ const regBase = import_node_path50.default.basename(regPath);
11903
+ registryWatcher = (0, import_node_fs31.watch)(regDir, (_eventType, filename) => {
11500
11904
  if (filename !== null && filename !== regBase) return;
11501
11905
  if (reloadTimer) clearTimeout(reloadTimer);
11502
11906
  reloadTimer = setTimeout(() => {
@@ -11545,7 +11949,7 @@ async function startDaemon(opts = {}) {
11545
11949
  if (daemonRecord) {
11546
11950
  await clearDaemonRecord(daemonRecord, home);
11547
11951
  }
11548
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11952
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11549
11953
  });
11550
11954
  };
11551
11955
  return {