@neat.is/core 0.4.28-dev.20260707 → 0.4.28-dev.20260709

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
@@ -54,15 +54,16 @@ function mountBearerAuth(app, opts) {
54
54
  if (!opts.token || opts.token.length === 0) return;
55
55
  if (opts.trustProxy) return;
56
56
  const expected = Buffer.from(opts.token, "utf8");
57
- const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
57
+ const exactUnauthPaths = /* @__PURE__ */ new Set([
58
+ ...DEFAULT_UNAUTH_PATHS,
59
+ ...opts.extraUnauthenticatedSuffixes ?? []
60
+ ]);
58
61
  const publicRead = opts.publicRead === true;
59
62
  app.addHook("preHandler", (req, reply, done) => {
60
- const path46 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
- for (const suffix of suffixes) {
62
- if (path46 === suffix || path46.endsWith(suffix)) {
63
- done();
64
- return;
65
- }
63
+ const path48 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
64
+ if (exactUnauthPaths.has(path48) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path48)) {
65
+ done();
66
+ return;
66
67
  }
67
68
  if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
68
69
  done();
@@ -97,7 +98,7 @@ function readAuthEnv(env = process.env) {
97
98
  publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
98
99
  };
99
100
  }
100
- var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
101
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
101
102
  var init_auth = __esm({
102
103
  "src/auth.ts"() {
103
104
  "use strict";
@@ -118,12 +119,15 @@ var init_auth = __esm({
118
119
  }
119
120
  };
120
121
  PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
121
- DEFAULT_UNAUTH_SUFFIXES = [
122
+ DEFAULT_UNAUTH_PATHS = [
122
123
  "/health",
123
124
  "/healthz",
124
125
  "/readyz",
125
126
  "/api/config"
126
127
  ];
128
+ PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
129
+ `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
130
+ );
127
131
  }
128
132
  });
129
133
 
@@ -189,8 +193,8 @@ function reshapeGrpcRequest(req) {
189
193
  };
190
194
  }
191
195
  function resolveProtoRoot() {
192
- const here = import_node_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
193
- return import_node_path42.default.resolve(here, "..", "proto");
196
+ const here = import_node_path43.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
197
+ return import_node_path43.default.resolve(here, "..", "proto");
194
198
  }
195
199
  function loadTraceService() {
196
200
  const protoRoot = resolveProtoRoot();
@@ -258,13 +262,13 @@ async function startOtelGrpcReceiver(opts) {
258
262
  })
259
263
  };
260
264
  }
261
- var import_node_url, import_node_path42, import_node_crypto2, grpc, protoLoader;
265
+ var import_node_url, import_node_path43, import_node_crypto2, grpc, protoLoader;
262
266
  var init_otel_grpc = __esm({
263
267
  "src/otel-grpc.ts"() {
264
268
  "use strict";
265
269
  init_cjs_shims();
266
270
  import_node_url = require("url");
267
- import_node_path42 = __toESM(require("path"), 1);
271
+ import_node_path43 = __toESM(require("path"), 1);
268
272
  import_node_crypto2 = require("crypto");
269
273
  grpc = __toESM(require("@grpc/grpc-js"), 1);
270
274
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -357,8 +361,8 @@ function websocketChannelPathOf(attrs) {
357
361
  const v = attrs[key];
358
362
  if (typeof v === "string" && v.length > 0) {
359
363
  const q = v.indexOf("?");
360
- const path46 = q === -1 ? v : v.slice(0, q);
361
- if (path46.length > 0) return path46;
364
+ const path48 = q === -1 ? v : v.slice(0, q);
365
+ if (path48.length > 0) return path48;
362
366
  }
363
367
  }
364
368
  return void 0;
@@ -412,10 +416,10 @@ function parseOtlpRequest(body) {
412
416
  return out;
413
417
  }
414
418
  function loadProtoRoot() {
415
- const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
416
- const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
419
+ const here = import_node_path44.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
420
+ const protoRoot = import_node_path44.default.resolve(here, "..", "proto");
417
421
  const root = new import_protobufjs.default.Root();
418
- root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
422
+ root.resolvePath = (_origin, target) => import_node_path44.default.resolve(protoRoot, target);
419
423
  root.loadSync(
420
424
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
421
425
  { keepCase: true }
@@ -651,12 +655,12 @@ function logSpanHandler(span) {
651
655
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
652
656
  );
653
657
  }
654
- var import_node_path43, 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_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;
655
659
  var init_otel = __esm({
656
660
  "src/otel.ts"() {
657
661
  "use strict";
658
662
  init_cjs_shims();
659
- import_node_path43 = __toESM(require("path"), 1);
663
+ import_node_path44 = __toESM(require("path"), 1);
660
664
  import_node_url2 = require("url");
661
665
  import_fastify2 = __toESM(require("fastify"), 1);
662
666
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1246,19 +1250,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1246
1250
  function longestIncomingWalk(graph, start, maxDepth) {
1247
1251
  let best = { path: [start], edges: [] };
1248
1252
  const visited = /* @__PURE__ */ new Set([start]);
1249
- function step(node, path46, edges) {
1250
- if (path46.length > best.path.length) {
1251
- best = { path: [...path46], edges: [...edges] };
1253
+ function step(node, path48, edges) {
1254
+ if (path48.length > best.path.length) {
1255
+ best = { path: [...path48], edges: [...edges] };
1252
1256
  }
1253
- if (path46.length - 1 >= maxDepth) return;
1257
+ if (path48.length - 1 >= maxDepth) return;
1254
1258
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1255
1259
  for (const [srcId, edge] of incoming) {
1256
1260
  if (visited.has(srcId)) continue;
1257
1261
  visited.add(srcId);
1258
- path46.push(srcId);
1262
+ path48.push(srcId);
1259
1263
  edges.push(edge);
1260
- step(srcId, path46, edges);
1261
- path46.pop();
1264
+ step(srcId, path48, edges);
1265
+ path48.pop();
1262
1266
  edges.pop();
1263
1267
  visited.delete(srcId);
1264
1268
  }
@@ -1273,7 +1277,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1273
1277
  for (const id of walk3.path) {
1274
1278
  const owner = resolveOwningService(graph, id);
1275
1279
  if (!owner) continue;
1276
- const { id: serviceId4, svc } = owner;
1280
+ const { id: serviceId5, svc } = owner;
1277
1281
  const deps = svc.dependencies ?? {};
1278
1282
  for (const pair of candidatePairs) {
1279
1283
  const declared = deps[pair.driver];
@@ -1286,7 +1290,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1286
1290
  );
1287
1291
  if (!result.compatible) {
1288
1292
  return {
1289
- rootCauseNode: serviceId4,
1293
+ rootCauseNode: serviceId5,
1290
1294
  rootCauseReason: result.reason ?? "incompatible driver",
1291
1295
  ...result.minDriverVersion ? {
1292
1296
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1301,7 +1305,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1301
1305
  for (const id of walk3.path) {
1302
1306
  const owner = resolveOwningService(graph, id);
1303
1307
  if (!owner) continue;
1304
- const { id: serviceId4, svc } = owner;
1308
+ const { id: serviceId5, svc } = owner;
1305
1309
  const deps = svc.dependencies ?? {};
1306
1310
  const serviceNodeEngine = svc.nodeEngine;
1307
1311
  for (const constraint of nodeEngineConstraints()) {
@@ -1310,7 +1314,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1310
1314
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1311
1315
  if (!result.compatible && result.reason) {
1312
1316
  return {
1313
- rootCauseNode: serviceId4,
1317
+ rootCauseNode: serviceId5,
1314
1318
  rootCauseReason: result.reason,
1315
1319
  ...result.requiredNodeVersion ? {
1316
1320
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1325,7 +1329,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1325
1329
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1326
1330
  if (!result.compatible && result.reason) {
1327
1331
  return {
1328
- rootCauseNode: serviceId4,
1332
+ rootCauseNode: serviceId5,
1329
1333
  rootCauseReason: result.reason,
1330
1334
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1331
1335
  };
@@ -1416,9 +1420,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1416
1420
  function isFailingCallEdge(e) {
1417
1421
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1418
1422
  }
1419
- function callSourcesForService(graph, serviceId4) {
1420
- const ids = [serviceId4];
1421
- for (const edgeId of graph.outboundEdges(serviceId4)) {
1423
+ function callSourcesForService(graph, serviceId5) {
1424
+ const ids = [serviceId5];
1425
+ for (const edgeId of graph.outboundEdges(serviceId5)) {
1422
1426
  const e = graph.getEdgeAttributes(edgeId);
1423
1427
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1424
1428
  const tgt = graph.getNodeAttributes(e.target);
@@ -1435,9 +1439,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1435
1439
  }
1436
1440
  return id < curId;
1437
1441
  }
1438
- function dominantFailingCall(graph, serviceId4, visited) {
1442
+ function dominantFailingCall(graph, serviceId5, visited) {
1439
1443
  let best = null;
1440
- for (const src of callSourcesForService(graph, serviceId4)) {
1444
+ for (const src of callSourcesForService(graph, serviceId5)) {
1441
1445
  for (const edgeId of graph.outboundEdges(src)) {
1442
1446
  const e = graph.getEdgeAttributes(edgeId);
1443
1447
  if (!isFailingCallEdge(e)) continue;
@@ -1452,26 +1456,26 @@ function dominantFailingCall(graph, serviceId4, visited) {
1452
1456
  return best;
1453
1457
  }
1454
1458
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1455
- const path46 = [originServiceId];
1459
+ const path48 = [originServiceId];
1456
1460
  const edges = [];
1457
1461
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1458
1462
  let current = originServiceId;
1459
1463
  for (let depth = 0; depth < maxDepth; depth++) {
1460
1464
  const hop = dominantFailingCall(graph, current, visited);
1461
1465
  if (!hop) break;
1462
- path46.push(hop.nextService);
1466
+ path48.push(hop.nextService);
1463
1467
  edges.push(hop.edge);
1464
1468
  visited.add(hop.nextService);
1465
1469
  current = hop.nextService;
1466
1470
  }
1467
1471
  if (edges.length === 0) return null;
1468
- return { path: path46, edges, culprit: current };
1472
+ return { path: path48, edges, culprit: current };
1469
1473
  }
1470
1474
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1471
1475
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1472
1476
  if (!chain) return null;
1473
1477
  const culprit = chain.culprit;
1474
- const path46 = [...chain.path];
1478
+ const path48 = [...chain.path];
1475
1479
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1476
1480
  const baseConfidence = confidenceFromMix(chain.edges);
1477
1481
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1479,14 +1483,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1479
1483
  if (loc) {
1480
1484
  let rootCauseNode = culprit;
1481
1485
  if (loc.fileNode) {
1482
- path46.push(loc.fileNode);
1486
+ path48.push(loc.fileNode);
1483
1487
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1484
1488
  rootCauseNode = loc.fileNode;
1485
1489
  }
1486
1490
  return import_types.RootCauseResultSchema.parse({
1487
1491
  rootCauseNode,
1488
1492
  rootCauseReason: loc.rootCauseReason,
1489
- traversalPath: path46,
1493
+ traversalPath: path48,
1490
1494
  edgeProvenances,
1491
1495
  confidence,
1492
1496
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1498,7 +1502,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1498
1502
  return import_types.RootCauseResultSchema.parse({
1499
1503
  rootCauseNode: culprit,
1500
1504
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1501
- traversalPath: path46,
1505
+ traversalPath: path48,
1502
1506
  edgeProvenances,
1503
1507
  confidence,
1504
1508
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2575,6 +2579,19 @@ function ensureServiceNode(graph, serviceName, env) {
2575
2579
  graph.addNode(id, node);
2576
2580
  return id;
2577
2581
  }
2582
+ function ensureInfraNode(graph, kind, name, provider) {
2583
+ const id = (0, import_types3.infraId)(kind, name);
2584
+ if (graph.hasNode(id)) return id;
2585
+ const node = {
2586
+ id,
2587
+ type: import_types3.NodeType.InfraNode,
2588
+ name,
2589
+ provider,
2590
+ kind
2591
+ };
2592
+ graph.addNode(id, node);
2593
+ return id;
2594
+ }
2578
2595
  function ensureDatabaseNode(graph, host, engine) {
2579
2596
  const id = (0, import_types3.databaseId)(host);
2580
2597
  if (graph.hasNode(id)) return id;
@@ -3054,29 +3071,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3054
3071
  toPromote.push({ frontierId: id, serviceId: target });
3055
3072
  });
3056
3073
  let promoted = 0;
3057
- for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
3074
+ for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
3058
3075
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3059
3076
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3060
3077
  if (!gate.allowed) {
3061
3078
  continue;
3062
3079
  }
3063
3080
  }
3064
- rewireFrontierEdges(graph, frontierId2, serviceId4);
3081
+ rewireFrontierEdges(graph, frontierId2, serviceId5);
3065
3082
  graph.dropNode(frontierId2);
3066
3083
  promoted++;
3067
3084
  }
3068
3085
  return promoted;
3069
3086
  }
3070
- function rewireFrontierEdges(graph, frontierId2, serviceId4) {
3087
+ function rewireFrontierEdges(graph, frontierId2, serviceId5) {
3071
3088
  const inbound = [...graph.inboundEdges(frontierId2)];
3072
3089
  const outbound = [...graph.outboundEdges(frontierId2)];
3073
3090
  for (const edgeId of inbound) {
3074
3091
  const edge = graph.getEdgeAttributes(edgeId);
3075
- rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
3092
+ rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
3076
3093
  }
3077
3094
  for (const edgeId of outbound) {
3078
3095
  const edge = graph.getEdgeAttributes(edgeId);
3079
- rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
3096
+ rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
3080
3097
  }
3081
3098
  }
3082
3099
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3227,24 +3244,58 @@ function dedupeIncidents(events) {
3227
3244
  return !hasRealFailure.has(groupKey(ev));
3228
3245
  });
3229
3246
  }
3247
+ var SnapshotValidationError = class extends Error {
3248
+ constructor(issues) {
3249
+ super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
3250
+ this.issues = issues;
3251
+ this.name = "SnapshotValidationError";
3252
+ }
3253
+ issues;
3254
+ };
3255
+ function describeZodIssues(error) {
3256
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
3257
+ }
3230
3258
  function mergeSnapshot(graph, snapshot) {
3231
3259
  const exported = snapshot.graph;
3260
+ const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
3261
+ const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
3262
+ const issues = [];
3263
+ const validNodes = [];
3264
+ const validEdges = [];
3265
+ for (const node of incomingNodes) {
3266
+ if (node.attributes === void 0) continue;
3267
+ const parsed = import_types3.GraphNodeSchema.safeParse(node.attributes);
3268
+ if (!parsed.success) {
3269
+ issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
3270
+ continue;
3271
+ }
3272
+ validNodes.push({ key: node.key, attributes: parsed.data });
3273
+ }
3274
+ for (const edge of incomingEdges) {
3275
+ if (edge.attributes === void 0) continue;
3276
+ const parsed = import_types3.GraphEdgeSchema.safeParse(edge.attributes);
3277
+ if (!parsed.success) {
3278
+ const label = edge.key ?? `${edge.source}->${edge.target}`;
3279
+ issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
3280
+ continue;
3281
+ }
3282
+ const id = edge.key ?? parsed.data.id;
3283
+ validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
3284
+ }
3285
+ if (issues.length > 0) {
3286
+ throw new SnapshotValidationError(issues);
3287
+ }
3232
3288
  let nodesAdded = 0;
3233
3289
  let edgesAdded = 0;
3234
- for (const node of exported.nodes ?? []) {
3290
+ for (const node of validNodes) {
3235
3291
  if (graph.hasNode(node.key)) continue;
3236
- if (!node.attributes) continue;
3237
3292
  graph.addNode(node.key, node.attributes);
3238
3293
  nodesAdded++;
3239
3294
  }
3240
- for (const edge of exported.edges ?? []) {
3241
- const attrs = edge.attributes;
3242
- if (!attrs) continue;
3243
- const id = edge.key ?? attrs.id;
3244
- if (!id) continue;
3245
- if (graph.hasEdge(id)) continue;
3295
+ for (const edge of validEdges) {
3296
+ if (graph.hasEdge(edge.key)) continue;
3246
3297
  if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
3247
- graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
3298
+ graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
3248
3299
  edgesAdded++;
3249
3300
  }
3250
3301
  return { nodesAdded, edgesAdded };
@@ -3838,9 +3889,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3838
3889
  "StatefulSet",
3839
3890
  "DaemonSet"
3840
3891
  ]);
3841
- function addAliases(graph, serviceId4, candidates) {
3842
- if (!graph.hasNode(serviceId4)) return;
3843
- const node = graph.getNodeAttributes(serviceId4);
3892
+ function addAliases(graph, serviceId5, candidates) {
3893
+ if (!graph.hasNode(serviceId5)) return;
3894
+ const node = graph.getNodeAttributes(serviceId5);
3844
3895
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3845
3896
  const set = new Set(node.aliases ?? []);
3846
3897
  for (const c of candidates) {
@@ -3850,7 +3901,7 @@ function addAliases(graph, serviceId4, candidates) {
3850
3901
  }
3851
3902
  if (set.size === 0) return;
3852
3903
  const updated = { ...node, aliases: [...set].sort() };
3853
- graph.replaceNodeAttributes(serviceId4, updated);
3904
+ graph.replaceNodeAttributes(serviceId5, updated);
3854
3905
  }
3855
3906
  function indexServicesByName(services) {
3856
3907
  const map = /* @__PURE__ */ new Map();
@@ -3883,12 +3934,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3883
3934
  }
3884
3935
  if (!compose?.services) return;
3885
3936
  for (const [composeName, svc] of Object.entries(compose.services)) {
3886
- const serviceId4 = serviceIndex.get(composeName);
3887
- if (!serviceId4) continue;
3937
+ const serviceId5 = serviceIndex.get(composeName);
3938
+ if (!serviceId5) continue;
3888
3939
  const aliases = /* @__PURE__ */ new Set([composeName]);
3889
3940
  if (svc.container_name) aliases.add(svc.container_name);
3890
3941
  if (svc.hostname) aliases.add(svc.hostname);
3891
- addAliases(graph, serviceId4, aliases);
3942
+ addAliases(graph, serviceId5, aliases);
3892
3943
  }
3893
3944
  }
3894
3945
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -5299,10 +5350,10 @@ function fastifyRouteMethods(objNode) {
5299
5350
  }
5300
5351
  return [];
5301
5352
  }
5302
- function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5353
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
5303
5354
  const tree = parseSource2(parser, source);
5304
5355
  const out = [];
5305
- const framework = hasExpress ? "express" : "fastify";
5356
+ const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
5306
5357
  walk(tree.rootNode, (node) => {
5307
5358
  if (node.type !== "call_expression") return;
5308
5359
  const fn = node.childForFieldName("function");
@@ -5452,8 +5503,9 @@ async function addRoutes(graph, services) {
5452
5503
  };
5453
5504
  const hasExpress = deps["express"] !== void 0;
5454
5505
  const hasFastify = deps["fastify"] !== void 0;
5506
+ const hasHono = deps["hono"] !== void 0;
5455
5507
  const hasNext = deps["next"] !== void 0;
5456
- if (!hasExpress && !hasFastify && !hasNext) continue;
5508
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
5457
5509
  const files = await loadSourceFiles(service.dir);
5458
5510
  for (const file of files) {
5459
5511
  if (isTestPath(file.path)) continue;
@@ -5463,8 +5515,8 @@ async function addRoutes(graph, services) {
5463
5515
  try {
5464
5516
  if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5465
5517
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5466
- } else if (hasExpress || hasFastify) {
5467
- routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5518
+ } else if (hasExpress || hasFastify || hasHono) {
5519
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
5468
5520
  } else {
5469
5521
  routes = [];
5470
5522
  }
@@ -6788,30 +6840,264 @@ async function addK8sResources(graph, scanPath) {
6788
6840
  return { nodesAdded, edgesAdded: 0 };
6789
6841
  }
6790
6842
 
6843
+ // src/extract/infra/cloudflare.ts
6844
+ init_cjs_shims();
6845
+ var import_node_fs19 = require("fs");
6846
+ var import_node_path35 = __toESM(require("path"), 1);
6847
+ var import_smol_toml2 = require("smol-toml");
6848
+ var import_types25 = require("@neat.is/types");
6849
+ var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
6850
+ async function readWranglerConfig(dir) {
6851
+ for (const filename of WRANGLER_FILENAMES) {
6852
+ const abs = import_node_path35.default.join(dir, filename);
6853
+ if (!await exists(abs)) continue;
6854
+ const raw = await import_node_fs19.promises.readFile(abs, "utf8");
6855
+ const config = filename === "wrangler.toml" ? (0, import_smol_toml2.parse)(raw) : JSON.parse(maskCommentsInSource(raw));
6856
+ return { config, relFile: filename, raw };
6857
+ }
6858
+ return null;
6859
+ }
6860
+ function lineContaining(raw, needle) {
6861
+ if (!needle) return void 0;
6862
+ const idx = raw.indexOf(needle);
6863
+ if (idx === -1) return void 0;
6864
+ let line = 1;
6865
+ for (let i = 0; i < idx; i++) if (raw[i] === "\n") line++;
6866
+ return line;
6867
+ }
6868
+ function normalizeRoutes(config) {
6869
+ const out = [];
6870
+ const pushOne = (r) => {
6871
+ if (typeof r === "string") out.push(r);
6872
+ else if (r && typeof r === "object" && typeof r.pattern === "string") {
6873
+ out.push(r.pattern);
6874
+ }
6875
+ };
6876
+ if (Array.isArray(config.routes)) config.routes.forEach(pushOne);
6877
+ else if (config.route) pushOne(config.route);
6878
+ return out;
6879
+ }
6880
+ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, line) {
6881
+ let nodesAdded = 0;
6882
+ let edgesAdded = 0;
6883
+ const node = makeInfraNode(kind, name, "cloudflare");
6884
+ if (!graph.hasNode(node.id)) {
6885
+ graph.addNode(node.id, node);
6886
+ nodesAdded++;
6887
+ }
6888
+ if (node.id === anchorId) return { nodesAdded, edgesAdded };
6889
+ const edgeId = (0, import_types4.extractedEdgeId)(anchorId, node.id, edgeType);
6890
+ if (!graph.hasEdge(edgeId)) {
6891
+ const edge = {
6892
+ id: edgeId,
6893
+ source: anchorId,
6894
+ target: node.id,
6895
+ type: edgeType,
6896
+ provenance: import_types25.Provenance.EXTRACTED,
6897
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
6898
+ evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
6899
+ };
6900
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
6901
+ edgesAdded++;
6902
+ }
6903
+ return { nodesAdded, edgesAdded };
6904
+ }
6905
+ async function addCloudflareWorkers(graph, services, scanPath) {
6906
+ let nodesAdded = 0;
6907
+ let edgesAdded = 0;
6908
+ const discovered = [];
6909
+ const workerIndex = /* @__PURE__ */ new Map();
6910
+ for (const service of services) {
6911
+ let read;
6912
+ try {
6913
+ read = await readWranglerConfig(service.dir);
6914
+ } catch (err) {
6915
+ recordExtractionError("infra cloudflare", import_node_path35.default.relative(scanPath, service.dir), err);
6916
+ continue;
6917
+ }
6918
+ if (!read || !read.config.name) continue;
6919
+ const evidenceFile = toPosix2(import_node_path35.default.relative(scanPath, import_node_path35.default.join(service.dir, read.relFile)));
6920
+ discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
6921
+ }
6922
+ for (const worker of discovered) {
6923
+ const { service, config } = worker;
6924
+ const serviceNode = graph.getNodeAttributes(service.node.id);
6925
+ if (serviceNode.platform !== "cloudflare") {
6926
+ const updated = { ...serviceNode, platform: "cloudflare" };
6927
+ graph.replaceNodeAttributes(service.node.id, updated);
6928
+ }
6929
+ let anchorId = service.node.id;
6930
+ if (config.main) {
6931
+ const entryRelPath = toPosix2(import_node_path35.default.normalize(config.main));
6932
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6933
+ graph,
6934
+ service.pkg.name,
6935
+ service.node.id,
6936
+ entryRelPath
6937
+ );
6938
+ nodesAdded += fn;
6939
+ edgesAdded += fe;
6940
+ const fileNode = graph.getNodeAttributes(fileNodeId);
6941
+ if (fileNode.platform !== "cloudflare" || fileNode.platformName !== config.name) {
6942
+ const updated = { ...fileNode, platform: "cloudflare", platformName: config.name };
6943
+ graph.replaceNodeAttributes(fileNodeId, updated);
6944
+ }
6945
+ anchorId = fileNodeId;
6946
+ }
6947
+ workerIndex.set(config.name, { anchorId });
6948
+ }
6949
+ for (const worker of discovered) {
6950
+ const { config, evidenceFile, raw } = worker;
6951
+ const anchorId = workerIndex.get(config.name).anchorId;
6952
+ const runtimeNode = makeInfraNode("workerd", "cloudflare", "cloudflare");
6953
+ if (!graph.hasNode(runtimeNode.id)) {
6954
+ graph.addNode(runtimeNode.id, runtimeNode);
6955
+ nodesAdded++;
6956
+ }
6957
+ if (runtimeNode.id !== anchorId) {
6958
+ const runsOnId = (0, import_types4.extractedEdgeId)(anchorId, runtimeNode.id, import_types25.EdgeType.RUNS_ON);
6959
+ if (!graph.hasEdge(runsOnId)) {
6960
+ const edge = {
6961
+ id: runsOnId,
6962
+ source: anchorId,
6963
+ target: runtimeNode.id,
6964
+ type: import_types25.EdgeType.RUNS_ON,
6965
+ provenance: import_types25.Provenance.EXTRACTED,
6966
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
6967
+ evidence: {
6968
+ file: evidenceFile,
6969
+ ...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
6970
+ }
6971
+ };
6972
+ graph.addEdgeWithKey(runsOnId, edge.source, edge.target, edge);
6973
+ edgesAdded++;
6974
+ }
6975
+ }
6976
+ for (const route of normalizeRoutes(config)) {
6977
+ const result = addResourceEdge(
6978
+ graph,
6979
+ anchorId,
6980
+ import_types25.EdgeType.CONNECTS_TO,
6981
+ "cloudflare-route",
6982
+ route,
6983
+ evidenceFile,
6984
+ lineContaining(raw, route)
6985
+ );
6986
+ nodesAdded += result.nodesAdded;
6987
+ edgesAdded += result.edgesAdded;
6988
+ }
6989
+ const bindingGroups = [
6990
+ { kind: "cloudflare-kv", entries: config.kv_namespaces ?? [], nameOf: (b) => b.binding },
6991
+ { kind: "cloudflare-d1", entries: config.d1_databases ?? [], nameOf: (b) => b.binding },
6992
+ { kind: "cloudflare-r2", entries: config.r2_buckets ?? [], nameOf: (b) => b.binding },
6993
+ { kind: "cloudflare-durable-object", entries: config.durable_objects?.bindings ?? [], nameOf: (b) => b.name },
6994
+ { kind: "cloudflare-queue", entries: config.queues?.producers ?? [], nameOf: (b) => b.queue },
6995
+ { kind: "cloudflare-queue", entries: config.queues?.consumers ?? [], nameOf: (b) => b.queue }
6996
+ ];
6997
+ for (const group of bindingGroups) {
6998
+ for (const entry of group.entries) {
6999
+ const name = group.nameOf(entry);
7000
+ if (!name) continue;
7001
+ const result = addResourceEdge(
7002
+ graph,
7003
+ anchorId,
7004
+ import_types25.EdgeType.DEPENDS_ON,
7005
+ group.kind,
7006
+ name,
7007
+ evidenceFile,
7008
+ lineContaining(raw, name)
7009
+ );
7010
+ nodesAdded += result.nodesAdded;
7011
+ edgesAdded += result.edgesAdded;
7012
+ }
7013
+ }
7014
+ for (const cron of config.triggers?.crons ?? []) {
7015
+ const result = addResourceEdge(
7016
+ graph,
7017
+ anchorId,
7018
+ import_types25.EdgeType.DEPENDS_ON,
7019
+ "cloudflare-cron",
7020
+ cron,
7021
+ evidenceFile,
7022
+ lineContaining(raw, cron)
7023
+ );
7024
+ nodesAdded += result.nodesAdded;
7025
+ edgesAdded += result.edgesAdded;
7026
+ }
7027
+ for (const varName of Object.keys(config.vars ?? {})) {
7028
+ const result = addResourceEdge(
7029
+ graph,
7030
+ anchorId,
7031
+ import_types25.EdgeType.DEPENDS_ON,
7032
+ "cloudflare-env-var",
7033
+ varName,
7034
+ evidenceFile,
7035
+ lineContaining(raw, varName)
7036
+ );
7037
+ nodesAdded += result.nodesAdded;
7038
+ edgesAdded += result.edgesAdded;
7039
+ }
7040
+ for (const svc of config.services ?? []) {
7041
+ if (!svc.service) continue;
7042
+ const target = workerIndex.get(svc.service);
7043
+ if (target && target.anchorId !== anchorId) {
7044
+ const edgeId = (0, import_types4.extractedEdgeId)(anchorId, target.anchorId, import_types25.EdgeType.CALLS);
7045
+ if (!graph.hasEdge(edgeId)) {
7046
+ const edge = {
7047
+ id: edgeId,
7048
+ source: anchorId,
7049
+ target: target.anchorId,
7050
+ type: import_types25.EdgeType.CALLS,
7051
+ provenance: import_types25.Provenance.EXTRACTED,
7052
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
7053
+ evidence: { file: evidenceFile, line: lineContaining(raw, svc.service) }
7054
+ };
7055
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
7056
+ edgesAdded++;
7057
+ }
7058
+ continue;
7059
+ }
7060
+ const result = addResourceEdge(
7061
+ graph,
7062
+ anchorId,
7063
+ import_types25.EdgeType.DEPENDS_ON,
7064
+ "cloudflare-service-binding",
7065
+ svc.service,
7066
+ evidenceFile,
7067
+ lineContaining(raw, svc.service)
7068
+ );
7069
+ nodesAdded += result.nodesAdded;
7070
+ edgesAdded += result.edgesAdded;
7071
+ }
7072
+ }
7073
+ return { nodesAdded, edgesAdded };
7074
+ }
7075
+
6791
7076
  // src/extract/infra/index.ts
6792
7077
  async function addInfra(graph, scanPath, services) {
6793
7078
  const compose = await addComposeInfra(graph, scanPath, services);
6794
7079
  const dockerfile = await addDockerfileRuntimes(graph, services, scanPath);
6795
7080
  const terraform = await addTerraformResources(graph, scanPath);
6796
7081
  const k8s = await addK8sResources(graph, scanPath);
7082
+ const cloudflare = await addCloudflareWorkers(graph, services, scanPath);
6797
7083
  return {
6798
- nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded,
6799
- edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded
7084
+ nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded,
7085
+ edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded
6800
7086
  };
6801
7087
  }
6802
7088
 
6803
7089
  // src/extract/index.ts
6804
- var import_node_path36 = __toESM(require("path"), 1);
7090
+ var import_node_path37 = __toESM(require("path"), 1);
6805
7091
 
6806
7092
  // src/extract/retire.ts
6807
7093
  init_cjs_shims();
6808
- var import_node_fs19 = require("fs");
6809
- var import_node_path35 = __toESM(require("path"), 1);
6810
- var import_types25 = require("@neat.is/types");
7094
+ var import_node_fs20 = require("fs");
7095
+ var import_node_path36 = __toESM(require("path"), 1);
7096
+ var import_types26 = require("@neat.is/types");
6811
7097
  function dropOrphanedFileNodes(graph) {
6812
7098
  const orphans = [];
6813
7099
  graph.forEachNode((id, attrs) => {
6814
- if (attrs.type !== import_types25.NodeType.FileNode) return;
7100
+ if (attrs.type !== import_types26.NodeType.FileNode) return;
6815
7101
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
6816
7102
  orphans.push(id);
6817
7103
  }
@@ -6824,14 +7110,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
6824
7110
  const bases = [scanPath, ...serviceDirs];
6825
7111
  graph.forEachEdge((id, attrs) => {
6826
7112
  const edge = attrs;
6827
- if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
7113
+ if (edge.provenance !== import_types26.Provenance.EXTRACTED) return;
6828
7114
  const evidenceFile = edge.evidence?.file;
6829
7115
  if (!evidenceFile) return;
6830
- if (import_node_path35.default.isAbsolute(evidenceFile)) {
6831
- if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
7116
+ if (import_node_path36.default.isAbsolute(evidenceFile)) {
7117
+ if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
6832
7118
  return;
6833
7119
  }
6834
- const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path35.default.join(base, evidenceFile)));
7120
+ const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path36.default.join(base, evidenceFile)));
6835
7121
  if (!found) toDrop.push(id);
6836
7122
  });
6837
7123
  for (const id of toDrop) graph.dropEdge(id);
@@ -6873,7 +7159,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6873
7159
  }
6874
7160
  const droppedEntries = drainDroppedExtracted();
6875
7161
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
6876
- const rejectedPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.errorsPath), "rejected.ndjson");
7162
+ const rejectedPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.errorsPath), "rejected.ndjson");
6877
7163
  try {
6878
7164
  await writeRejectedExtracted(droppedEntries, rejectedPath);
6879
7165
  } catch (err) {
@@ -6907,9 +7193,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6907
7193
 
6908
7194
  // src/persist.ts
6909
7195
  init_cjs_shims();
6910
- var import_node_fs20 = require("fs");
6911
- var import_node_path37 = __toESM(require("path"), 1);
6912
- var import_types26 = require("@neat.is/types");
7196
+ var import_node_fs21 = require("fs");
7197
+ var import_node_path38 = __toESM(require("path"), 1);
7198
+ var import_types27 = require("@neat.is/types");
6913
7199
  var SCHEMA_VERSION = 4;
6914
7200
  function migrateV1ToV2(payload) {
6915
7201
  const nodes = payload.graph.nodes;
@@ -6931,12 +7217,12 @@ function migrateV2ToV3(payload) {
6931
7217
  for (const edge of edges) {
6932
7218
  const attrs = edge.attributes;
6933
7219
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
6934
- attrs.provenance = import_types26.Provenance.OBSERVED;
7220
+ attrs.provenance = import_types27.Provenance.OBSERVED;
6935
7221
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
6936
7222
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
6937
7223
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
6938
7224
  if (type && source && target) {
6939
- const newId = (0, import_types26.observedEdgeId)(source, target, type);
7225
+ const newId = (0, import_types27.observedEdgeId)(source, target, type);
6940
7226
  attrs.id = newId;
6941
7227
  if (edge.key) edge.key = newId;
6942
7228
  }
@@ -6945,7 +7231,7 @@ function migrateV2ToV3(payload) {
6945
7231
  return { ...payload, schemaVersion: 3 };
6946
7232
  }
6947
7233
  async function ensureDir(filePath) {
6948
- await import_node_fs20.promises.mkdir(import_node_path37.default.dirname(filePath), { recursive: true });
7234
+ await import_node_fs21.promises.mkdir(import_node_path38.default.dirname(filePath), { recursive: true });
6949
7235
  }
6950
7236
  async function saveGraphToDisk(graph, outPath) {
6951
7237
  await ensureDir(outPath);
@@ -6955,13 +7241,13 @@ async function saveGraphToDisk(graph, outPath) {
6955
7241
  graph: graph.export()
6956
7242
  };
6957
7243
  const tmp = `${outPath}.tmp`;
6958
- await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6959
- await import_node_fs20.promises.rename(tmp, outPath);
7244
+ await import_node_fs21.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
7245
+ await import_node_fs21.promises.rename(tmp, outPath);
6960
7246
  }
6961
7247
  async function loadGraphFromDisk(graph, outPath) {
6962
7248
  let raw;
6963
7249
  try {
6964
- raw = await import_node_fs20.promises.readFile(outPath, "utf8");
7250
+ raw = await import_node_fs21.promises.readFile(outPath, "utf8");
6965
7251
  } catch (err) {
6966
7252
  if (err.code === "ENOENT") return;
6967
7253
  throw err;
@@ -7028,19 +7314,19 @@ function startPersistLoop(graph, outPath, opts = {}) {
7028
7314
  init_cjs_shims();
7029
7315
  var import_fastify = __toESM(require("fastify"), 1);
7030
7316
  var import_cors = __toESM(require("@fastify/cors"), 1);
7031
- var import_types29 = require("@neat.is/types");
7317
+ var import_types30 = require("@neat.is/types");
7032
7318
 
7033
7319
  // src/extend/index.ts
7034
7320
  init_cjs_shims();
7035
- var import_node_fs22 = require("fs");
7036
- var import_node_path39 = __toESM(require("path"), 1);
7321
+ var import_node_fs23 = require("fs");
7322
+ var import_node_path40 = __toESM(require("path"), 1);
7037
7323
  var import_node_os2 = __toESM(require("os"), 1);
7038
7324
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
7039
7325
 
7040
7326
  // src/installers/package-manager.ts
7041
7327
  init_cjs_shims();
7042
- var import_node_fs21 = require("fs");
7043
- var import_node_path38 = __toESM(require("path"), 1);
7328
+ var import_node_fs22 = require("fs");
7329
+ var import_node_path39 = __toESM(require("path"), 1);
7044
7330
  var import_node_child_process = require("child_process");
7045
7331
  var LOCKFILE_PRIORITY = [
7046
7332
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -7055,29 +7341,29 @@ var LOCKFILE_PRIORITY = [
7055
7341
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
7056
7342
  async function exists2(p) {
7057
7343
  try {
7058
- await import_node_fs21.promises.access(p);
7344
+ await import_node_fs22.promises.access(p);
7059
7345
  return true;
7060
7346
  } catch {
7061
7347
  return false;
7062
7348
  }
7063
7349
  }
7064
7350
  async function detectPackageManager(serviceDir) {
7065
- let dir = import_node_path38.default.resolve(serviceDir);
7351
+ let dir = import_node_path39.default.resolve(serviceDir);
7066
7352
  const stops = /* @__PURE__ */ new Set();
7067
7353
  for (let i = 0; i < 64; i++) {
7068
7354
  if (stops.has(dir)) break;
7069
7355
  stops.add(dir);
7070
7356
  for (const candidate of LOCKFILE_PRIORITY) {
7071
- const lockPath = import_node_path38.default.join(dir, candidate.lockfile);
7357
+ const lockPath = import_node_path39.default.join(dir, candidate.lockfile);
7072
7358
  if (await exists2(lockPath)) {
7073
7359
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
7074
7360
  }
7075
7361
  }
7076
- const parent = import_node_path38.default.dirname(dir);
7362
+ const parent = import_node_path39.default.dirname(dir);
7077
7363
  if (parent === dir) break;
7078
7364
  dir = parent;
7079
7365
  }
7080
- return { pm: "npm", cwd: import_node_path38.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7366
+ return { pm: "npm", cwd: import_node_path39.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7081
7367
  }
7082
7368
  async function runPackageManagerInstall(cmd) {
7083
7369
  return new Promise((resolve) => {
@@ -7119,30 +7405,30 @@ ${err.message}`
7119
7405
  // src/extend/index.ts
7120
7406
  async function fileExists2(p) {
7121
7407
  try {
7122
- await import_node_fs22.promises.access(p);
7408
+ await import_node_fs23.promises.access(p);
7123
7409
  return true;
7124
7410
  } catch {
7125
7411
  return false;
7126
7412
  }
7127
7413
  }
7128
7414
  async function readPackageJson(scanPath) {
7129
- const pkgPath = import_node_path39.default.join(scanPath, "package.json");
7130
- const raw = await import_node_fs22.promises.readFile(pkgPath, "utf8");
7415
+ const pkgPath = import_node_path40.default.join(scanPath, "package.json");
7416
+ const raw = await import_node_fs23.promises.readFile(pkgPath, "utf8");
7131
7417
  return JSON.parse(raw);
7132
7418
  }
7133
7419
  async function findHookFiles(scanPath) {
7134
- const entries = await import_node_fs22.promises.readdir(scanPath);
7420
+ const entries = await import_node_fs23.promises.readdir(scanPath);
7135
7421
  return entries.filter(
7136
7422
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
7137
7423
  ).sort();
7138
7424
  }
7139
7425
  function extendLogPath() {
7140
- return process.env.NEAT_EXTEND_LOG ?? import_node_path39.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7426
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path40.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7141
7427
  }
7142
7428
  async function appendExtendLog(entry) {
7143
7429
  const logPath = extendLogPath();
7144
- await import_node_fs22.promises.mkdir(import_node_path39.default.dirname(logPath), { recursive: true });
7145
- await import_node_fs22.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
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");
7146
7432
  }
7147
7433
  function splicedContent(fileContent, snippet2) {
7148
7434
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -7200,7 +7486,7 @@ function lookupInstrumentation(library, installedVersion) {
7200
7486
  }
7201
7487
  async function describeProjectInstrumentation(ctx) {
7202
7488
  const hookFiles = await findHookFiles(ctx.scanPath);
7203
- const envNeat = await fileExists2(import_node_path39.default.join(ctx.scanPath, ".env.neat"));
7489
+ const envNeat = await fileExists2(import_node_path40.default.join(ctx.scanPath, ".env.neat"));
7204
7490
  const registryInstrPackages = new Set(
7205
7491
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
7206
7492
  );
@@ -7222,31 +7508,31 @@ async function applyExtension(ctx, args, options) {
7222
7508
  );
7223
7509
  }
7224
7510
  for (const file of hookFiles) {
7225
- const content = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, file), "utf8");
7511
+ const content = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7226
7512
  if (content.includes(args.registration_snippet)) {
7227
7513
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
7228
7514
  }
7229
7515
  }
7230
7516
  const primaryFile = hookFiles[0];
7231
- const primaryPath = import_node_path39.default.join(ctx.scanPath, primaryFile);
7517
+ const primaryPath = import_node_path40.default.join(ctx.scanPath, primaryFile);
7232
7518
  const filesTouched = [];
7233
7519
  const depsAdded = [];
7234
- const pkgPath = import_node_path39.default.join(ctx.scanPath, "package.json");
7520
+ const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7235
7521
  const pkg = await readPackageJson(ctx.scanPath);
7236
7522
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
7237
7523
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
7238
- await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7524
+ await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7239
7525
  filesTouched.push("package.json");
7240
7526
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
7241
7527
  }
7242
- const hookContent = await import_node_fs22.promises.readFile(primaryPath, "utf8");
7528
+ const hookContent = await import_node_fs23.promises.readFile(primaryPath, "utf8");
7243
7529
  const patched = splicedContent(hookContent, args.registration_snippet);
7244
7530
  if (!patched) {
7245
7531
  throw new Error(
7246
7532
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
7247
7533
  );
7248
7534
  }
7249
- await import_node_fs22.promises.writeFile(primaryPath, patched, "utf8");
7535
+ await import_node_fs23.promises.writeFile(primaryPath, patched, "utf8");
7250
7536
  filesTouched.push(primaryFile);
7251
7537
  const cmd = await detectPackageManager(ctx.scanPath);
7252
7538
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -7277,7 +7563,7 @@ async function dryRunExtension(ctx, args) {
7277
7563
  };
7278
7564
  }
7279
7565
  for (const file of hookFiles) {
7280
- const content = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, file), "utf8");
7566
+ const content = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7281
7567
  if (content.includes(args.registration_snippet)) {
7282
7568
  return {
7283
7569
  library: args.library,
@@ -7299,7 +7585,7 @@ async function dryRunExtension(ctx, args) {
7299
7585
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
7300
7586
  filesTouched.push("package.json");
7301
7587
  }
7302
- const hookContent = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, primaryFile), "utf8");
7588
+ const hookContent = await import_node_fs23.promises.readFile(import_node_path40.default.join(ctx.scanPath, primaryFile), "utf8");
7303
7589
  const patched = splicedContent(hookContent, args.registration_snippet);
7304
7590
  if (patched) {
7305
7591
  filesTouched.push(primaryFile);
@@ -7314,28 +7600,28 @@ async function rollbackExtension(ctx, args) {
7314
7600
  if (!await fileExists2(logPath)) {
7315
7601
  return { undone: false, message: "no apply found for library" };
7316
7602
  }
7317
- const raw = await import_node_fs22.promises.readFile(logPath, "utf8");
7603
+ const raw = await import_node_fs23.promises.readFile(logPath, "utf8");
7318
7604
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
7319
7605
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
7320
7606
  if (!match) {
7321
7607
  return { undone: false, message: "no apply found for library" };
7322
7608
  }
7323
- const pkgPath = import_node_path39.default.join(ctx.scanPath, "package.json");
7609
+ const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7324
7610
  if (await fileExists2(pkgPath)) {
7325
7611
  const pkg = await readPackageJson(ctx.scanPath);
7326
7612
  if (pkg.dependencies?.[match.instrumentation_package]) {
7327
7613
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
7328
7614
  pkg.dependencies = rest;
7329
- await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7615
+ await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7330
7616
  }
7331
7617
  }
7332
7618
  const hookFiles = await findHookFiles(ctx.scanPath);
7333
7619
  for (const file of hookFiles) {
7334
- const filePath = import_node_path39.default.join(ctx.scanPath, file);
7335
- const content = await import_node_fs22.promises.readFile(filePath, "utf8");
7620
+ const filePath = import_node_path40.default.join(ctx.scanPath, file);
7621
+ const content = await import_node_fs23.promises.readFile(filePath, "utf8");
7336
7622
  if (content.includes(match.registration_snippet)) {
7337
7623
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
7338
- await import_node_fs22.promises.writeFile(filePath, filtered, "utf8");
7624
+ await import_node_fs23.promises.writeFile(filePath, filtered, "utf8");
7339
7625
  break;
7340
7626
  }
7341
7627
  }
@@ -7347,44 +7633,44 @@ async function rollbackExtension(ctx, args) {
7347
7633
 
7348
7634
  // src/divergences.ts
7349
7635
  init_cjs_shims();
7350
- var import_types27 = require("@neat.is/types");
7636
+ var import_types28 = require("@neat.is/types");
7351
7637
  function bucketKey(source, target, type) {
7352
7638
  return `${type}|${source}|${target}`;
7353
7639
  }
7354
7640
  function bucketEdges(graph) {
7355
- const buckets = /* @__PURE__ */ new Map();
7641
+ const buckets2 = /* @__PURE__ */ new Map();
7356
7642
  graph.forEachEdge((id, attrs) => {
7357
7643
  const e = attrs;
7358
- const parsed = (0, import_types27.parseEdgeId)(id);
7644
+ const parsed = (0, import_types28.parseEdgeId)(id);
7359
7645
  const provenance = parsed?.provenance ?? e.provenance;
7360
7646
  const key = bucketKey(e.source, e.target, e.type);
7361
- const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
7647
+ const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
7362
7648
  switch (provenance) {
7363
- case import_types27.Provenance.EXTRACTED:
7649
+ case import_types28.Provenance.EXTRACTED:
7364
7650
  cur.extracted = e;
7365
7651
  break;
7366
- case import_types27.Provenance.OBSERVED:
7652
+ case import_types28.Provenance.OBSERVED:
7367
7653
  cur.observed = e;
7368
7654
  break;
7369
- case import_types27.Provenance.INFERRED:
7655
+ case import_types28.Provenance.INFERRED:
7370
7656
  cur.inferred = e;
7371
7657
  break;
7372
7658
  default:
7373
- if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
7659
+ if (e.provenance === import_types28.Provenance.STALE) cur.stale = e;
7374
7660
  }
7375
- buckets.set(key, cur);
7661
+ buckets2.set(key, cur);
7376
7662
  });
7377
- return buckets;
7663
+ return buckets2;
7378
7664
  }
7379
7665
  function nodeIsFrontier(graph, nodeId) {
7380
7666
  if (!graph.hasNode(nodeId)) return false;
7381
7667
  const attrs = graph.getNodeAttributes(nodeId);
7382
- return attrs.type === import_types27.NodeType.FrontierNode;
7668
+ return attrs.type === import_types28.NodeType.FrontierNode;
7383
7669
  }
7384
7670
  function nodeIsWebsocketChannel(graph, nodeId) {
7385
7671
  if (!graph.hasNode(nodeId)) return false;
7386
7672
  const attrs = graph.getNodeAttributes(nodeId);
7387
- return attrs.type === import_types27.NodeType.WebSocketChannelNode;
7673
+ return attrs.type === import_types28.NodeType.WebSocketChannelNode;
7388
7674
  }
7389
7675
  function clampConfidence(n) {
7390
7676
  if (!Number.isFinite(n)) return 0;
@@ -7404,14 +7690,14 @@ function gradedConfidence(edge) {
7404
7690
  return clampConfidence(confidenceForEdge(edge));
7405
7691
  }
7406
7692
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
7407
- import_types27.EdgeType.CALLS,
7408
- import_types27.EdgeType.CONNECTS_TO,
7409
- import_types27.EdgeType.PUBLISHES_TO,
7410
- import_types27.EdgeType.CONSUMES_FROM
7693
+ import_types28.EdgeType.CALLS,
7694
+ import_types28.EdgeType.CONNECTS_TO,
7695
+ import_types28.EdgeType.PUBLISHES_TO,
7696
+ import_types28.EdgeType.CONSUMES_FROM
7411
7697
  ]);
7412
7698
  function detectMissingDivergences(graph, bucket) {
7413
7699
  const out = [];
7414
- if (bucket.type === import_types27.EdgeType.CONTAINS) return out;
7700
+ if (bucket.type === import_types28.EdgeType.CONTAINS) return out;
7415
7701
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
7416
7702
  if (!nodeIsFrontier(graph, bucket.target)) {
7417
7703
  out.push({
@@ -7452,7 +7738,7 @@ function declaredHostFor(svc) {
7452
7738
  function hasExtractedConfiguredBy(graph, svcId) {
7453
7739
  for (const edgeId of graph.outboundEdges(svcId)) {
7454
7740
  const e = graph.getEdgeAttributes(edgeId);
7455
- if (e.type === import_types27.EdgeType.CONFIGURED_BY && e.provenance === import_types27.Provenance.EXTRACTED) {
7741
+ if (e.type === import_types28.EdgeType.CONFIGURED_BY && e.provenance === import_types28.Provenance.EXTRACTED) {
7456
7742
  return true;
7457
7743
  }
7458
7744
  }
@@ -7465,10 +7751,10 @@ function detectHostMismatch(graph, svcId, svc) {
7465
7751
  const out = [];
7466
7752
  for (const edgeId of graph.outboundEdges(svcId)) {
7467
7753
  const edge = graph.getEdgeAttributes(edgeId);
7468
- if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7469
- if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
7754
+ if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7755
+ if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
7470
7756
  const target = graph.getNodeAttributes(edge.target);
7471
- if (target.type !== import_types27.NodeType.DatabaseNode) continue;
7757
+ if (target.type !== import_types28.NodeType.DatabaseNode) continue;
7472
7758
  const observedHost = target.host?.trim();
7473
7759
  if (!observedHost) continue;
7474
7760
  if (observedHost === declaredHost) continue;
@@ -7490,10 +7776,10 @@ function detectCompatDivergences(graph, svcId, svc) {
7490
7776
  const deps = svc.dependencies ?? {};
7491
7777
  for (const edgeId of graph.outboundEdges(svcId)) {
7492
7778
  const edge = graph.getEdgeAttributes(edgeId);
7493
- if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7494
- if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
7779
+ if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7780
+ if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
7495
7781
  const target = graph.getNodeAttributes(edge.target);
7496
- if (target.type !== import_types27.NodeType.DatabaseNode) continue;
7782
+ if (target.type !== import_types28.NodeType.DatabaseNode) continue;
7497
7783
  for (const pair of compatPairs()) {
7498
7784
  if (pair.engine !== target.engine) continue;
7499
7785
  const declared = deps[pair.driver];
@@ -7552,7 +7838,7 @@ function suppressHostMismatchHalves(all) {
7552
7838
  for (const d of all) {
7553
7839
  if (d.type !== "host-mismatch") continue;
7554
7840
  observedHalf.add(`${d.source}->${d.target}`);
7555
- declaredHalf.add((0, import_types27.databaseId)(d.extractedHost));
7841
+ declaredHalf.add((0, import_types28.databaseId)(d.extractedHost));
7556
7842
  }
7557
7843
  if (observedHalf.size === 0) return all;
7558
7844
  return all.filter((d) => {
@@ -7565,13 +7851,13 @@ function suppressHostMismatchHalves(all) {
7565
7851
  }
7566
7852
  function computeDivergences(graph, opts = {}) {
7567
7853
  const all = [];
7568
- const buckets = bucketEdges(graph);
7569
- for (const bucket of buckets.values()) {
7854
+ const buckets2 = bucketEdges(graph);
7855
+ for (const bucket of buckets2.values()) {
7570
7856
  for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
7571
7857
  }
7572
7858
  graph.forEachNode((nodeId, attrs) => {
7573
7859
  const n = attrs;
7574
- if (n.type !== import_types27.NodeType.ServiceNode) return;
7860
+ if (n.type !== import_types28.NodeType.ServiceNode) return;
7575
7861
  const svc = n;
7576
7862
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
7577
7863
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -7605,16 +7891,61 @@ function computeDivergences(graph, opts = {}) {
7605
7891
  if (a.source !== b.source) return a.source.localeCompare(b.source);
7606
7892
  return a.target.localeCompare(b.target);
7607
7893
  });
7608
- return import_types27.DivergenceResultSchema.parse({
7894
+ return import_types28.DivergenceResultSchema.parse({
7609
7895
  divergences: filtered,
7610
7896
  totalAffected: filtered.length,
7611
7897
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
7612
7898
  });
7613
7899
  }
7614
7900
 
7901
+ // src/logs-store.ts
7902
+ init_cjs_shims();
7903
+ var LOGS_STORE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7904
+ var LOGS_QUERY_DEFAULT_LIMIT = 100;
7905
+ var LOGS_QUERY_MAX_LIMIT = 1e3;
7906
+ var buffers = /* @__PURE__ */ new Map();
7907
+ var KEY_SEP = "::";
7908
+ function bufferKey(projectName, source) {
7909
+ return `${projectName}${KEY_SEP}${source}`;
7910
+ }
7911
+ function sourcesForProject(projectName) {
7912
+ const prefix = `${projectName}${KEY_SEP}`;
7913
+ const sources = [];
7914
+ for (const key of buffers.keys()) {
7915
+ if (key.startsWith(prefix)) sources.push(key.slice(prefix.length));
7916
+ }
7917
+ return sources;
7918
+ }
7919
+ function queryLogEntries(opts) {
7920
+ const { projectName, source, service, since, limit } = opts;
7921
+ const sources = source && source.length > 0 ? source : sourcesForProject(projectName);
7922
+ let merged = [];
7923
+ for (const src of sources) {
7924
+ const buf = buffers.get(bufferKey(projectName, src));
7925
+ if (buf) merged = merged.concat(buf);
7926
+ }
7927
+ if (service) {
7928
+ merged = merged.filter((e) => e.serviceName === service);
7929
+ }
7930
+ if (since) {
7931
+ const sinceMs = Date.parse(since);
7932
+ if (!Number.isNaN(sinceMs)) {
7933
+ merged = merged.filter((e) => {
7934
+ const t = Date.parse(e.timestamp);
7935
+ return Number.isNaN(t) || t >= sinceMs;
7936
+ });
7937
+ }
7938
+ }
7939
+ merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
7940
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
7941
+ return merged.slice(0, limit);
7942
+ }
7943
+ return merged;
7944
+ }
7945
+
7615
7946
  // src/diff.ts
7616
7947
  init_cjs_shims();
7617
- var import_node_fs23 = require("fs");
7948
+ var import_node_fs24 = require("fs");
7618
7949
  async function loadSnapshotForDiff(target) {
7619
7950
  if (/^https?:\/\//i.test(target)) {
7620
7951
  const res = await fetch(target);
@@ -7623,7 +7954,7 @@ async function loadSnapshotForDiff(target) {
7623
7954
  }
7624
7955
  return await res.json();
7625
7956
  }
7626
- const raw = await import_node_fs23.promises.readFile(target, "utf8");
7957
+ const raw = await import_node_fs24.promises.readFile(target, "utf8");
7627
7958
  return JSON.parse(raw);
7628
7959
  }
7629
7960
  function indexEntries(entries) {
@@ -7691,23 +8022,23 @@ function canonicalJson(value) {
7691
8022
 
7692
8023
  // src/projects.ts
7693
8024
  init_cjs_shims();
7694
- var import_node_path40 = __toESM(require("path"), 1);
8025
+ var import_node_path41 = __toESM(require("path"), 1);
7695
8026
  function pathsForProject(project, baseDir) {
7696
8027
  if (project === DEFAULT_PROJECT) {
7697
8028
  return {
7698
- snapshotPath: import_node_path40.default.join(baseDir, "graph.json"),
7699
- errorsPath: import_node_path40.default.join(baseDir, "errors.ndjson"),
7700
- staleEventsPath: import_node_path40.default.join(baseDir, "stale-events.ndjson"),
7701
- embeddingsCachePath: import_node_path40.default.join(baseDir, "embeddings.json"),
7702
- policyViolationsPath: import_node_path40.default.join(baseDir, "policy-violations.ndjson")
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")
7703
8034
  };
7704
8035
  }
7705
8036
  return {
7706
- snapshotPath: import_node_path40.default.join(baseDir, `${project}.json`),
7707
- errorsPath: import_node_path40.default.join(baseDir, `errors.${project}.ndjson`),
7708
- staleEventsPath: import_node_path40.default.join(baseDir, `stale-events.${project}.ndjson`),
7709
- embeddingsCachePath: import_node_path40.default.join(baseDir, `embeddings.${project}.json`),
7710
- policyViolationsPath: import_node_path40.default.join(baseDir, `policy-violations.${project}.ndjson`)
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`)
7711
8042
  };
7712
8043
  }
7713
8044
  var Projects = class {
@@ -7743,25 +8074,25 @@ var Projects = class {
7743
8074
 
7744
8075
  // src/registry.ts
7745
8076
  init_cjs_shims();
7746
- var import_node_fs24 = require("fs");
8077
+ var import_node_fs25 = require("fs");
7747
8078
  var import_node_os3 = __toESM(require("os"), 1);
7748
- var import_node_path41 = __toESM(require("path"), 1);
7749
- var import_types28 = require("@neat.is/types");
8079
+ var import_node_path42 = __toESM(require("path"), 1);
8080
+ var import_types29 = require("@neat.is/types");
7750
8081
  var LOCK_TIMEOUT_MS = 5e3;
7751
8082
  var LOCK_RETRY_MS = 50;
7752
8083
  function neatHome() {
7753
8084
  const override = process.env.NEAT_HOME;
7754
- if (override && override.length > 0) return import_node_path41.default.resolve(override);
7755
- return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
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");
7756
8087
  }
7757
8088
  function registryPath() {
7758
- return import_node_path41.default.join(neatHome(), "projects.json");
8089
+ return import_node_path42.default.join(neatHome(), "projects.json");
7759
8090
  }
7760
8091
  function registryLockPath() {
7761
- return import_node_path41.default.join(neatHome(), "projects.json.lock");
8092
+ return import_node_path42.default.join(neatHome(), "projects.json.lock");
7762
8093
  }
7763
8094
  function daemonPidPath() {
7764
- return import_node_path41.default.join(neatHome(), "neatd.pid");
8095
+ return import_node_path42.default.join(neatHome(), "neatd.pid");
7765
8096
  }
7766
8097
  function isPidAliveDefault(pid) {
7767
8098
  try {
@@ -7773,7 +8104,7 @@ function isPidAliveDefault(pid) {
7773
8104
  }
7774
8105
  async function readPidFile(file) {
7775
8106
  try {
7776
- const raw = await import_node_fs24.promises.readFile(file, "utf8");
8107
+ const raw = await import_node_fs25.promises.readFile(file, "utf8");
7777
8108
  const pid = Number.parseInt(raw.trim(), 10);
7778
8109
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
7779
8110
  } catch {
@@ -7821,32 +8152,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
7821
8152
  }
7822
8153
  }
7823
8154
  async function normalizeProjectPath(input) {
7824
- const resolved = import_node_path41.default.resolve(input);
8155
+ const resolved = import_node_path42.default.resolve(input);
7825
8156
  try {
7826
- return await import_node_fs24.promises.realpath(resolved);
8157
+ return await import_node_fs25.promises.realpath(resolved);
7827
8158
  } catch {
7828
8159
  return resolved;
7829
8160
  }
7830
8161
  }
7831
8162
  async function writeAtomically(target, contents) {
7832
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(target), { recursive: true });
8163
+ await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(target), { recursive: true });
7833
8164
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
7834
- const fd = await import_node_fs24.promises.open(tmp, "w");
8165
+ const fd = await import_node_fs25.promises.open(tmp, "w");
7835
8166
  try {
7836
8167
  await fd.writeFile(contents, "utf8");
7837
8168
  await fd.sync();
7838
8169
  } finally {
7839
8170
  await fd.close();
7840
8171
  }
7841
- await import_node_fs24.promises.rename(tmp, target);
8172
+ await import_node_fs25.promises.rename(tmp, target);
7842
8173
  }
7843
8174
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
7844
8175
  const deadline = Date.now() + timeoutMs;
7845
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(lockPath), { recursive: true });
8176
+ await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(lockPath), { recursive: true });
7846
8177
  let probedHolder = false;
7847
8178
  while (true) {
7848
8179
  try {
7849
- const fd = await import_node_fs24.promises.open(lockPath, "wx");
8180
+ const fd = await import_node_fs25.promises.open(lockPath, "wx");
7850
8181
  try {
7851
8182
  await fd.writeFile(`${process.pid}
7852
8183
  `, "utf8");
@@ -7871,7 +8202,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
7871
8202
  }
7872
8203
  }
7873
8204
  async function releaseLock(lockPath) {
7874
- await import_node_fs24.promises.unlink(lockPath).catch(() => {
8205
+ await import_node_fs25.promises.unlink(lockPath).catch(() => {
7875
8206
  });
7876
8207
  }
7877
8208
  async function withLock(fn) {
@@ -7887,7 +8218,7 @@ async function readRegistry() {
7887
8218
  const file = registryPath();
7888
8219
  let raw;
7889
8220
  try {
7890
- raw = await import_node_fs24.promises.readFile(file, "utf8");
8221
+ raw = await import_node_fs25.promises.readFile(file, "utf8");
7891
8222
  } catch (err) {
7892
8223
  if (err.code === "ENOENT") {
7893
8224
  return { version: 1, projects: [] };
@@ -7895,10 +8226,10 @@ async function readRegistry() {
7895
8226
  throw err;
7896
8227
  }
7897
8228
  const parsed = JSON.parse(raw);
7898
- return import_types28.RegistryFileSchema.parse(parsed);
8229
+ return import_types29.RegistryFileSchema.parse(parsed);
7899
8230
  }
7900
8231
  async function writeRegistry(reg) {
7901
- const validated = import_types28.RegistryFileSchema.parse(reg);
8232
+ const validated = import_types29.RegistryFileSchema.parse(reg);
7902
8233
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
7903
8234
  }
7904
8235
  var ProjectNameCollisionError = class extends Error {
@@ -7992,7 +8323,7 @@ function pruneTtlMs() {
7992
8323
  }
7993
8324
  async function statPathStatus(p) {
7994
8325
  try {
7995
- const stat = await import_node_fs24.promises.stat(p);
8326
+ const stat = await import_node_fs25.promises.stat(p);
7996
8327
  return stat.isDirectory() ? "present" : "unknown";
7997
8328
  } catch (err) {
7998
8329
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -8239,11 +8570,11 @@ function registerRoutes(scope, ctx) {
8239
8570
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
8240
8571
  const parsed = [];
8241
8572
  for (const c of candidates) {
8242
- const r = import_types29.DivergenceTypeSchema.safeParse(c);
8573
+ const r = import_types30.DivergenceTypeSchema.safeParse(c);
8243
8574
  if (!r.success) {
8244
8575
  return reply.code(400).send({
8245
8576
  error: `unknown divergence type "${c}"`,
8246
- allowed: import_types29.DivergenceTypeSchema.options
8577
+ allowed: import_types30.DivergenceTypeSchema.options
8247
8578
  });
8248
8579
  }
8249
8580
  parsed.push(r.data);
@@ -8291,6 +8622,23 @@ function registerRoutes(scope, ctx) {
8291
8622
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
8292
8623
  return { count: sliced.length, total, events: sliced };
8293
8624
  });
8625
+ scope.get("/logs", async (req, reply) => {
8626
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
8627
+ if (!proj) return;
8628
+ const rawSource = req.query.source;
8629
+ const sources = rawSource === void 0 ? void 0 : Array.isArray(rawSource) ? rawSource : [rawSource];
8630
+ const filtered = queryLogEntries({
8631
+ projectName: proj.name,
8632
+ ...sources && sources.length > 0 ? { source: sources } : {},
8633
+ ...req.query.service ? { service: req.query.service } : {},
8634
+ ...req.query.since ? { since: req.query.since } : {}
8635
+ });
8636
+ const total = filtered.length;
8637
+ const limit = req.query.limit ? Number(req.query.limit) : LOGS_QUERY_DEFAULT_LIMIT;
8638
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, LOGS_QUERY_MAX_LIMIT) : LOGS_QUERY_DEFAULT_LIMIT;
8639
+ const sliced = filtered.slice(0, safeLimit);
8640
+ return { count: sliced.length, total, logs: sliced };
8641
+ });
8294
8642
  const incidentHistoryHandler = async (req, reply) => {
8295
8643
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
8296
8644
  if (!proj) return;
@@ -8386,8 +8734,16 @@ function registerRoutes(scope, ctx) {
8386
8734
  if (!against) {
8387
8735
  return reply.code(400).send({ error: "query parameter `against` is required" });
8388
8736
  }
8737
+ const targetProjectName = against === "self" ? proj.name : against;
8738
+ const targetProject = registry.get(targetProjectName);
8739
+ if (!targetProject) {
8740
+ return reply.code(400).send({
8741
+ error: "unknown snapshot id \u2014 `against` must be `self` or the name of a project this server has a snapshot for",
8742
+ against
8743
+ });
8744
+ }
8389
8745
  try {
8390
- const snapshot = await loadSnapshotForDiff(against);
8746
+ const snapshot = await loadSnapshotForDiff(targetProject.paths.snapshotPath);
8391
8747
  return computeGraphDiff(proj.graph, snapshot);
8392
8748
  } catch (err) {
8393
8749
  return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
@@ -8417,6 +8773,13 @@ function registerRoutes(scope, ctx) {
8417
8773
  edgeCount: proj.graph.size
8418
8774
  };
8419
8775
  } catch (err) {
8776
+ if (err instanceof SnapshotValidationError) {
8777
+ return reply.code(400).send({
8778
+ error: "snapshot merge failed",
8779
+ details: err.message,
8780
+ issues: err.issues
8781
+ });
8782
+ }
8420
8783
  return reply.code(400).send({
8421
8784
  error: "snapshot merge failed",
8422
8785
  details: err.message
@@ -8462,7 +8825,7 @@ function registerRoutes(scope, ctx) {
8462
8825
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
8463
8826
  let violations = await log.readAll();
8464
8827
  if (req.query.severity) {
8465
- const sev = import_types29.PolicySeveritySchema.safeParse(req.query.severity);
8828
+ const sev = import_types30.PolicySeveritySchema.safeParse(req.query.severity);
8466
8829
  if (!sev.success) {
8467
8830
  return reply.code(400).send({
8468
8831
  error: "invalid severity",
@@ -8501,7 +8864,7 @@ function registerRoutes(scope, ctx) {
8501
8864
  scope.post("/policies/check", async (req, reply) => {
8502
8865
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
8503
8866
  if (!proj) return;
8504
- const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8867
+ const parsed = import_types30.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8505
8868
  if (!parsed.success) {
8506
8869
  return reply.code(400).send({
8507
8870
  error: "invalid /policies/check body",
@@ -8763,8 +9126,8 @@ init_otel_grpc();
8763
9126
 
8764
9127
  // src/daemon.ts
8765
9128
  init_cjs_shims();
8766
- var import_node_fs26 = require("fs");
8767
- var import_node_path45 = __toESM(require("path"), 1);
9129
+ var import_node_fs28 = require("fs");
9130
+ var import_node_path47 = __toESM(require("path"), 1);
8768
9131
  var import_node_module = require("module");
8769
9132
  init_otel();
8770
9133
 
@@ -8782,6 +9145,10 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
8782
9145
  unresolved++;
8783
9146
  continue;
8784
9147
  }
9148
+ if (resolved.ensureInfraNode) {
9149
+ const { kind, name, provider } = resolved.ensureInfraNode;
9150
+ ensureInfraNode(graph, kind, name, provider);
9151
+ }
8785
9152
  const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
8786
9153
  const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
8787
9154
  const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
@@ -8845,64 +9212,1652 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
8845
9212
  };
8846
9213
  }
8847
9214
 
8848
- // src/daemon.ts
8849
- init_auth();
9215
+ // src/connectors/registry.ts
9216
+ init_cjs_shims();
8850
9217
 
8851
- // src/unrouted.ts
9218
+ // src/connectors/junction.ts
8852
9219
  init_cjs_shims();
8853
- var import_node_fs25 = require("fs");
8854
- var import_node_path44 = __toESM(require("path"), 1);
8855
- function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
8856
- return {
8857
- timestamp: now.toISOString(),
8858
- reason: "no-project-match",
8859
- service_name: serviceName ?? null,
8860
- traceId: traceId ?? null
8861
- };
9220
+ var buckets = /* @__PURE__ */ new Map();
9221
+ function bucketMapKey(provider, accountKey) {
9222
+ return `${provider}\0${accountKey}`;
9223
+ }
9224
+ function getBucket(provider, accountKey, config) {
9225
+ const key = bucketMapKey(provider, accountKey);
9226
+ const existing = buckets.get(key);
9227
+ if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {
9228
+ return existing;
9229
+ }
9230
+ const fresh = { ...config, tokens: config.capacity, updatedAt: Date.now() };
9231
+ buckets.set(key, fresh);
9232
+ return fresh;
9233
+ }
9234
+ function refillBucket(bucket, now) {
9235
+ if (now <= bucket.updatedAt) return;
9236
+ const elapsed = now - bucket.updatedAt;
9237
+ const grant = elapsed / bucket.refillMs;
9238
+ if (grant <= 0) return;
9239
+ bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant);
9240
+ bucket.updatedAt = now;
9241
+ }
9242
+ var RateLimitExceededError = class extends Error {
9243
+ constructor(provider, accountKey) {
9244
+ super(
9245
+ `junction: rate limit exceeded for ${provider}:${accountKey} \u2014 waiting for the next token would exceed this call's wall-clock budget`
9246
+ );
9247
+ this.name = "RateLimitExceededError";
9248
+ }
9249
+ };
9250
+ function delay(ms) {
9251
+ if (ms <= 0) return Promise.resolve();
9252
+ return new Promise((resolve) => {
9253
+ const timer = setTimeout(resolve, ms);
9254
+ if (typeof timer.unref === "function") timer.unref();
9255
+ });
8862
9256
  }
8863
- async function appendUnroutedSpan(neatHome2, record) {
8864
- const target = import_node_path44.default.join(neatHome2, "errors.ndjson");
8865
- await import_node_fs25.promises.mkdir(neatHome2, { recursive: true });
8866
- await import_node_fs25.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
9257
+ async function acquireToken(provider, accountKey, config, remainingBudgetMs) {
9258
+ const bucket = getBucket(provider, accountKey, config);
9259
+ refillBucket(bucket, Date.now());
9260
+ if (bucket.tokens >= 1) {
9261
+ bucket.tokens -= 1;
9262
+ return 0;
9263
+ }
9264
+ const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs);
9265
+ if (waitMs > remainingBudgetMs) {
9266
+ throw new RateLimitExceededError(provider, accountKey);
9267
+ }
9268
+ await delay(waitMs);
9269
+ refillBucket(bucket, Date.now());
9270
+ bucket.tokens = Math.max(0, bucket.tokens - 1);
9271
+ return waitMs;
9272
+ }
9273
+ var JUNCTION_DEFAULT_RATE_LIMITS = {
9274
+ // ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of
9275
+ // that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)
9276
+ // stays well clear of the documented limit even under sustained polling.
9277
+ cloudflare: { capacity: 100, refillMs: 3e3 },
9278
+ // Placeholder pending a live project confirming the real cap
9279
+ // (docs/connectors/railway.md: "does not appear to publish one as of this
9280
+ // writing").
9281
+ railway: { capacity: 30, refillMs: 1e4 },
9282
+ // Placeholder pending a live rate-limit check (docs/connectors/
9283
+ // firebase.md: "needs-endpoint-testing against entries.list's live rate
9284
+ // limits").
9285
+ firebase: { capacity: 30, refillMs: 1e4 },
9286
+ // Placeholder pending a live rate-limit check (docs/connectors/supabase.md:
9287
+ // "the documented rate limit for this specific endpoint is unconfirmed").
9288
+ supabase: { capacity: 30, refillMs: 1e4 },
9289
+ // Not a documented API limit at all — a self-imposed ceiling on the raw
9290
+ // pg_stat_statements connection (see module header above).
9291
+ "supabase-postgres": { capacity: 20, refillMs: 3e3 }
9292
+ };
9293
+ var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
9294
+ function defaultRateLimitFor(provider) {
9295
+ return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT;
9296
+ }
9297
+ var JUNCTION_DEFAULT_TIMEOUT_MS = 1e4;
9298
+ var JUNCTION_DEFAULT_MAX_ATTEMPTS = 3;
9299
+ var JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200;
9300
+ var JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4;
9301
+ var JUNCTION_DEFAULT_MAX_ELAPSED_MS = 3e4;
9302
+ var JUNCTION_DEFAULT_DB_TIMEOUT_MS = 1e4;
9303
+ async function backoff(attempt, initialBackoffMs, backoffMultiplier, remainingBudgetMs) {
9304
+ const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1);
9305
+ const capped = Math.max(0, Math.min(raw, remainingBudgetMs));
9306
+ await delay(capped);
9307
+ }
9308
+ function safeUrlLabel(url) {
9309
+ try {
9310
+ const u = typeof url === "string" ? new URL(url) : url;
9311
+ return `${u.origin}${u.pathname}`;
9312
+ } catch {
9313
+ return String(url);
9314
+ }
8867
9315
  }
8868
- function unroutedErrorsPath(neatHome2) {
8869
- return import_node_path44.default.join(neatHome2, "errors.ndjson");
9316
+ function logOutcome(provider, accountKey, outcome, method, label, attempt, startedAt) {
9317
+ const elapsedMs = Date.now() - startedAt;
9318
+ const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} \u2014 ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`;
9319
+ if (outcome === "success" || outcome === "retried-then-succeeded") {
9320
+ console.log(line);
9321
+ } else if (outcome === "rate-limited") {
9322
+ console.warn(line);
9323
+ } else {
9324
+ console.error(line);
9325
+ }
8870
9326
  }
8871
-
8872
- // src/daemon.ts
8873
- var import_types30 = require("@neat.is/types");
8874
- function daemonJsonPath(scanPath) {
8875
- return import_node_path45.default.join(scanPath, "neat-out", "daemon.json");
9327
+ function bearerAuthHeader(token) {
9328
+ return { Authorization: `Bearer ${token}` };
8876
9329
  }
8877
- function daemonsDiscoveryDir(home) {
8878
- const base = home && home.length > 0 ? home : neatHomeFromEnv();
8879
- return import_node_path45.default.join(base, "daemons");
9330
+ async function junctionFetch(url, init = {}, policy) {
9331
+ const {
9332
+ provider,
9333
+ accountKey,
9334
+ timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,
9335
+ maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
9336
+ maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
9337
+ initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
9338
+ backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
9339
+ rateLimit = defaultRateLimitFor(provider),
9340
+ fetchImpl = fetch
9341
+ } = policy;
9342
+ const method = (init.method ?? "GET").toUpperCase();
9343
+ const label = safeUrlLabel(url);
9344
+ const startedAt = Date.now();
9345
+ let attempt = 0;
9346
+ let sawRetry = false;
9347
+ for (; ; ) {
9348
+ attempt++;
9349
+ const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
9350
+ if (remainingBudget <= 0) {
9351
+ logOutcome(provider, accountKey, "retried-then-failed", method, label, attempt - 1, startedAt);
9352
+ throw new Error(
9353
+ `junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
9354
+ );
9355
+ }
9356
+ try {
9357
+ await acquireToken(provider, accountKey, rateLimit, remainingBudget);
9358
+ } catch (err) {
9359
+ if (err instanceof RateLimitExceededError) {
9360
+ logOutcome(provider, accountKey, "rate-limited", method, label, attempt, startedAt);
9361
+ }
9362
+ throw err;
9363
+ }
9364
+ const controller = new AbortController();
9365
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
9366
+ if (typeof timer.unref === "function") timer.unref();
9367
+ try {
9368
+ const res = await fetchImpl(url, { ...init, signal: controller.signal });
9369
+ clearTimeout(timer);
9370
+ if (res.ok || res.status < 500) {
9371
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", method, label, attempt, startedAt);
9372
+ return res;
9373
+ }
9374
+ if (attempt >= maxAttempts) {
9375
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
9376
+ return res;
9377
+ }
9378
+ sawRetry = true;
9379
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9380
+ } catch (err) {
9381
+ clearTimeout(timer);
9382
+ if (attempt >= maxAttempts) {
9383
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
9384
+ throw err;
9385
+ }
9386
+ sawRetry = true;
9387
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9388
+ }
9389
+ }
8880
9390
  }
8881
- function daemonDiscoveryPath(project, home) {
8882
- return import_node_path45.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
9391
+ var DbJunctionTimeoutError = class extends Error {
9392
+ constructor(ms) {
9393
+ super(`junction: db query exceeded its ${ms}ms timeout`);
9394
+ this.name = "DbJunctionTimeoutError";
9395
+ }
9396
+ };
9397
+ function withTimeout(run, timeoutMs) {
9398
+ return new Promise((resolve, reject) => {
9399
+ const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs);
9400
+ if (typeof timer.unref === "function") timer.unref();
9401
+ run().then(
9402
+ (value) => {
9403
+ clearTimeout(timer);
9404
+ resolve(value);
9405
+ },
9406
+ (err) => {
9407
+ clearTimeout(timer);
9408
+ reject(err);
9409
+ }
9410
+ );
9411
+ });
8883
9412
  }
8884
- function sanitizeDiscoveryName(project) {
8885
- return project.replace(/[^A-Za-z0-9._-]/g, "_");
9413
+ var RETRYABLE_NODE_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN", "EPIPE"]);
9414
+ function isRetryableDbError(err) {
9415
+ if (err instanceof DbJunctionTimeoutError) return true;
9416
+ const code = err?.code;
9417
+ if (typeof code !== "string") return false;
9418
+ if (code.startsWith("08") || code === "57P03") return true;
9419
+ return RETRYABLE_NODE_ERROR_CODES.has(code);
8886
9420
  }
8887
- function neatHomeFromEnv() {
8888
- const env = process.env.NEAT_HOME;
8889
- if (env && env.length > 0) return import_node_path45.default.resolve(env);
8890
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8891
- return import_node_path45.default.join(home, ".neat");
9421
+ async function dbJunction(run, policy) {
9422
+ const {
9423
+ provider,
9424
+ accountKey,
9425
+ timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,
9426
+ maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
9427
+ maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
9428
+ initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
9429
+ backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
9430
+ rateLimit = defaultRateLimitFor(provider)
9431
+ } = policy;
9432
+ const startedAt = Date.now();
9433
+ let attempt = 0;
9434
+ let sawRetry = false;
9435
+ for (; ; ) {
9436
+ attempt++;
9437
+ const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
9438
+ if (remainingBudget <= 0) {
9439
+ logOutcome(provider, accountKey, "retried-then-failed", "QUERY", "db", attempt - 1, startedAt);
9440
+ throw new Error(
9441
+ `junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
9442
+ );
9443
+ }
9444
+ try {
9445
+ await acquireToken(provider, accountKey, rateLimit, remainingBudget);
9446
+ } catch (err) {
9447
+ if (err instanceof RateLimitExceededError) {
9448
+ logOutcome(provider, accountKey, "rate-limited", "QUERY", "db", attempt, startedAt);
9449
+ }
9450
+ throw err;
9451
+ }
9452
+ try {
9453
+ const result = await withTimeout(run, timeoutMs);
9454
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", "QUERY", "db", attempt, startedAt);
9455
+ return result;
9456
+ } catch (err) {
9457
+ if (!isRetryableDbError(err) || attempt >= maxAttempts) {
9458
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", "QUERY", "db", attempt, startedAt);
9459
+ throw err;
9460
+ }
9461
+ sawRetry = true;
9462
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9463
+ }
9464
+ }
8892
9465
  }
8893
- function resolveNeatVersion() {
8894
- if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
8895
- return process.env.NEAT_LOCAL_VERSION;
9466
+
9467
+ // src/connectors/supabase/index.ts
9468
+ init_cjs_shims();
9469
+
9470
+ // src/connectors/supabase/client.ts
9471
+ init_cjs_shims();
9472
+ var DEFAULT_SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com";
9473
+ var DEFAULT_LOG_LIMIT = 1e3;
9474
+ var SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1e3;
9475
+ function boundedSupabaseLogWindow(since, now, maxLookbackMs) {
9476
+ const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS);
9477
+ const floor = new Date(now.getTime() - window);
9478
+ const endIso = now.toISOString();
9479
+ if (!since) return { startIso: floor.toISOString(), endIso, truncated: false };
9480
+ const sinceMs = new Date(since).getTime();
9481
+ if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false };
9482
+ if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true };
9483
+ return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false };
9484
+ }
9485
+ function buildEdgeLogsQuery(limit) {
9486
+ const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT);
9487
+ return [
9488
+ "select",
9489
+ " format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,",
9490
+ " request.method as method,",
9491
+ " request.path as path,",
9492
+ " response.status_code as status_code",
9493
+ "from edge_logs",
9494
+ "cross join unnest(metadata) as metadata",
9495
+ "cross join unnest(metadata.request) as request",
9496
+ "cross join unnest(metadata.response) as response",
9497
+ "where regexp_contains(request.path, '^/rest/v1/')",
9498
+ "order by timestamp asc",
9499
+ `limit ${safeLimit}`
9500
+ ].join("\n");
9501
+ }
9502
+ async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
9503
+ const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
9504
+ const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
9505
+ url.searchParams.set("sql", buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT));
9506
+ url.searchParams.set("iso_timestamp_start", startIso);
9507
+ url.searchParams.set("iso_timestamp_end", endIso);
9508
+ const res = await junctionFetch(
9509
+ url,
9510
+ { method: "GET", headers: bearerAuthHeader(token) },
9511
+ // accountKey: the Supabase project ref (ADR-131's own worked example) —
9512
+ // the Management API's rate limit is enforced per project.
9513
+ { provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
9514
+ );
9515
+ if (!res.ok) {
9516
+ throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
8896
9517
  }
8897
- try {
8898
- const req = (0, import_node_module.createRequire)(importMetaUrl);
8899
- const pkg = req("../package.json");
8900
- return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8901
- } catch {
8902
- return "0.0.0";
9518
+ const body = await res.json();
9519
+ 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})`);
8903
9522
  }
9523
+ return body.result ?? [];
8904
9524
  }
8905
- async function writeDaemonRecord(record, home) {
9525
+
9526
+ // src/connectors/supabase/map.ts
9527
+ init_cjs_shims();
9528
+
9529
+ // src/connectors/supabase/types.ts
9530
+ init_cjs_shims();
9531
+ function readSupabaseCredentials(raw) {
9532
+ const managementToken = raw["managementToken"];
9533
+ if (typeof managementToken !== "string" || managementToken.length === 0) {
9534
+ throw new Error("supabase connector: credentials.managementToken must be a non-empty string");
9535
+ }
9536
+ const postgresConnectionString = raw["postgresConnectionString"];
9537
+ if (postgresConnectionString !== void 0 && (typeof postgresConnectionString !== "string" || postgresConnectionString.length === 0)) {
9538
+ throw new Error(
9539
+ "supabase connector: credentials.postgresConnectionString must be a non-empty string when present"
9540
+ );
9541
+ }
9542
+ return {
9543
+ managementToken,
9544
+ ...postgresConnectionString ? { postgresConnectionString } : {}
9545
+ };
9546
+ }
9547
+ var SUPABASE_TABLE_TARGET_KIND = "supabase-table";
9548
+ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
9549
+
9550
+ // src/connectors/supabase/map.ts
9551
+ var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
9552
+ var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
9553
+ function targetFromRestPath(path48) {
9554
+ const rpcMatch = REST_RPC_PATH_RE.exec(path48);
9555
+ if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
9556
+ const tableMatch = REST_TABLE_PATH_RE.exec(path48);
9557
+ if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
9558
+ return null;
9559
+ }
9560
+ var ERROR_STATUS_THRESHOLD = 500;
9561
+ function mapEdgeLogRowsToSignals(rows) {
9562
+ const buckets2 = /* @__PURE__ */ new Map();
9563
+ for (const row of rows) {
9564
+ const target = targetFromRestPath(row.path);
9565
+ if (!target) continue;
9566
+ const key = `${target.targetKind}:${target.name}`;
9567
+ const isError = row.status_code >= ERROR_STATUS_THRESHOLD;
9568
+ const existing = buckets2.get(key);
9569
+ if (existing) {
9570
+ existing.callCount += 1;
9571
+ if (isError) existing.errorCount += 1;
9572
+ if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp;
9573
+ } else {
9574
+ buckets2.set(key, {
9575
+ targetKind: target.targetKind,
9576
+ targetName: target.name,
9577
+ callCount: 1,
9578
+ errorCount: isError ? 1 : 0,
9579
+ lastObservedIso: row.timestamp
9580
+ });
9581
+ }
9582
+ }
9583
+ return [...buckets2.values()].map((b) => ({
9584
+ targetKind: b.targetKind,
9585
+ targetName: b.targetName,
9586
+ callCount: b.callCount,
9587
+ errorCount: b.errorCount,
9588
+ lastObservedIso: b.lastObservedIso
9589
+ }));
9590
+ }
9591
+ var FROM_TABLE_RE = /\bfrom\s+"?(?:[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i;
9592
+ var SYSTEM_SCHEMA_PREFIXES = ["pg_", "information_schema"];
9593
+ function tableNameFromQueryText(query) {
9594
+ const match = FROM_TABLE_RE.exec(query);
9595
+ if (!match) return null;
9596
+ const name = match[1];
9597
+ const lower = name.toLowerCase();
9598
+ if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
9599
+ return name;
9600
+ }
9601
+ function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
9602
+ const signals = [];
9603
+ const seen = /* @__PURE__ */ new Set();
9604
+ for (const row of rows) {
9605
+ seen.add(row.queryid);
9606
+ const calls = Number(row.calls);
9607
+ const prior = previous.get(row.queryid);
9608
+ previous.set(row.queryid, { calls });
9609
+ if (!prior || calls < prior.calls) continue;
9610
+ const delta = calls - prior.calls;
9611
+ if (delta <= 0) continue;
9612
+ const table = tableNameFromQueryText(row.query);
9613
+ if (!table) continue;
9614
+ signals.push({
9615
+ targetKind: SUPABASE_TABLE_TARGET_KIND,
9616
+ targetName: table,
9617
+ callCount: delta,
9618
+ errorCount: 0,
9619
+ lastObservedIso: nowIso2
9620
+ });
9621
+ }
9622
+ for (const queryid of [...previous.keys()]) {
9623
+ if (!seen.has(queryid)) previous.delete(queryid);
9624
+ }
9625
+ return signals;
9626
+ }
9627
+
9628
+ // src/connectors/supabase/postgres-client.ts
9629
+ init_cjs_shims();
9630
+ var import_pg = __toESM(require("pg"), 1);
9631
+ var { Client } = import_pg.default;
9632
+ var DEFAULT_STATEMENT_LIMIT = 500;
9633
+ var STATEMENTS_QUERY = `
9634
+ select queryid, query, calls, total_exec_time, rows
9635
+ from pg_stat_statements
9636
+ where query ~* '^\\s*select\\b'
9637
+ order by calls desc
9638
+ limit $1
9639
+ `;
9640
+ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT_LIMIT, accountKey = "unknown", clientFactory = (cs) => new Client({ connectionString: cs })) {
9641
+ return dbJunction(
9642
+ async () => {
9643
+ const client = clientFactory(connectionString);
9644
+ await client.connect();
9645
+ try {
9646
+ await client.query("SET default_transaction_read_only = on");
9647
+ const result = await client.query(STATEMENTS_QUERY, [limit]);
9648
+ return result.rows;
9649
+ } finally {
9650
+ await client.end();
9651
+ }
9652
+ },
9653
+ { provider: "supabase-postgres", accountKey }
9654
+ );
9655
+ }
9656
+
9657
+ // src/connectors/supabase/resolve.ts
9658
+ init_cjs_shims();
9659
+ var import_types32 = require("@neat.is/types");
9660
+ function createSupabaseResolveTarget(graph, config) {
9661
+ return (signal, _ctx) => {
9662
+ if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
9663
+ return null;
9664
+ }
9665
+ const subResourceId = (0, import_types32.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
9666
+ if (graph.hasNode(subResourceId)) {
9667
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
9668
+ }
9669
+ const projectLevelId = (0, import_types32.infraId)("supabase", config.nodeRef);
9670
+ if (graph.hasNode(projectLevelId)) {
9671
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
9672
+ }
9673
+ return null;
9674
+ };
9675
+ }
9676
+
9677
+ // src/connectors/supabase/index.ts
9678
+ var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
9679
+ var SupabaseConnector = class {
9680
+ // `deps.fetchPgStatStatements` defaults to the real Postgres-backed
9681
+ // implementation; tests override it to exercise the "both surfaces
9682
+ // combine" and "surface 2 only runs when a connection string is present"
9683
+ // behavior without a live database — the same dependency-injection seam
9684
+ // `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.
9685
+ constructor(config, deps = {}) {
9686
+ this.config = config;
9687
+ this.deps = deps;
9688
+ }
9689
+ config;
9690
+ deps;
9691
+ provider = "supabase";
9692
+ // pg_stat_statements.calls is cumulative, not per-window (map.ts's
9693
+ // diffPgStatStatementsToSignals doc comment) — this Map carries the
9694
+ // previous poll's counts across ticks, the same way
9695
+ // `startConnectorPollLoop` (connectors/index.ts) carries `since` across
9696
+ // ticks for every connector. Lives on the instance, not `ConnectorContext`,
9697
+ // because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's
9698
+ // `{ ...ctx, since }`) while this connector object is the one thing every
9699
+ // tick shares.
9700
+ statementBaselines = /* @__PURE__ */ new Map();
9701
+ async poll(ctx) {
9702
+ const creds = readSupabaseCredentials(ctx.credentials);
9703
+ const now = /* @__PURE__ */ new Date();
9704
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS;
9705
+ const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs);
9706
+ const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso);
9707
+ const signals = mapEdgeLogRowsToSignals(logRows);
9708
+ if (creds.postgresConnectionString) {
9709
+ 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()));
9716
+ }
9717
+ return signals;
9718
+ }
9719
+ };
9720
+ function createSupabaseConnector(graph, config, deps = {}) {
9721
+ return {
9722
+ connector: new SupabaseConnector(config, deps),
9723
+ resolveTarget: createSupabaseResolveTarget(graph, config)
9724
+ };
9725
+ }
9726
+
9727
+ // src/connectors/railway/index.ts
9728
+ init_cjs_shims();
9729
+ var import_types36 = require("@neat.is/types");
9730
+
9731
+ // src/connectors/railway/client.ts
9732
+ init_cjs_shims();
9733
+ var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
9734
+ var DEFAULT_MAX_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
9735
+ var DEFAULT_LOG_LIMIT2 = 1e3;
9736
+ function readRailwayToken(credentials) {
9737
+ const token = credentials.token;
9738
+ if (typeof token !== "string" || token.length === 0) {
9739
+ throw new Error(
9740
+ "Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)"
9741
+ );
9742
+ }
9743
+ return token;
9744
+ }
9745
+ async function railwayGraphQL(apiUrl, token, query, variables, accountKey) {
9746
+ const res = await junctionFetch(
9747
+ apiUrl,
9748
+ {
9749
+ method: "POST",
9750
+ headers: {
9751
+ "Content-Type": "application/json",
9752
+ // docs.railway.com/integrations/api/graphql-overview confirms
9753
+ // `Authorization: Bearer <token>` for an account/workspace token;
9754
+ // Railway also documents a dedicated `Project-Access-Token: <token>`
9755
+ // header for the project-scoped token ADR-127 targets specifically.
9756
+ // This connector sends the Bearer form — needs-endpoint-testing
9757
+ // whether a live Project-Access-Token requires the dedicated header
9758
+ // instead once this poller runs against a real project.
9759
+ ...bearerAuthHeader(token)
9760
+ },
9761
+ body: JSON.stringify({ query, variables })
9762
+ },
9763
+ { provider: "railway", accountKey }
9764
+ );
9765
+ if (!res.ok) {
9766
+ throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`);
9767
+ }
9768
+ const body = await res.json();
9769
+ if (body.errors && body.errors.length > 0) {
9770
+ throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
9771
+ }
9772
+ if (!body.data) throw new Error("Railway GraphQL response carried no data");
9773
+ return body.data;
9774
+ }
9775
+ var HTTP_LOGS_QUERY = `
9776
+ query HttpLogs(
9777
+ $environmentId: String!
9778
+ $serviceId: String!
9779
+ $startDate: String!
9780
+ $endDate: String!
9781
+ $limit: Int
9782
+ ) {
9783
+ httpLogs(
9784
+ environmentId: $environmentId
9785
+ serviceId: $serviceId
9786
+ startDate: $startDate
9787
+ endDate: $endDate
9788
+ limit: $limit
9789
+ ) {
9790
+ timestamp
9791
+ method
9792
+ path
9793
+ httpStatus
9794
+ totalDuration
9795
+ requestId
9796
+ deploymentId
9797
+ edgeRegion
9798
+ }
9799
+ }
9800
+ `;
9801
+ var NETWORK_FLOW_LOGS_QUERY = `
9802
+ query NetworkFlowLogs(
9803
+ $environmentId: String!
9804
+ $serviceId: String!
9805
+ $startDate: String!
9806
+ $endDate: String!
9807
+ $limit: Int
9808
+ ) {
9809
+ networkFlowLogs(
9810
+ environmentId: $environmentId
9811
+ serviceId: $serviceId
9812
+ startDate: $startDate
9813
+ endDate: $endDate
9814
+ limit: $limit
9815
+ ) {
9816
+ timestamp
9817
+ peerServiceId
9818
+ peerKind
9819
+ direction
9820
+ byteCount
9821
+ packetCount
9822
+ dropCause
9823
+ }
9824
+ }
9825
+ `;
9826
+ async function fetchRailwayHttpLogs(config, token, startDate, endDate) {
9827
+ const data = await railwayGraphQL(
9828
+ config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
9829
+ token,
9830
+ HTTP_LOGS_QUERY,
9831
+ {
9832
+ environmentId: config.environmentId,
9833
+ serviceId: config.serviceId,
9834
+ startDate,
9835
+ endDate,
9836
+ limit: config.limit ?? DEFAULT_LOG_LIMIT2
9837
+ },
9838
+ config.environmentId
9839
+ );
9840
+ return data.httpLogs;
9841
+ }
9842
+ async function fetchRailwayNetworkFlowLogs(config, token, startDate, endDate) {
9843
+ const data = await railwayGraphQL(
9844
+ config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
9845
+ token,
9846
+ NETWORK_FLOW_LOGS_QUERY,
9847
+ {
9848
+ environmentId: config.environmentId,
9849
+ serviceId: config.serviceId,
9850
+ startDate,
9851
+ endDate,
9852
+ limit: config.limit ?? DEFAULT_LOG_LIMIT2
9853
+ },
9854
+ config.environmentId
9855
+ );
9856
+ return data.networkFlowLogs;
9857
+ }
9858
+ function boundedRailwayStartDate(since, now, maxLookbackMs) {
9859
+ const floor = new Date(now.getTime() - maxLookbackMs);
9860
+ if (!since) return floor.toISOString();
9861
+ const sinceMs = new Date(since).getTime();
9862
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
9863
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
9864
+ }
9865
+
9866
+ // src/connectors/railway/index.ts
9867
+ var ROUTE_TARGET_KIND = "route";
9868
+ var UNMATCHED_ROUTE_TARGET_KIND = "unmatched-route";
9869
+ var PEER_SERVICE_TARGET_KIND = "peer-service";
9870
+ function buildRailwayRouteIndex(graph, serviceName) {
9871
+ const out = [];
9872
+ graph.forEachNode((_id, attrs) => {
9873
+ const node = attrs;
9874
+ if (node.type !== import_types36.NodeType.RouteNode) return;
9875
+ const route = attrs;
9876
+ if (route.service !== serviceName) return;
9877
+ out.push({
9878
+ method: route.method.toUpperCase(),
9879
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
9880
+ routeNodeId: route.id,
9881
+ path: route.path,
9882
+ line: route.line
9883
+ });
9884
+ });
9885
+ return out;
9886
+ }
9887
+ function findRailwayRoute(entries, method, normalizedPath) {
9888
+ return entries.find(
9889
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
9890
+ );
9891
+ }
9892
+ function bucketKey2(method, normalizedPath) {
9893
+ return `${method} ${normalizedPath}`;
9894
+ }
9895
+ function isHttpErrorStatus(status2) {
9896
+ return status2 >= 400;
9897
+ }
9898
+ function upsertBucket(buckets2, key, isError, timestamp, build) {
9899
+ const existing = buckets2.get(key);
9900
+ if (existing) {
9901
+ existing.callCount += 1;
9902
+ if (isError) existing.errorCount += 1;
9903
+ if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
9904
+ return;
9905
+ }
9906
+ buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
9907
+ }
9908
+ function mapRailwayHttpLogsToSignals(entries, routeIndex) {
9909
+ const buckets2 = /* @__PURE__ */ new Map();
9910
+ for (const entry of entries) {
9911
+ const method = entry.method.toUpperCase();
9912
+ const normalizedPath = normalizePathTemplate(entry.path);
9913
+ const match = findRailwayRoute(routeIndex, method, normalizedPath);
9914
+ const isError = isHttpErrorStatus(entry.httpStatus);
9915
+ if (match) {
9916
+ upsertBucket(buckets2, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({
9917
+ targetKind: ROUTE_TARGET_KIND,
9918
+ targetName: match.routeNodeId,
9919
+ // RouteNode.line is optional in the schema (packages/types/src/
9920
+ // nodes.ts) even though routes.ts always sets it today — skip the
9921
+ // callSite rather than fabricate a line when it's ever absent
9922
+ // (file-awareness.md §6).
9923
+ ...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
9924
+ }));
9925
+ } else {
9926
+ upsertBucket(
9927
+ buckets2,
9928
+ `unmatched:${bucketKey2(method, normalizedPath)}`,
9929
+ isError,
9930
+ entry.timestamp,
9931
+ () => ({
9932
+ targetKind: UNMATCHED_ROUTE_TARGET_KIND,
9933
+ targetName: bucketKey2(method, normalizedPath)
9934
+ })
9935
+ );
9936
+ }
9937
+ }
9938
+ return [...buckets2.values()].map((b) => ({
9939
+ targetKind: b.targetKind,
9940
+ targetName: b.targetName,
9941
+ callCount: b.callCount,
9942
+ errorCount: b.errorCount,
9943
+ lastObservedIso: b.lastObservedIso,
9944
+ ...b.callSite ? { callSite: b.callSite } : {}
9945
+ }));
9946
+ }
9947
+ function mapRailwayNetworkFlowLogsToSignals(entries) {
9948
+ const buckets2 = /* @__PURE__ */ new Map();
9949
+ for (const entry of entries) {
9950
+ if (!entry.peerServiceId) continue;
9951
+ const isError = entry.dropCause !== null && entry.dropCause !== "";
9952
+ upsertBucket(buckets2, entry.peerServiceId, isError, entry.timestamp, () => ({
9953
+ targetKind: PEER_SERVICE_TARGET_KIND,
9954
+ targetName: entry.peerServiceId
9955
+ }));
9956
+ }
9957
+ return [...buckets2.values()].map((b) => ({
9958
+ targetKind: b.targetKind,
9959
+ targetName: b.targetName,
9960
+ callCount: b.callCount,
9961
+ errorCount: b.errorCount,
9962
+ lastObservedIso: b.lastObservedIso
9963
+ }));
9964
+ }
9965
+ function createRailwayResolveTarget(config) {
9966
+ return (signal) => {
9967
+ const serviceName = config.serviceNameById[config.serviceId];
9968
+ if (!serviceName) return null;
9969
+ if (signal.targetKind === ROUTE_TARGET_KIND) {
9970
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types36.EdgeType.CALLS };
9971
+ }
9972
+ if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
9973
+ const peerName = config.serviceNameById[signal.targetName];
9974
+ if (!peerName) return null;
9975
+ return { targetNodeId: (0, import_types36.serviceId)(peerName), serviceName, edgeType: import_types36.EdgeType.CONNECTS_TO };
9976
+ }
9977
+ return null;
9978
+ };
9979
+ }
9980
+ function createRailwayConnector(graph, config) {
9981
+ return {
9982
+ provider: "railway",
9983
+ async poll(ctx) {
9984
+ const token = readRailwayToken(ctx.credentials);
9985
+ const now = /* @__PURE__ */ new Date();
9986
+ const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS2;
9987
+ const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs);
9988
+ const endDate = now.toISOString();
9989
+ const [httpLogs, flowLogs] = await Promise.all([
9990
+ fetchRailwayHttpLogs(config, token, startDate, endDate),
9991
+ fetchRailwayNetworkFlowLogs(config, token, startDate, endDate)
9992
+ ]);
9993
+ const serviceName = config.serviceNameById[config.serviceId];
9994
+ const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : [];
9995
+ return [
9996
+ ...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),
9997
+ ...mapRailwayNetworkFlowLogsToSignals(flowLogs)
9998
+ ];
9999
+ }
10000
+ };
10001
+ }
10002
+
10003
+ // src/connectors/firebase/index.ts
10004
+ init_cjs_shims();
10005
+
10006
+ // src/connectors/firebase/logging-api.ts
10007
+ init_cjs_shims();
10008
+ function readFirebaseCredentials(raw) {
10009
+ const projectId = raw["projectId"];
10010
+ const accessToken = raw["accessToken"];
10011
+ if (typeof projectId !== "string" || projectId.length === 0) {
10012
+ throw new Error("firebase connector: credentials.projectId must be a non-empty string");
10013
+ }
10014
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
10015
+ throw new Error("firebase connector: credentials.accessToken must be a non-empty string");
10016
+ }
10017
+ return { projectId, accessToken };
10018
+ }
10019
+ var RESOURCE_TYPES = [
10020
+ "cloud_function",
10021
+ "cloud_run_revision",
10022
+ "firebase_domain"
10023
+ ];
10024
+ function isFirebaseResourceType(value) {
10025
+ return RESOURCE_TYPES.includes(value);
10026
+ }
10027
+ function buildEntriesFilter(sinceIso) {
10028
+ return [
10029
+ 'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
10030
+ "httpRequest:*",
10031
+ `timestamp >= "${sinceIso}"`
10032
+ ].join(" AND ");
10033
+ }
10034
+ var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
10035
+ var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
10036
+ var PAGE_SIZE = 1e3;
10037
+ var MAX_PAGES = 20;
10038
+ async function fetchHttpRequestLogEntries(creds, sinceIso) {
10039
+ const filter = buildEntriesFilter(sinceIso);
10040
+ const out = [];
10041
+ let pageToken;
10042
+ for (let page = 0; page < MAX_PAGES; page++) {
10043
+ const body = {
10044
+ resourceNames: [`projects/${creds.projectId}`],
10045
+ filter,
10046
+ orderBy: "timestamp asc",
10047
+ pageSize: PAGE_SIZE,
10048
+ ...pageToken ? { pageToken } : {}
10049
+ };
10050
+ const res = await junctionFetch(
10051
+ ENTRIES_LIST_URL,
10052
+ {
10053
+ method: "POST",
10054
+ headers: {
10055
+ ...bearerAuthHeader(creds.accessToken),
10056
+ "Content-Type": "application/json"
10057
+ },
10058
+ body: JSON.stringify(body)
10059
+ },
10060
+ // accountKey: the GCP project id (ADR-131's own worked example for
10061
+ // Firebase) — one customer's Cloud Logging quota is scoped per GCP
10062
+ // project, not per Firebase site/function.
10063
+ { provider: "firebase", accountKey: creds.projectId }
10064
+ );
10065
+ if (!res.ok) {
10066
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
10067
+ }
10068
+ const json = await res.json();
10069
+ out.push(...json.entries ?? []);
10070
+ if (!json.nextPageToken) break;
10071
+ pageToken = json.nextPageToken;
10072
+ }
10073
+ return out;
10074
+ }
10075
+
10076
+ // src/connectors/firebase/map.ts
10077
+ init_cjs_shims();
10078
+ var FIELD_SEP = "\0";
10079
+ function packFirebaseTargetName(identity) {
10080
+ return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP);
10081
+ }
10082
+ function parseFirebaseTargetName(targetName) {
10083
+ const firstSep = targetName.indexOf(FIELD_SEP);
10084
+ if (firstSep === -1) return null;
10085
+ const resourceName = targetName.slice(0, firstSep);
10086
+ const rest = targetName.slice(firstSep + 1);
10087
+ const secondSep = rest.indexOf(FIELD_SEP);
10088
+ if (secondSep === -1) return null;
10089
+ 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 };
10093
+ }
10094
+ function resourceNameFor(type, labels) {
10095
+ if (!labels) return null;
10096
+ switch (type) {
10097
+ case "cloud_function":
10098
+ return labels["function_name"] ?? null;
10099
+ case "cloud_run_revision":
10100
+ return labels["service_name"] ?? null;
10101
+ case "firebase_domain":
10102
+ return labels["site_name"] ?? null;
10103
+ }
10104
+ }
10105
+ function pathFromRequestUrl(requestUrl) {
10106
+ if (!requestUrl) return null;
10107
+ if (requestUrl.startsWith("/")) {
10108
+ const withoutQuery = requestUrl.split("?")[0];
10109
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
10110
+ }
10111
+ try {
10112
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
10113
+ const parsed = new URL(candidate);
10114
+ return parsed.pathname || "/";
10115
+ } catch {
10116
+ return null;
10117
+ }
10118
+ }
10119
+ var ERROR_STATUS_THRESHOLD2 = 500;
10120
+ function mapLogEntryToSignal(entry) {
10121
+ const resourceType = entry.resource?.type;
10122
+ if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
10123
+ const resourceName = resourceNameFor(resourceType, entry.resource?.labels);
10124
+ if (!resourceName) return null;
10125
+ const req = entry.httpRequest;
10126
+ if (!req) return null;
10127
+ if (!req.requestMethod) return null;
10128
+ const method = req.requestMethod.toUpperCase();
10129
+ const path48 = pathFromRequestUrl(req.requestUrl);
10130
+ if (path48 === null) return null;
10131
+ const timestamp = entry.timestamp;
10132
+ if (!timestamp) return null;
10133
+ const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
10134
+ return {
10135
+ targetKind: resourceType,
10136
+ targetName: packFirebaseTargetName({ resourceName, method, path: path48 }),
10137
+ callCount: 1,
10138
+ errorCount: isError ? 1 : 0,
10139
+ lastObservedIso: timestamp
10140
+ };
10141
+ }
10142
+ function mapLogEntriesToSignals(entries) {
10143
+ const out = [];
10144
+ for (const entry of entries) {
10145
+ const signal = mapLogEntryToSignal(entry);
10146
+ if (signal) out.push(signal);
10147
+ }
10148
+ return out;
10149
+ }
10150
+
10151
+ // src/connectors/firebase/resolve.ts
10152
+ init_cjs_shims();
10153
+ var import_types37 = require("@neat.is/types");
10154
+ function neatServiceNameFor(resourceType, resourceName, serviceMap) {
10155
+ switch (resourceType) {
10156
+ case "cloud_function":
10157
+ return serviceMap.functions?.[resourceName] ?? null;
10158
+ case "cloud_run_revision":
10159
+ return serviceMap.cloudRun?.[resourceName] ?? null;
10160
+ case "firebase_domain":
10161
+ return serviceMap.hosting?.[resourceName] ?? null;
10162
+ }
10163
+ }
10164
+ function routeEntriesFor(graph, serviceName) {
10165
+ const entries = [];
10166
+ graph.forEachNode((_id, attrs) => {
10167
+ const node = attrs;
10168
+ if (node.type !== import_types37.NodeType.RouteNode) return;
10169
+ const route = attrs;
10170
+ if (route.service !== serviceName) return;
10171
+ entries.push({
10172
+ method: route.method.toUpperCase(),
10173
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
10174
+ routeNodeId: route.id
10175
+ });
10176
+ });
10177
+ return entries;
10178
+ }
10179
+ function findRoute2(entries, method, normalizedPath) {
10180
+ return entries.find(
10181
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
10182
+ );
10183
+ }
10184
+ function createFirebaseResolveTarget(graph, serviceMap) {
10185
+ return (signal, _ctx) => {
10186
+ const resourceType = signal.targetKind;
10187
+ if (resourceType !== "cloud_function" && resourceType !== "cloud_run_revision" && resourceType !== "firebase_domain") {
10188
+ return null;
10189
+ }
10190
+ const identity = parseFirebaseTargetName(signal.targetName);
10191
+ if (!identity) return null;
10192
+ const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
10193
+ if (!serviceName) return null;
10194
+ const normalizedPath = normalizePathTemplate(identity.path);
10195
+ const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
10196
+ if (!match) return null;
10197
+ return {
10198
+ targetNodeId: match.routeNodeId,
10199
+ serviceName,
10200
+ edgeType: import_types37.EdgeType.CALLS
10201
+ };
10202
+ };
10203
+ }
10204
+
10205
+ // src/connectors/firebase/index.ts
10206
+ var FirebaseConnector = class {
10207
+ provider = "firebase";
10208
+ async poll(ctx) {
10209
+ const creds = readFirebaseCredentials(ctx.credentials);
10210
+ const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
10211
+ const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
10212
+ return mapLogEntriesToSignals(entries);
10213
+ }
10214
+ };
10215
+ function createFirebaseConnector(graph, serviceMap) {
10216
+ return {
10217
+ connector: new FirebaseConnector(),
10218
+ resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
10219
+ };
10220
+ }
10221
+
10222
+ // src/connectors/cloudflare/index.ts
10223
+ init_cjs_shims();
10224
+
10225
+ // src/connectors/cloudflare/connector.ts
10226
+ init_cjs_shims();
10227
+ var import_types39 = require("@neat.is/types");
10228
+
10229
+ // src/connectors/cloudflare/client.ts
10230
+ init_cjs_shims();
10231
+ var import_node_crypto3 = require("crypto");
10232
+ var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
10233
+ var DEFAULT_EVENT_LIMIT = 1e3;
10234
+ async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
10235
+ const token = ctx.credentials.apiToken;
10236
+ if (typeof token !== "string" || token.length === 0) {
10237
+ throw new Error("cloudflare connector: ctx.credentials.apiToken must be a non-empty string");
10238
+ }
10239
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
10240
+ const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`;
10241
+ const body = {
10242
+ // Cloudflare's schema requires an identifier per query even for an
10243
+ // ad-hoc, unsaved one — a fresh id per tick, never reused.
10244
+ queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
10245
+ timeframe: { from: window.fromMs, to: window.toMs },
10246
+ view: "events",
10247
+ limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
10248
+ // Execute without persisting — this is a read, not a saved query
10249
+ // (connectors.md §2's "never writes on the read path" applies to
10250
+ // Cloudflare's own query-history state too).
10251
+ dry: true
10252
+ };
10253
+ const res = await junctionFetch(
10254
+ url,
10255
+ {
10256
+ method: "POST",
10257
+ headers: {
10258
+ "Content-Type": "application/json",
10259
+ ...bearerAuthHeader(token)
10260
+ },
10261
+ body: JSON.stringify(body)
10262
+ },
10263
+ // accountKey: the Cloudflare account id (ADR-131's own worked example) —
10264
+ // the Telemetry Query API's ~300/5min limit is enforced per account.
10265
+ { provider: "cloudflare", accountKey: config.accountId, fetchImpl }
10266
+ );
10267
+ if (!res.ok) {
10268
+ throw new Error(
10269
+ `cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`
10270
+ );
10271
+ }
10272
+ const payload = await res.json();
10273
+ if (!payload.success) {
10274
+ const message = payload.errors?.map((e) => e.message).join("; ") || "unknown error";
10275
+ throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
10276
+ }
10277
+ const events = payload.result?.events?.events;
10278
+ if (events === void 0) {
10279
+ console.warn(
10280
+ "[neat connector] cloudflare: telemetry query returned success:true but no result.events.events array \u2014 the response shape may have changed; treating as zero events this tick"
10281
+ );
10282
+ return [];
10283
+ }
10284
+ return events;
10285
+ }
10286
+
10287
+ // src/connectors/cloudflare/map.ts
10288
+ init_cjs_shims();
10289
+
10290
+ // src/connectors/cloudflare/types.ts
10291
+ init_cjs_shims();
10292
+ var CLOUDFLARE_TARGET_KIND = "cloudflare-worker-invocation";
10293
+
10294
+ // src/connectors/cloudflare/map.ts
10295
+ var HTTP_METHODS = /* @__PURE__ */ new Set([
10296
+ "GET",
10297
+ "POST",
10298
+ "PUT",
10299
+ "PATCH",
10300
+ "DELETE",
10301
+ "HEAD",
10302
+ "OPTIONS",
10303
+ "TRACE",
10304
+ "CONNECT"
10305
+ ]);
10306
+ var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
10307
+ function parseHttpMethodFromTrigger(trigger) {
10308
+ if (!trigger) return null;
10309
+ const match = LEADING_TOKEN_RE.exec(trigger.trim());
10310
+ const token = match?.[1];
10311
+ if (!token) return null;
10312
+ const method = token.toUpperCase();
10313
+ return HTTP_METHODS.has(method) ? method : null;
10314
+ }
10315
+ function parsePathFromTrigger(trigger) {
10316
+ const trimmed = trigger.trim();
10317
+ const spaceIdx = trimmed.indexOf(" ");
10318
+ if (spaceIdx === -1) return void 0;
10319
+ const rest = trimmed.slice(spaceIdx + 1).trim();
10320
+ return rest.length > 0 ? rest : void 0;
10321
+ }
10322
+ var ERROR_STATUS_THRESHOLD3 = 500;
10323
+ function mapEventToSignal(event) {
10324
+ const metadata = event.$metadata;
10325
+ const workers = event.$workers;
10326
+ const method = parseHttpMethodFromTrigger(metadata?.trigger);
10327
+ if (!method) return null;
10328
+ const scriptName = workers?.scriptName ?? metadata?.service;
10329
+ if (!scriptName) return null;
10330
+ const timestampMs = event.timestamp ?? metadata?.startTime;
10331
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
10332
+ const statusCode = metadata?.statusCode;
10333
+ const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
10334
+ const path48 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10335
+ return {
10336
+ targetKind: CLOUDFLARE_TARGET_KIND,
10337
+ targetName: scriptName,
10338
+ callCount: 1,
10339
+ errorCount: isError ? 1 : 0,
10340
+ lastObservedIso: new Date(timestampMs).toISOString(),
10341
+ method,
10342
+ ...path48 ? { path: path48 } : {},
10343
+ ...typeof statusCode === "number" ? { statusCode } : {},
10344
+ ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
10345
+ };
10346
+ }
10347
+
10348
+ // src/connectors/cloudflare/connector.ts
10349
+ var DEFAULT_MAX_LOOKBACK_MS3 = 60 * 60 * 1e3;
10350
+ function resolveFromMs(since, maxLookbackMs) {
10351
+ const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS3;
10352
+ const now = Date.now();
10353
+ const floor = now - cap;
10354
+ if (!since) return floor;
10355
+ const parsed = Date.parse(since);
10356
+ if (Number.isNaN(parsed)) return floor;
10357
+ return Math.max(parsed, floor);
10358
+ }
10359
+ var CloudflareConnector = class {
10360
+ constructor(config) {
10361
+ this.config = config;
10362
+ }
10363
+ config;
10364
+ provider = "cloudflare";
10365
+ async poll(ctx) {
10366
+ const toMs = Date.now();
10367
+ const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs);
10368
+ const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs });
10369
+ const signals = [];
10370
+ for (const event of events) {
10371
+ const signal = mapEventToSignal(event);
10372
+ if (signal) signals.push(signal);
10373
+ }
10374
+ return signals;
10375
+ }
10376
+ };
10377
+ function findTaggedWorkerFileNode(graph, workerName) {
10378
+ let found = null;
10379
+ graph.forEachNode((id, attrs) => {
10380
+ if (found) return;
10381
+ const a = attrs;
10382
+ if (a.type === import_types39.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10383
+ found = id;
10384
+ }
10385
+ });
10386
+ return found;
10387
+ }
10388
+ function findMatchingRouteNode(graph, serviceName, method, path48) {
10389
+ const normalizedPath = normalizePathTemplate(path48);
10390
+ let found = null;
10391
+ graph.forEachNode((id, attrs) => {
10392
+ if (found) return;
10393
+ const a = attrs;
10394
+ if (a.type !== import_types39.NodeType.RouteNode || a.service !== serviceName) return;
10395
+ if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
10396
+ const routeMethod = (a.method ?? "").toUpperCase();
10397
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
10398
+ found = id;
10399
+ });
10400
+ return found;
10401
+ }
10402
+ function createCloudflareResolveTarget(config, graph) {
10403
+ return (signal) => {
10404
+ if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
10405
+ const scriptName = signal.targetName;
10406
+ const { method, path: path48 } = signal;
10407
+ const resolveRouteGrain = (serviceName, wholeFileId) => {
10408
+ if (!method || !path48) return wholeFileId;
10409
+ return findMatchingRouteNode(graph, serviceName, method, path48) ?? wholeFileId;
10410
+ };
10411
+ const mapping = config.workers?.[scriptName];
10412
+ if (mapping) {
10413
+ const wholeFileId = (0, import_types39.fileId)(mapping.service, mapping.entryFile);
10414
+ return {
10415
+ targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
10416
+ serviceName: mapping.service,
10417
+ edgeType: import_types39.EdgeType.CALLS
10418
+ };
10419
+ }
10420
+ const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
10421
+ if (taggedFileId) {
10422
+ const fileNode = graph.getNodeAttributes(taggedFileId);
10423
+ return {
10424
+ targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
10425
+ serviceName: fileNode.service,
10426
+ edgeType: import_types39.EdgeType.CALLS
10427
+ };
10428
+ }
10429
+ return {
10430
+ targetNodeId: (0, import_types39.infraId)("cloudflare-worker", scriptName),
10431
+ serviceName: scriptName,
10432
+ edgeType: import_types39.EdgeType.CALLS,
10433
+ ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
10434
+ };
10435
+ };
10436
+ }
10437
+
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
+ // src/connectors/registry.ts
10590
+ var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
10591
+ async function authProbe(input) {
10592
+ const { provider, accountKey, url, token, init, fetchImpl } = input;
10593
+ try {
10594
+ const res = await junctionFetch(
10595
+ url,
10596
+ {
10597
+ ...init ?? {},
10598
+ headers: { ...bearerAuthHeader(token), ...init?.headers ?? {} }
10599
+ },
10600
+ { provider, accountKey, ...fetchImpl ? { fetchImpl } : {} }
10601
+ );
10602
+ if (res.ok) return { ok: true };
10603
+ if (res.status === 401 || res.status === 403) {
10604
+ return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` };
10605
+ }
10606
+ return {
10607
+ ok: false,
10608
+ reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} \u2014 could not confirm the credential`
10609
+ };
10610
+ } catch (err) {
10611
+ return {
10612
+ ok: false,
10613
+ reason: `${provider} auth check could not reach the provider: ${err.message}`
10614
+ };
10615
+ }
10616
+ }
10617
+ var PROVIDER_DISPATCH = {
10618
+ supabase: {
10619
+ provider: "supabase",
10620
+ primaryCredentialKey: "managementToken",
10621
+ requiredCredentialFields: ["managementToken"],
10622
+ requiredOptionFields: ["apiProjectRef", "nodeRef", "serviceName"],
10623
+ build(graph, options) {
10624
+ return createSupabaseConnector(graph, options);
10625
+ },
10626
+ // GET /v1/projects — the Management API's own auth-gated list endpoint, the
10627
+ // cheapest confirmation the management token is live (the same surface
10628
+ // client.ts polls, minus the heavy log query).
10629
+ validate({ credentials, options, fetchImpl }) {
10630
+ const cfg = options;
10631
+ const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
10632
+ return authProbe({
10633
+ provider: "supabase",
10634
+ accountKey: cfg.apiProjectRef ?? "validate",
10635
+ url: `${baseUrl}/v1/projects`,
10636
+ token: String(credentials.managementToken ?? ""),
10637
+ ...fetchImpl ? { fetchImpl } : {}
10638
+ });
10639
+ }
10640
+ },
10641
+ railway: {
10642
+ provider: "railway",
10643
+ primaryCredentialKey: "token",
10644
+ requiredCredentialFields: ["token"],
10645
+ requiredOptionFields: ["environmentId", "serviceId", "serviceNameById"],
10646
+ build(graph, options) {
10647
+ const config = options;
10648
+ return {
10649
+ connector: createRailwayConnector(graph, config),
10650
+ resolveTarget: createRailwayResolveTarget(config)
10651
+ };
10652
+ },
10653
+ // A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call
10654
+ // at the HTTP layer, so a 2xx confirms the token was accepted regardless of
10655
+ // the query's own shape (the connector's real queries are still
10656
+ // needs-endpoint-testing per railway/types.ts — auth is what we check).
10657
+ validate({ credentials, options, fetchImpl }) {
10658
+ const cfg = options;
10659
+ return authProbe({
10660
+ provider: "railway",
10661
+ accountKey: cfg.environmentId ?? "validate",
10662
+ url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,
10663
+ token: String(credentials.token ?? ""),
10664
+ init: {
10665
+ method: "POST",
10666
+ headers: { "Content-Type": "application/json" },
10667
+ body: JSON.stringify({ query: "{ __typename }" })
10668
+ },
10669
+ ...fetchImpl ? { fetchImpl } : {}
10670
+ });
10671
+ }
10672
+ },
10673
+ firebase: {
10674
+ provider: "firebase",
10675
+ // Firebase reads both projectId and accessToken from the credential; the
10676
+ // single-string form maps to the secret, and the required-fields check
10677
+ // below catches a projectId that was never supplied.
10678
+ primaryCredentialKey: "accessToken",
10679
+ requiredCredentialFields: ["projectId", "accessToken"],
10680
+ requiredOptionFields: [],
10681
+ build(graph, options) {
10682
+ return createFirebaseConnector(graph, options);
10683
+ },
10684
+ // GET the project's Cloud Logging log-name list (pageSize 1) — within the
10685
+ // same `roles/logging.viewer` grant the connector polls under, and the
10686
+ // lightest call that still fails 401/403 on a bad or wrong-scoped token.
10687
+ validate({ credentials, fetchImpl }) {
10688
+ const projectId = String(credentials.projectId ?? "");
10689
+ return authProbe({
10690
+ provider: "firebase",
10691
+ accountKey: projectId || "validate",
10692
+ url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,
10693
+ token: String(credentials.accessToken ?? ""),
10694
+ ...fetchImpl ? { fetchImpl } : {}
10695
+ });
10696
+ }
10697
+ },
10698
+ cloudflare: {
10699
+ provider: "cloudflare",
10700
+ primaryCredentialKey: "apiToken",
10701
+ requiredCredentialFields: ["apiToken"],
10702
+ // `workers` dropped as a required field (ADR-133) — the mapping is now
10703
+ // derived from the extracted graph's platform tag; an `options.workers`
10704
+ // entry still works as an explicit override
10705
+ // (CloudflareConnectorConfig.workers).
10706
+ requiredOptionFields: ["accountId"],
10707
+ build(graph, options) {
10708
+ const config = options;
10709
+ return {
10710
+ connector: new CloudflareConnector(config),
10711
+ resolveTarget: createCloudflareResolveTarget(config, graph)
10712
+ };
10713
+ },
10714
+ // GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
10715
+ // token live" endpoint. 200 on a valid token, 401 on an invalid one.
10716
+ validate({ credentials, options, fetchImpl }) {
10717
+ const cfg = options;
10718
+ const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
10719
+ return authProbe({
10720
+ provider: "cloudflare",
10721
+ accountKey: cfg.accountId ?? "validate",
10722
+ url: `${baseUrl}/user/tokens/verify`,
10723
+ token: String(credentials.apiToken ?? ""),
10724
+ ...fetchImpl ? { fetchImpl } : {}
10725
+ });
10726
+ }
10727
+ }
10728
+ };
10729
+ function resolveEntryCredentials(dispatch, entry, env) {
10730
+ let credentials;
10731
+ try {
10732
+ const resolved = resolveCredential(entry.credential, env);
10733
+ credentials = resolved.kind === "single" ? { [dispatch.primaryCredentialKey]: resolved.value } : { ...resolved.fields };
10734
+ } catch (err) {
10735
+ if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
10736
+ return { ok: false, kind: "error", reason: err.message };
10737
+ }
10738
+ const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
10739
+ if (missingCreds.length > 0) {
10740
+ return {
10741
+ ok: false,
10742
+ kind: "missing-field",
10743
+ reason: `credential missing required field(s): ${missingCreds.join(", ")}`
10744
+ };
10745
+ }
10746
+ return { ok: true, credentials };
10747
+ }
10748
+ function buildRegistration(entry, graph, env = process.env) {
10749
+ const dispatch = PROVIDER_DISPATCH[entry.provider];
10750
+ if (!dispatch) {
10751
+ return { ok: false, reason: `unknown provider "${entry.provider}"` };
10752
+ }
10753
+ const creds = resolveEntryCredentials(dispatch, entry, env);
10754
+ if (!creds.ok) return { ok: false, reason: creds.reason };
10755
+ const credentials = creds.credentials;
10756
+ const options = entry.options ?? {};
10757
+ const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
10758
+ if (missingOpts.length > 0) {
10759
+ return {
10760
+ ok: false,
10761
+ reason: `options missing required field(s): ${missingOpts.join(", ")}`
10762
+ };
10763
+ }
10764
+ let built;
10765
+ try {
10766
+ built = dispatch.build(graph, options);
10767
+ } catch (err) {
10768
+ return { ok: false, reason: err.message };
10769
+ }
10770
+ const intervalMs = typeof options.intervalMs === "number" ? options.intervalMs : void 0;
10771
+ return {
10772
+ ok: true,
10773
+ registration: {
10774
+ connector: built.connector,
10775
+ credentials,
10776
+ resolveTarget: built.resolveTarget,
10777
+ ...intervalMs !== void 0 ? { intervalMs } : {}
10778
+ }
10779
+ };
10780
+ }
10781
+ async function loadConnectorRegistrations(input) {
10782
+ const { project, graph, home, env = process.env, onSkip } = input;
10783
+ let connectors;
10784
+ try {
10785
+ connectors = (await readConnectorsConfig(home)).connectors;
10786
+ } catch (err) {
10787
+ onSkip?.(
10788
+ { id: "(file)", provider: "(all)", credential: "" },
10789
+ `connectors.json unreadable \u2014 ${err.message}`
10790
+ );
10791
+ return [];
10792
+ }
10793
+ const registrations = [];
10794
+ for (const entry of connectors) {
10795
+ if (!connectorMatchesProject(entry, project)) continue;
10796
+ const result = buildRegistration(entry, graph, env);
10797
+ if (result.ok) registrations.push(result.registration);
10798
+ else onSkip?.(entry, result.reason);
10799
+ }
10800
+ return registrations;
10801
+ }
10802
+
10803
+ // src/daemon.ts
10804
+ init_auth();
10805
+
10806
+ // src/unrouted.ts
10807
+ init_cjs_shims();
10808
+ var import_node_fs27 = require("fs");
10809
+ var import_node_path46 = __toESM(require("path"), 1);
10810
+ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
10811
+ return {
10812
+ timestamp: now.toISOString(),
10813
+ reason: "no-project-match",
10814
+ service_name: serviceName ?? null,
10815
+ traceId: traceId ?? null
10816
+ };
10817
+ }
10818
+ 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");
10822
+ }
10823
+ function unroutedErrorsPath(neatHome3) {
10824
+ return import_node_path46.default.join(neatHome3, "errors.ndjson");
10825
+ }
10826
+
10827
+ // src/daemon.ts
10828
+ var import_types42 = require("@neat.is/types");
10829
+ function daemonJsonPath(scanPath) {
10830
+ return import_node_path47.default.join(scanPath, "neat-out", "daemon.json");
10831
+ }
10832
+ function daemonsDiscoveryDir(home) {
10833
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
10834
+ return import_node_path47.default.join(base, "daemons");
10835
+ }
10836
+ function daemonDiscoveryPath(project, home) {
10837
+ return import_node_path47.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
10838
+ }
10839
+ function sanitizeDiscoveryName(project) {
10840
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
10841
+ }
10842
+ function neatHomeFromEnv() {
10843
+ const env = process.env.NEAT_HOME;
10844
+ if (env && env.length > 0) return import_node_path47.default.resolve(env);
10845
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10846
+ return import_node_path47.default.join(home, ".neat");
10847
+ }
10848
+ function resolveNeatVersion() {
10849
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
10850
+ return process.env.NEAT_LOCAL_VERSION;
10851
+ }
10852
+ try {
10853
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
10854
+ const pkg = req("../package.json");
10855
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
10856
+ } catch {
10857
+ return "0.0.0";
10858
+ }
10859
+ }
10860
+ async function writeDaemonRecord(record, home) {
8906
10861
  const body = JSON.stringify(record, null, 2) + "\n";
8907
10862
  await writeAtomically(daemonJsonPath(record.projectPath), body);
8908
10863
  try {
@@ -8920,7 +10875,7 @@ async function clearDaemonRecord(record, home) {
8920
10875
  } catch {
8921
10876
  }
8922
10877
  try {
8923
- await import_node_fs26.promises.unlink(daemonDiscoveryPath(record.project, home));
10878
+ await import_node_fs28.promises.unlink(daemonDiscoveryPath(record.project, home));
8924
10879
  } catch {
8925
10880
  }
8926
10881
  }
@@ -8943,11 +10898,11 @@ function teardownSlot(slot) {
8943
10898
  }
8944
10899
  }
8945
10900
  function neatHomeFor(opts) {
8946
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path45.default.resolve(opts.neatHome);
10901
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path47.default.resolve(opts.neatHome);
8947
10902
  const env = process.env.NEAT_HOME;
8948
- if (env && env.length > 0) return import_node_path45.default.resolve(env);
10903
+ if (env && env.length > 0) return import_node_path47.default.resolve(env);
8949
10904
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8950
- return import_node_path45.default.join(home, ".neat");
10905
+ return import_node_path47.default.join(home, ".neat");
8951
10906
  }
8952
10907
  function routeSpanToProject(serviceName, projects) {
8953
10908
  if (!serviceName) return DEFAULT_PROJECT;
@@ -8991,13 +10946,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
8991
10946
  if (!serviceName) return true;
8992
10947
  if (serviceNameMatchesProject(serviceName, project)) return true;
8993
10948
  return graph.someNode(
8994
- (_id, attrs) => attrs.type === import_types30.NodeType.ServiceNode && attrs.name === serviceName
10949
+ (_id, attrs) => attrs.type === import_types42.NodeType.ServiceNode && attrs.name === serviceName
8995
10950
  );
8996
10951
  }
8997
- async function bootstrapProject(entry, connectors = []) {
8998
- const paths = pathsForProject(entry.name, import_node_path45.default.join(entry.path, "neat-out"));
10952
+ async function bootstrapProject(entry, connectors = [], neatHome3) {
10953
+ const paths = pathsForProject(entry.name, import_node_path47.default.join(entry.path, "neat-out"));
8999
10954
  try {
9000
- const stat = await import_node_fs26.promises.stat(entry.path);
10955
+ const stat = await import_node_fs28.promises.stat(entry.path);
9001
10956
  if (!stat.isDirectory()) {
9002
10957
  throw new Error(`registered path ${entry.path} is not a directory`);
9003
10958
  }
@@ -9035,7 +10990,16 @@ async function bootstrapProject(entry, connectors = []) {
9035
10990
  staleEventsPath: paths.staleEventsPath,
9036
10991
  project: entry.name
9037
10992
  });
9038
- const stopFns = connectors.map(
10993
+ const fileConnectors = neatHome3 ? await loadConnectorRegistrations({
10994
+ project: entry.name,
10995
+ graph,
10996
+ home: neatHome3,
10997
+ onSkip: (skipped, reason) => console.warn(
10998
+ `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
10999
+ )
11000
+ }) : [];
11001
+ const allConnectors = [...connectors, ...fileConnectors];
11002
+ const stopFns = allConnectors.map(
9039
11003
  (registration) => startConnectorPollLoop(
9040
11004
  registration.connector,
9041
11005
  { projectDir: entry.path, credentials: registration.credentials },
@@ -9113,7 +11077,7 @@ async function startDaemon(opts = {}) {
9113
11077
  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;
9114
11078
  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;
9115
11079
  const singleProject = projectArg;
9116
- const singleProjectPath = singleProject && projectPathArg ? import_node_path45.default.resolve(projectPathArg) : null;
11080
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path47.default.resolve(projectPathArg) : null;
9117
11081
  if (singleProject && !singleProjectPath) {
9118
11082
  throw new Error(
9119
11083
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -9121,14 +11085,14 @@ async function startDaemon(opts = {}) {
9121
11085
  }
9122
11086
  if (!singleProject) {
9123
11087
  try {
9124
- await import_node_fs26.promises.access(regPath);
11088
+ await import_node_fs28.promises.access(regPath);
9125
11089
  } catch {
9126
11090
  throw new Error(
9127
11091
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
9128
11092
  );
9129
11093
  }
9130
11094
  }
9131
- const pidPath = import_node_path45.default.join(home, "neatd.pid");
11095
+ const pidPath = import_node_path47.default.join(home, "neatd.pid");
9132
11096
  await writeAtomically(pidPath, `${process.pid}
9133
11097
  `);
9134
11098
  const slots = /* @__PURE__ */ new Map();
@@ -9172,7 +11136,7 @@ async function startDaemon(opts = {}) {
9172
11136
  }
9173
11137
  async function tryRecoverSlot(entry) {
9174
11138
  try {
9175
- const fresh = await bootstrapProject(entry, opts.connectors ?? []);
11139
+ const fresh = await bootstrapProject(entry, opts.connectors ?? [], home);
9176
11140
  const prior = slots.get(entry.name);
9177
11141
  if (prior) teardownSlot(prior);
9178
11142
  slots.set(entry.name, fresh);
@@ -9196,7 +11160,7 @@ async function startDaemon(opts = {}) {
9196
11160
  bootstrapStatus.set(entry.name, "bootstrapping");
9197
11161
  bootstrapStartedAt.set(entry.name, Date.now());
9198
11162
  try {
9199
- const slot = await bootstrapProject(entry, opts.connectors ?? []);
11163
+ const slot = await bootstrapProject(entry, opts.connectors ?? [], home);
9200
11164
  const prior = slots.get(entry.name);
9201
11165
  if (prior) teardownSlot(prior);
9202
11166
  slots.set(entry.name, slot);
@@ -9322,7 +11286,7 @@ async function startDaemon(opts = {}) {
9322
11286
  }
9323
11287
  if (restApp) await restApp.close().catch(() => {
9324
11288
  });
9325
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11289
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9326
11290
  });
9327
11291
  throw new Error(
9328
11292
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -9448,7 +11412,7 @@ async function startDaemon(opts = {}) {
9448
11412
  });
9449
11413
  if (otlpApp) await otlpApp.close().catch(() => {
9450
11414
  });
9451
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11415
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9452
11416
  });
9453
11417
  throw new Error(
9454
11418
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -9483,7 +11447,7 @@ async function startDaemon(opts = {}) {
9483
11447
  });
9484
11448
  if (otlpApp) await otlpApp.close().catch(() => {
9485
11449
  });
9486
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11450
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9487
11451
  });
9488
11452
  throw new Error(
9489
11453
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -9530,9 +11494,9 @@ async function startDaemon(opts = {}) {
9530
11494
  let registryWatcher = null;
9531
11495
  let reloadTimer = null;
9532
11496
  if (!singleProject) try {
9533
- const regDir = import_node_path45.default.dirname(regPath);
9534
- const regBase = import_node_path45.default.basename(regPath);
9535
- registryWatcher = (0, import_node_fs26.watch)(regDir, (_eventType, filename) => {
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) => {
9536
11500
  if (filename !== null && filename !== regBase) return;
9537
11501
  if (reloadTimer) clearTimeout(reloadTimer);
9538
11502
  reloadTimer = setTimeout(() => {
@@ -9581,7 +11545,7 @@ async function startDaemon(opts = {}) {
9581
11545
  if (daemonRecord) {
9582
11546
  await clearDaemonRecord(daemonRecord, home);
9583
11547
  }
9584
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11548
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9585
11549
  });
9586
11550
  };
9587
11551
  return {