@neat.is/core 0.4.28-dev.20260712 → 0.4.28-dev.20260714

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",
@@ -9009,9 +9471,11 @@ async function buildApi(opts) {
9009
9471
  trustProxy,
9010
9472
  publicRead
9011
9473
  });
9474
+ const requiresAuth = authToken !== void 0 && authToken.length > 0 && trustProxy !== true;
9012
9475
  app.get("/api/config", async () => ({
9013
9476
  publicRead: publicRead === true,
9014
- authProxy: trustProxy === true
9477
+ authProxy: trustProxy === true,
9478
+ requiresAuth
9015
9479
  }));
9016
9480
  const startedAt = opts.startedAt ?? Date.now();
9017
9481
  const registry = buildLegacyRegistry(opts);
@@ -9041,7 +9505,8 @@ async function buildApi(opts) {
9041
9505
  staleEventsPathFor,
9042
9506
  policyFilePathFor,
9043
9507
  bootstrap: opts.bootstrap,
9044
- singleProject: opts.singleProject?.name
9508
+ singleProject: opts.singleProject?.name,
9509
+ connectorsHome: opts.connectorsHome
9045
9510
  };
9046
9511
  app.get("/health", async () => {
9047
9512
  const uptimeMs = Date.now() - startedAt;
@@ -9126,8 +9591,8 @@ init_otel_grpc();
9126
9591
 
9127
9592
  // src/daemon.ts
9128
9593
  init_cjs_shims();
9129
- var import_node_fs28 = require("fs");
9130
- var import_node_path47 = __toESM(require("path"), 1);
9594
+ var import_node_fs31 = require("fs");
9595
+ var import_node_path50 = __toESM(require("path"), 1);
9131
9596
  var import_node_module = require("module");
9132
9597
  init_otel();
9133
9598
 
@@ -9191,16 +9656,31 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
9191
9656
  let stopped = false;
9192
9657
  let since = ctx.since;
9193
9658
  const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9659
+ const connectorId = options.connectorId;
9194
9660
  const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
9195
9661
  const tick = () => {
9196
9662
  if (stopped) return;
9197
9663
  void (async () => {
9198
9664
  const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
9199
9665
  try {
9200
- await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
9666
+ const result = await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
9201
9667
  since = tickStartedAt;
9668
+ if (connectorId) {
9669
+ recordConnectorPoll(connectorId, {
9670
+ outcome: "ok",
9671
+ at: tickStartedAt,
9672
+ signalsLastPoll: result.signalCount
9673
+ });
9674
+ }
9202
9675
  } catch (err) {
9203
9676
  onError(err);
9677
+ if (connectorId) {
9678
+ recordConnectorPoll(connectorId, {
9679
+ outcome: "error",
9680
+ at: tickStartedAt,
9681
+ error: sanitizePollError(err)
9682
+ });
9683
+ }
9204
9684
  }
9205
9685
  })();
9206
9686
  };
@@ -9499,6 +9979,30 @@ function buildEdgeLogsQuery(limit) {
9499
9979
  `limit ${safeLimit}`
9500
9980
  ].join("\n");
9501
9981
  }
9982
+ function logsAllHttpFailureMessage(status2) {
9983
+ if (status2 === 401 || status2 === 403) {
9984
+ return `supabase connector: logs.all request rejected (HTTP ${status2}). Check the Management API token, its analytics read scope, and --api-project-ref.`;
9985
+ }
9986
+ if (status2 === 404) {
9987
+ return "supabase connector: logs.all project not found (HTTP 404). Check --api-project-ref.";
9988
+ }
9989
+ if (status2 === 429) {
9990
+ return "supabase connector: logs.all rate-limited (HTTP 429). The connector will retry on the next poll; reduce poll cadence if this persists.";
9991
+ }
9992
+ if (status2 === 400) {
9993
+ return "supabase connector: logs.all query rejected (HTTP 400). Provider details redacted; confirm the live log-query dialect before shipping this connector.";
9994
+ }
9995
+ return `supabase connector: logs.all request failed (HTTP ${status2}); provider details redacted.`;
9996
+ }
9997
+ function providerErrorDetails(error) {
9998
+ if (!error || typeof error === "string") return "";
9999
+ const parts = [];
10000
+ if (typeof error.code === "number") parts.push(`code ${error.code}`);
10001
+ if (typeof error.status === "string" && /^[A-Z0-9_.-]+$/.test(error.status)) {
10002
+ parts.push(`status ${error.status}`);
10003
+ }
10004
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
10005
+ }
9502
10006
  async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
9503
10007
  const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
9504
10008
  const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
@@ -9513,12 +10017,18 @@ async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl
9513
10017
  { provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
9514
10018
  );
9515
10019
  if (!res.ok) {
9516
- throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
10020
+ throw new Error(logsAllHttpFailureMessage(res.status));
10021
+ }
10022
+ let body;
10023
+ try {
10024
+ body = await res.json();
10025
+ } catch {
10026
+ throw new Error("supabase connector: logs.all returned invalid JSON; provider details redacted.");
9517
10027
  }
9518
- const body = await res.json();
9519
10028
  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})`);
10029
+ throw new Error(
10030
+ `supabase connector: logs.all returned a provider error${providerErrorDetails(body.error)}; provider message redacted.`
10031
+ );
9522
10032
  }
9523
10033
  return body.result ?? [];
9524
10034
  }
@@ -9550,10 +10060,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
9550
10060
  // src/connectors/supabase/map.ts
9551
10061
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
9552
10062
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
9553
- function targetFromRestPath(path48) {
9554
- const rpcMatch = REST_RPC_PATH_RE.exec(path48);
10063
+ function targetFromRestPath(path51) {
10064
+ const rpcMatch = REST_RPC_PATH_RE.exec(path51);
9555
10065
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
9556
- const tableMatch = REST_TABLE_PATH_RE.exec(path48);
10066
+ const tableMatch = REST_TABLE_PATH_RE.exec(path51);
9557
10067
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
9558
10068
  return null;
9559
10069
  }
@@ -9656,19 +10166,19 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
9656
10166
 
9657
10167
  // src/connectors/supabase/resolve.ts
9658
10168
  init_cjs_shims();
9659
- var import_types32 = require("@neat.is/types");
10169
+ var import_types35 = require("@neat.is/types");
9660
10170
  function createSupabaseResolveTarget(graph, config) {
9661
10171
  return (signal, _ctx) => {
9662
10172
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
9663
10173
  return null;
9664
10174
  }
9665
- const subResourceId = (0, import_types32.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
10175
+ const subResourceId = (0, import_types35.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
9666
10176
  if (graph.hasNode(subResourceId)) {
9667
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
10177
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types35.EdgeType.CALLS };
9668
10178
  }
9669
- const projectLevelId = (0, import_types32.infraId)("supabase", config.nodeRef);
10179
+ const projectLevelId = (0, import_types35.infraId)("supabase", config.nodeRef);
9670
10180
  if (graph.hasNode(projectLevelId)) {
9671
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
10181
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types35.EdgeType.CALLS };
9672
10182
  }
9673
10183
  return null;
9674
10184
  };
@@ -9676,6 +10186,35 @@ function createSupabaseResolveTarget(graph, config) {
9676
10186
 
9677
10187
  // src/connectors/supabase/index.ts
9678
10188
  var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
10189
+ function errorCode(err) {
10190
+ const code = err?.code;
10191
+ return typeof code === "string" && code.length > 0 ? code : void 0;
10192
+ }
10193
+ function describeSupabasePostgresSurfaceFailure(projectRef, err) {
10194
+ const code = errorCode(err);
10195
+ let reason;
10196
+ switch (code) {
10197
+ case "42501":
10198
+ reason = "permission denied; grant pg_read_all_stats to the configured Postgres role";
10199
+ break;
10200
+ case "42P01":
10201
+ case "42704":
10202
+ reason = "pg_stat_statements is not enabled or visible to the configured Postgres role";
10203
+ break;
10204
+ case "28P01":
10205
+ case "28000":
10206
+ reason = "Postgres credential rejected";
10207
+ break;
10208
+ case "3D000":
10209
+ reason = "database not found";
10210
+ break;
10211
+ default: {
10212
+ const name = err instanceof Error && err.name ? err.name : "Error";
10213
+ reason = code ? `${name} ${code}` : name;
10214
+ }
10215
+ }
10216
+ return `supabase connector: pg_stat_statements surface unavailable for project ${projectRef} (${reason}); continuing with Management API log surface.`;
10217
+ }
9679
10218
  var SupabaseConnector = class {
9680
10219
  // `deps.fetchPgStatStatements` defaults to the real Postgres-backed
9681
10220
  // implementation; tests override it to exercise the "both surfaces
@@ -9707,12 +10246,18 @@ var SupabaseConnector = class {
9707
10246
  const signals = mapEdgeLogRowsToSignals(logRows);
9708
10247
  if (creds.postgresConnectionString) {
9709
10248
  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()));
10249
+ try {
10250
+ const statementRows = await fetchStatements(
10251
+ creds.postgresConnectionString,
10252
+ this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
10253
+ this.config.apiProjectRef
10254
+ );
10255
+ signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
10256
+ } catch (err) {
10257
+ const summary = describeSupabasePostgresSurfaceFailure(this.config.apiProjectRef, err);
10258
+ if (this.deps.onPostgresSurfaceError) this.deps.onPostgresSurfaceError(err, summary);
10259
+ else console.warn(summary);
10260
+ }
9716
10261
  }
9717
10262
  return signals;
9718
10263
  }
@@ -9726,7 +10271,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
9726
10271
 
9727
10272
  // src/connectors/railway/index.ts
9728
10273
  init_cjs_shims();
9729
- var import_types36 = require("@neat.is/types");
10274
+ var import_types39 = require("@neat.is/types");
9730
10275
 
9731
10276
  // src/connectors/railway/client.ts
9732
10277
  init_cjs_shims();
@@ -9871,7 +10416,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
9871
10416
  const out = [];
9872
10417
  graph.forEachNode((_id, attrs) => {
9873
10418
  const node = attrs;
9874
- if (node.type !== import_types36.NodeType.RouteNode) return;
10419
+ if (node.type !== import_types39.NodeType.RouteNode) return;
9875
10420
  const route = attrs;
9876
10421
  if (route.service !== serviceName) return;
9877
10422
  out.push({
@@ -9967,12 +10512,12 @@ function createRailwayResolveTarget(config) {
9967
10512
  const serviceName = config.serviceNameById[config.serviceId];
9968
10513
  if (!serviceName) return null;
9969
10514
  if (signal.targetKind === ROUTE_TARGET_KIND) {
9970
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types36.EdgeType.CALLS };
10515
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types39.EdgeType.CALLS };
9971
10516
  }
9972
10517
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
9973
10518
  const peerName = config.serviceNameById[signal.targetName];
9974
10519
  if (!peerName) return null;
9975
- return { targetNodeId: (0, import_types36.serviceId)(peerName), serviceName, edgeType: import_types36.EdgeType.CONNECTS_TO };
10520
+ return { targetNodeId: (0, import_types39.serviceId)(peerName), serviceName, edgeType: import_types39.EdgeType.CONNECTS_TO };
9976
10521
  }
9977
10522
  return null;
9978
10523
  };
@@ -10087,9 +10632,9 @@ function parseFirebaseTargetName(targetName) {
10087
10632
  const secondSep = rest.indexOf(FIELD_SEP);
10088
10633
  if (secondSep === -1) return null;
10089
10634
  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 };
10635
+ const path51 = rest.slice(secondSep + 1);
10636
+ if (!resourceName || !method || !path51) return null;
10637
+ return { resourceName, method, path: path51 };
10093
10638
  }
10094
10639
  function resourceNameFor(type, labels) {
10095
10640
  if (!labels) return null;
@@ -10126,14 +10671,14 @@ function mapLogEntryToSignal(entry) {
10126
10671
  if (!req) return null;
10127
10672
  if (!req.requestMethod) return null;
10128
10673
  const method = req.requestMethod.toUpperCase();
10129
- const path48 = pathFromRequestUrl(req.requestUrl);
10130
- if (path48 === null) return null;
10674
+ const path51 = pathFromRequestUrl(req.requestUrl);
10675
+ if (path51 === null) return null;
10131
10676
  const timestamp = entry.timestamp;
10132
10677
  if (!timestamp) return null;
10133
10678
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
10134
10679
  return {
10135
10680
  targetKind: resourceType,
10136
- targetName: packFirebaseTargetName({ resourceName, method, path: path48 }),
10681
+ targetName: packFirebaseTargetName({ resourceName, method, path: path51 }),
10137
10682
  callCount: 1,
10138
10683
  errorCount: isError ? 1 : 0,
10139
10684
  lastObservedIso: timestamp
@@ -10150,7 +10695,7 @@ function mapLogEntriesToSignals(entries) {
10150
10695
 
10151
10696
  // src/connectors/firebase/resolve.ts
10152
10697
  init_cjs_shims();
10153
- var import_types37 = require("@neat.is/types");
10698
+ var import_types40 = require("@neat.is/types");
10154
10699
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
10155
10700
  switch (resourceType) {
10156
10701
  case "cloud_function":
@@ -10165,7 +10710,7 @@ function routeEntriesFor(graph, serviceName) {
10165
10710
  const entries = [];
10166
10711
  graph.forEachNode((_id, attrs) => {
10167
10712
  const node = attrs;
10168
- if (node.type !== import_types37.NodeType.RouteNode) return;
10713
+ if (node.type !== import_types40.NodeType.RouteNode) return;
10169
10714
  const route = attrs;
10170
10715
  if (route.service !== serviceName) return;
10171
10716
  entries.push({
@@ -10197,7 +10742,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
10197
10742
  return {
10198
10743
  targetNodeId: match.routeNodeId,
10199
10744
  serviceName,
10200
- edgeType: import_types37.EdgeType.CALLS
10745
+ edgeType: import_types40.EdgeType.CALLS
10201
10746
  };
10202
10747
  };
10203
10748
  }
@@ -10224,7 +10769,7 @@ init_cjs_shims();
10224
10769
 
10225
10770
  // src/connectors/cloudflare/connector.ts
10226
10771
  init_cjs_shims();
10227
- var import_types39 = require("@neat.is/types");
10772
+ var import_types42 = require("@neat.is/types");
10228
10773
 
10229
10774
  // src/connectors/cloudflare/client.ts
10230
10775
  init_cjs_shims();
@@ -10331,7 +10876,7 @@ function mapEventToSignal(event) {
10331
10876
  if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
10332
10877
  const statusCode = metadata?.statusCode;
10333
10878
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
10334
- const path48 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10879
+ const path51 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10335
10880
  return {
10336
10881
  targetKind: CLOUDFLARE_TARGET_KIND,
10337
10882
  targetName: scriptName,
@@ -10339,7 +10884,7 @@ function mapEventToSignal(event) {
10339
10884
  errorCount: isError ? 1 : 0,
10340
10885
  lastObservedIso: new Date(timestampMs).toISOString(),
10341
10886
  method,
10342
- ...path48 ? { path: path48 } : {},
10887
+ ...path51 ? { path: path51 } : {},
10343
10888
  ...typeof statusCode === "number" ? { statusCode } : {},
10344
10889
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
10345
10890
  };
@@ -10379,19 +10924,19 @@ function findTaggedWorkerFileNode(graph, workerName) {
10379
10924
  graph.forEachNode((id, attrs) => {
10380
10925
  if (found) return;
10381
10926
  const a = attrs;
10382
- if (a.type === import_types39.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10927
+ if (a.type === import_types42.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10383
10928
  found = id;
10384
10929
  }
10385
10930
  });
10386
10931
  return found;
10387
10932
  }
10388
- function findMatchingRouteNode(graph, serviceName, method, path48) {
10389
- const normalizedPath = normalizePathTemplate(path48);
10933
+ function findMatchingRouteNode(graph, serviceName, method, path51) {
10934
+ const normalizedPath = normalizePathTemplate(path51);
10390
10935
  let found = null;
10391
10936
  graph.forEachNode((id, attrs) => {
10392
10937
  if (found) return;
10393
10938
  const a = attrs;
10394
- if (a.type !== import_types39.NodeType.RouteNode || a.service !== serviceName) return;
10939
+ if (a.type !== import_types42.NodeType.RouteNode || a.service !== serviceName) return;
10395
10940
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
10396
10941
  const routeMethod = (a.method ?? "").toUpperCase();
10397
10942
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -10403,18 +10948,18 @@ function createCloudflareResolveTarget(config, graph) {
10403
10948
  return (signal) => {
10404
10949
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
10405
10950
  const scriptName = signal.targetName;
10406
- const { method, path: path48 } = signal;
10951
+ const { method, path: path51 } = signal;
10407
10952
  const resolveRouteGrain = (serviceName, wholeFileId) => {
10408
- if (!method || !path48) return wholeFileId;
10409
- return findMatchingRouteNode(graph, serviceName, method, path48) ?? wholeFileId;
10953
+ if (!method || !path51) return wholeFileId;
10954
+ return findMatchingRouteNode(graph, serviceName, method, path51) ?? wholeFileId;
10410
10955
  };
10411
10956
  const mapping = config.workers?.[scriptName];
10412
10957
  if (mapping) {
10413
- const wholeFileId = (0, import_types39.fileId)(mapping.service, mapping.entryFile);
10958
+ const wholeFileId = (0, import_types42.fileId)(mapping.service, mapping.entryFile);
10414
10959
  return {
10415
10960
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
10416
10961
  serviceName: mapping.service,
10417
- edgeType: import_types39.EdgeType.CALLS
10962
+ edgeType: import_types42.EdgeType.CALLS
10418
10963
  };
10419
10964
  }
10420
10965
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -10423,169 +10968,18 @@ function createCloudflareResolveTarget(config, graph) {
10423
10968
  return {
10424
10969
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
10425
10970
  serviceName: fileNode.service,
10426
- edgeType: import_types39.EdgeType.CALLS
10971
+ edgeType: import_types42.EdgeType.CALLS
10427
10972
  };
10428
10973
  }
10429
10974
  return {
10430
- targetNodeId: (0, import_types39.infraId)("cloudflare-worker", scriptName),
10975
+ targetNodeId: (0, import_types42.infraId)("cloudflare-worker", scriptName),
10431
10976
  serviceName: scriptName,
10432
- edgeType: import_types39.EdgeType.CALLS,
10977
+ edgeType: import_types42.EdgeType.CALLS,
10433
10978
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
10434
10979
  };
10435
10980
  };
10436
10981
  }
10437
10982
 
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
10983
  // src/connectors/registry.ts
10590
10984
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
10591
10985
  async function authProbe(input) {
@@ -10771,6 +11165,9 @@ function buildRegistration(entry, graph, env = process.env) {
10771
11165
  return {
10772
11166
  ok: true,
10773
11167
  registration: {
11168
+ // Carry the entry id so the daemon can key this connector's poll-status
11169
+ // records to it (ADR-136).
11170
+ id: entry.id,
10774
11171
  connector: built.connector,
10775
11172
  credentials,
10776
11173
  resolveTarget: built.resolveTarget,
@@ -10805,8 +11202,8 @@ init_auth();
10805
11202
 
10806
11203
  // src/unrouted.ts
10807
11204
  init_cjs_shims();
10808
- var import_node_fs27 = require("fs");
10809
- var import_node_path46 = __toESM(require("path"), 1);
11205
+ var import_node_fs30 = require("fs");
11206
+ var import_node_path49 = __toESM(require("path"), 1);
10810
11207
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
10811
11208
  return {
10812
11209
  timestamp: now.toISOString(),
@@ -10816,34 +11213,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
10816
11213
  };
10817
11214
  }
10818
11215
  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");
11216
+ const target = import_node_path49.default.join(neatHome3, "errors.ndjson");
11217
+ await import_node_fs30.promises.mkdir(neatHome3, { recursive: true });
11218
+ await import_node_fs30.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
10822
11219
  }
10823
11220
  function unroutedErrorsPath(neatHome3) {
10824
- return import_node_path46.default.join(neatHome3, "errors.ndjson");
11221
+ return import_node_path49.default.join(neatHome3, "errors.ndjson");
10825
11222
  }
10826
11223
 
10827
11224
  // src/daemon.ts
10828
- var import_types42 = require("@neat.is/types");
11225
+ var import_types45 = require("@neat.is/types");
10829
11226
  function daemonJsonPath(scanPath) {
10830
- return import_node_path47.default.join(scanPath, "neat-out", "daemon.json");
11227
+ return import_node_path50.default.join(scanPath, "neat-out", "daemon.json");
10831
11228
  }
10832
11229
  function daemonsDiscoveryDir(home) {
10833
11230
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
10834
- return import_node_path47.default.join(base, "daemons");
11231
+ return import_node_path50.default.join(base, "daemons");
10835
11232
  }
10836
11233
  function daemonDiscoveryPath(project, home) {
10837
- return import_node_path47.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
11234
+ return import_node_path50.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
10838
11235
  }
10839
11236
  function sanitizeDiscoveryName(project) {
10840
11237
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
10841
11238
  }
10842
11239
  function neatHomeFromEnv() {
10843
11240
  const env = process.env.NEAT_HOME;
10844
- if (env && env.length > 0) return import_node_path47.default.resolve(env);
11241
+ if (env && env.length > 0) return import_node_path50.default.resolve(env);
10845
11242
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10846
- return import_node_path47.default.join(home, ".neat");
11243
+ return import_node_path50.default.join(home, ".neat");
10847
11244
  }
10848
11245
  function resolveNeatVersion() {
10849
11246
  if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
@@ -10875,7 +11272,7 @@ async function clearDaemonRecord(record, home) {
10875
11272
  } catch {
10876
11273
  }
10877
11274
  try {
10878
- await import_node_fs28.promises.unlink(daemonDiscoveryPath(record.project, home));
11275
+ await import_node_fs31.promises.unlink(daemonDiscoveryPath(record.project, home));
10879
11276
  } catch {
10880
11277
  }
10881
11278
  }
@@ -10898,11 +11295,11 @@ function teardownSlot(slot) {
10898
11295
  }
10899
11296
  }
10900
11297
  function neatHomeFor(opts) {
10901
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path47.default.resolve(opts.neatHome);
11298
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path50.default.resolve(opts.neatHome);
10902
11299
  const env = process.env.NEAT_HOME;
10903
- if (env && env.length > 0) return import_node_path47.default.resolve(env);
11300
+ if (env && env.length > 0) return import_node_path50.default.resolve(env);
10904
11301
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10905
- return import_node_path47.default.join(home, ".neat");
11302
+ return import_node_path50.default.join(home, ".neat");
10906
11303
  }
10907
11304
  function routeSpanToProject(serviceName, projects) {
10908
11305
  if (!serviceName) return DEFAULT_PROJECT;
@@ -10946,13 +11343,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
10946
11343
  if (!serviceName) return true;
10947
11344
  if (serviceNameMatchesProject(serviceName, project)) return true;
10948
11345
  return graph.someNode(
10949
- (_id, attrs) => attrs.type === import_types42.NodeType.ServiceNode && attrs.name === serviceName
11346
+ (_id, attrs) => attrs.type === import_types45.NodeType.ServiceNode && attrs.name === serviceName
10950
11347
  );
10951
11348
  }
10952
11349
  async function bootstrapProject(entry, connectors = [], neatHome3) {
10953
- const paths = pathsForProject(entry.name, import_node_path47.default.join(entry.path, "neat-out"));
11350
+ const paths = pathsForProject(entry.name, import_node_path50.default.join(entry.path, "neat-out"));
10954
11351
  try {
10955
- const stat = await import_node_fs28.promises.stat(entry.path);
11352
+ const stat = await import_node_fs31.promises.stat(entry.path);
10956
11353
  if (!stat.isDirectory()) {
10957
11354
  throw new Error(`registered path ${entry.path} is not a directory`);
10958
11355
  }
@@ -11005,7 +11402,11 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
11005
11402
  { projectDir: entry.path, credentials: registration.credentials },
11006
11403
  graph,
11007
11404
  registration.resolveTarget,
11008
- { intervalMs: registration.intervalMs }
11405
+ // `connectorId` is threaded through so every tick lands in the
11406
+ // in-process status tracker the connector-status endpoint reads
11407
+ // (ADR-136). Undefined for a programmatic registration, which records
11408
+ // nothing.
11409
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
11009
11410
  )
11010
11411
  );
11011
11412
  const stopConnectors = () => {
@@ -11077,7 +11478,7 @@ async function startDaemon(opts = {}) {
11077
11478
  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
11479
  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
11480
  const singleProject = projectArg;
11080
- const singleProjectPath = singleProject && projectPathArg ? import_node_path47.default.resolve(projectPathArg) : null;
11481
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path50.default.resolve(projectPathArg) : null;
11081
11482
  if (singleProject && !singleProjectPath) {
11082
11483
  throw new Error(
11083
11484
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -11085,14 +11486,14 @@ async function startDaemon(opts = {}) {
11085
11486
  }
11086
11487
  if (!singleProject) {
11087
11488
  try {
11088
- await import_node_fs28.promises.access(regPath);
11489
+ await import_node_fs31.promises.access(regPath);
11089
11490
  } catch {
11090
11491
  throw new Error(
11091
11492
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
11092
11493
  );
11093
11494
  }
11094
11495
  }
11095
- const pidPath = import_node_path47.default.join(home, "neatd.pid");
11496
+ const pidPath = import_node_path50.default.join(home, "neatd.pid");
11096
11497
  await writeAtomically(pidPath, `${process.pid}
11097
11498
  `);
11098
11499
  const slots = /* @__PURE__ */ new Map();
@@ -11274,7 +11675,12 @@ async function startDaemon(opts = {}) {
11274
11675
  // only this project (the dashboard pins to it), and the daemon-wide
11275
11676
  // `/health` carries it at the top level for the spawn-reuse identity
11276
11677
  // check. Absent for the legacy multi-project daemon.
11277
- singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
11678
+ singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0,
11679
+ // ADR-136 — the connector-status endpoint reads ~/.neat/connectors.json
11680
+ // through the same resolved home the slot bootstrap read it from, so a
11681
+ // daemon given an explicit NEAT_HOME serves status for the same file it
11682
+ // polls.
11683
+ connectorsHome: home
11278
11684
  });
11279
11685
  restAddress = await restApp.listen({ port: restPort, host });
11280
11686
  console.log(
@@ -11286,7 +11692,7 @@ async function startDaemon(opts = {}) {
11286
11692
  }
11287
11693
  if (restApp) await restApp.close().catch(() => {
11288
11694
  });
11289
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11695
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11290
11696
  });
11291
11697
  throw new Error(
11292
11698
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -11412,7 +11818,7 @@ async function startDaemon(opts = {}) {
11412
11818
  });
11413
11819
  if (otlpApp) await otlpApp.close().catch(() => {
11414
11820
  });
11415
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11821
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11416
11822
  });
11417
11823
  throw new Error(
11418
11824
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -11447,7 +11853,7 @@ async function startDaemon(opts = {}) {
11447
11853
  });
11448
11854
  if (otlpApp) await otlpApp.close().catch(() => {
11449
11855
  });
11450
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11856
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11451
11857
  });
11452
11858
  throw new Error(
11453
11859
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -11494,9 +11900,9 @@ async function startDaemon(opts = {}) {
11494
11900
  let registryWatcher = null;
11495
11901
  let reloadTimer = null;
11496
11902
  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) => {
11903
+ const regDir = import_node_path50.default.dirname(regPath);
11904
+ const regBase = import_node_path50.default.basename(regPath);
11905
+ registryWatcher = (0, import_node_fs31.watch)(regDir, (_eventType, filename) => {
11500
11906
  if (filename !== null && filename !== regBase) return;
11501
11907
  if (reloadTimer) clearTimeout(reloadTimer);
11502
11908
  reloadTimer = setTimeout(() => {
@@ -11545,7 +11951,7 @@ async function startDaemon(opts = {}) {
11545
11951
  if (daemonRecord) {
11546
11952
  await clearDaemonRecord(daemonRecord, home);
11547
11953
  }
11548
- await import_node_fs28.promises.unlink(pidPath).catch(() => {
11954
+ await import_node_fs31.promises.unlink(pidPath).catch(() => {
11549
11955
  });
11550
11956
  };
11551
11957
  return {