@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/neatd.cjs CHANGED
@@ -55,15 +55,16 @@ function mountBearerAuth(app, opts) {
55
55
  if (!opts.token || opts.token.length === 0) return;
56
56
  if (opts.trustProxy) return;
57
57
  const expected = Buffer.from(opts.token, "utf8");
58
- const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
+ const exactUnauthPaths = /* @__PURE__ */ new Set([
59
+ ...DEFAULT_UNAUTH_PATHS,
60
+ ...opts.extraUnauthenticatedSuffixes ?? []
61
+ ]);
59
62
  const publicRead = opts.publicRead === true;
60
63
  app.addHook("preHandler", (req2, reply, done) => {
61
- const path48 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
62
- for (const suffix of suffixes) {
63
- if (path48 === suffix || path48.endsWith(suffix)) {
64
- done();
65
- return;
66
- }
64
+ const path50 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path50) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path50)) {
66
+ done();
67
+ return;
67
68
  }
68
69
  if (publicRead && PUBLIC_READ_METHODS.has(req2.method)) {
69
70
  done();
@@ -98,7 +99,7 @@ function readAuthEnv(env = process.env) {
98
99
  publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
99
100
  };
100
101
  }
101
- var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
102
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
102
103
  var init_auth = __esm({
103
104
  "src/auth.ts"() {
104
105
  "use strict";
@@ -119,12 +120,15 @@ var init_auth = __esm({
119
120
  }
120
121
  };
121
122
  PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
122
- DEFAULT_UNAUTH_SUFFIXES = [
123
+ DEFAULT_UNAUTH_PATHS = [
123
124
  "/health",
124
125
  "/healthz",
125
126
  "/readyz",
126
127
  "/api/config"
127
128
  ];
129
+ PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
130
+ `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
131
+ );
128
132
  }
129
133
  });
130
134
 
@@ -190,8 +194,8 @@ function reshapeGrpcRequest(req2) {
190
194
  };
191
195
  }
192
196
  function resolveProtoRoot() {
193
- const here = import_node_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
194
- return import_node_path42.default.resolve(here, "..", "proto");
197
+ const here = import_node_path43.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
198
+ return import_node_path43.default.resolve(here, "..", "proto");
195
199
  }
196
200
  function loadTraceService() {
197
201
  const protoRoot = resolveProtoRoot();
@@ -259,13 +263,13 @@ async function startOtelGrpcReceiver(opts) {
259
263
  })
260
264
  };
261
265
  }
262
- var import_node_url, import_node_path42, import_node_crypto2, grpc, protoLoader;
266
+ var import_node_url, import_node_path43, import_node_crypto2, grpc, protoLoader;
263
267
  var init_otel_grpc = __esm({
264
268
  "src/otel-grpc.ts"() {
265
269
  "use strict";
266
270
  init_cjs_shims();
267
271
  import_node_url = require("url");
268
- import_node_path42 = __toESM(require("path"), 1);
272
+ import_node_path43 = __toESM(require("path"), 1);
269
273
  import_node_crypto2 = require("crypto");
270
274
  grpc = __toESM(require("@grpc/grpc-js"), 1);
271
275
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -358,8 +362,8 @@ function websocketChannelPathOf(attrs) {
358
362
  const v = attrs[key];
359
363
  if (typeof v === "string" && v.length > 0) {
360
364
  const q = v.indexOf("?");
361
- const path48 = q === -1 ? v : v.slice(0, q);
362
- if (path48.length > 0) return path48;
365
+ const path50 = q === -1 ? v : v.slice(0, q);
366
+ if (path50.length > 0) return path50;
363
367
  }
364
368
  }
365
369
  return void 0;
@@ -413,10 +417,10 @@ function parseOtlpRequest(body) {
413
417
  return out;
414
418
  }
415
419
  function loadProtoRoot() {
416
- const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
417
- const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
420
+ const here = import_node_path44.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
421
+ const protoRoot = import_node_path44.default.resolve(here, "..", "proto");
418
422
  const root = new import_protobufjs.default.Root();
419
- root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
423
+ root.resolvePath = (_origin, target) => import_node_path44.default.resolve(protoRoot, target);
420
424
  root.loadSync(
421
425
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
422
426
  { keepCase: true }
@@ -644,12 +648,12 @@ async function listenSteppingOtlp(app, requestedPort, host) {
644
648
  }
645
649
  }
646
650
  }
647
- 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;
651
+ 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;
648
652
  var init_otel = __esm({
649
653
  "src/otel.ts"() {
650
654
  "use strict";
651
655
  init_cjs_shims();
652
- import_node_path43 = __toESM(require("path"), 1);
656
+ import_node_path44 = __toESM(require("path"), 1);
653
657
  import_node_url2 = require("url");
654
658
  import_fastify2 = __toESM(require("fastify"), 1);
655
659
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -672,14 +676,14 @@ __export(neatd_exports, {
672
676
  });
673
677
  module.exports = __toCommonJS(neatd_exports);
674
678
  init_cjs_shims();
675
- var import_node_fs28 = require("fs");
676
- var import_node_path47 = __toESM(require("path"), 1);
679
+ var import_node_fs30 = require("fs");
680
+ var import_node_path49 = __toESM(require("path"), 1);
677
681
  var import_node_module2 = require("module");
678
682
 
679
683
  // src/daemon.ts
680
684
  init_cjs_shims();
681
- var import_node_fs26 = require("fs");
682
- var import_node_path45 = __toESM(require("path"), 1);
685
+ var import_node_fs28 = require("fs");
686
+ var import_node_path47 = __toESM(require("path"), 1);
683
687
  var import_node_module = require("module");
684
688
 
685
689
  // src/graph.ts
@@ -1209,19 +1213,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1209
1213
  function longestIncomingWalk(graph, start, maxDepth) {
1210
1214
  let best = { path: [start], edges: [] };
1211
1215
  const visited = /* @__PURE__ */ new Set([start]);
1212
- function step(node, path48, edges) {
1213
- if (path48.length > best.path.length) {
1214
- best = { path: [...path48], edges: [...edges] };
1216
+ function step(node, path50, edges) {
1217
+ if (path50.length > best.path.length) {
1218
+ best = { path: [...path50], edges: [...edges] };
1215
1219
  }
1216
- if (path48.length - 1 >= maxDepth) return;
1220
+ if (path50.length - 1 >= maxDepth) return;
1217
1221
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1218
1222
  for (const [srcId, edge] of incoming) {
1219
1223
  if (visited.has(srcId)) continue;
1220
1224
  visited.add(srcId);
1221
- path48.push(srcId);
1225
+ path50.push(srcId);
1222
1226
  edges.push(edge);
1223
- step(srcId, path48, edges);
1224
- path48.pop();
1227
+ step(srcId, path50, edges);
1228
+ path50.pop();
1225
1229
  edges.pop();
1226
1230
  visited.delete(srcId);
1227
1231
  }
@@ -1236,7 +1240,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1236
1240
  for (const id of walk3.path) {
1237
1241
  const owner = resolveOwningService(graph, id);
1238
1242
  if (!owner) continue;
1239
- const { id: serviceId4, svc } = owner;
1243
+ const { id: serviceId5, svc } = owner;
1240
1244
  const deps = svc.dependencies ?? {};
1241
1245
  for (const pair of candidatePairs) {
1242
1246
  const declared = deps[pair.driver];
@@ -1249,7 +1253,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1249
1253
  );
1250
1254
  if (!result.compatible) {
1251
1255
  return {
1252
- rootCauseNode: serviceId4,
1256
+ rootCauseNode: serviceId5,
1253
1257
  rootCauseReason: result.reason ?? "incompatible driver",
1254
1258
  ...result.minDriverVersion ? {
1255
1259
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1264,7 +1268,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1264
1268
  for (const id of walk3.path) {
1265
1269
  const owner = resolveOwningService(graph, id);
1266
1270
  if (!owner) continue;
1267
- const { id: serviceId4, svc } = owner;
1271
+ const { id: serviceId5, svc } = owner;
1268
1272
  const deps = svc.dependencies ?? {};
1269
1273
  const serviceNodeEngine = svc.nodeEngine;
1270
1274
  for (const constraint of nodeEngineConstraints()) {
@@ -1273,7 +1277,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1273
1277
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1274
1278
  if (!result.compatible && result.reason) {
1275
1279
  return {
1276
- rootCauseNode: serviceId4,
1280
+ rootCauseNode: serviceId5,
1277
1281
  rootCauseReason: result.reason,
1278
1282
  ...result.requiredNodeVersion ? {
1279
1283
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1288,7 +1292,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1288
1292
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1289
1293
  if (!result.compatible && result.reason) {
1290
1294
  return {
1291
- rootCauseNode: serviceId4,
1295
+ rootCauseNode: serviceId5,
1292
1296
  rootCauseReason: result.reason,
1293
1297
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1294
1298
  };
@@ -1379,9 +1383,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1379
1383
  function isFailingCallEdge(e) {
1380
1384
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1381
1385
  }
1382
- function callSourcesForService(graph, serviceId4) {
1383
- const ids = [serviceId4];
1384
- for (const edgeId of graph.outboundEdges(serviceId4)) {
1386
+ function callSourcesForService(graph, serviceId5) {
1387
+ const ids = [serviceId5];
1388
+ for (const edgeId of graph.outboundEdges(serviceId5)) {
1385
1389
  const e = graph.getEdgeAttributes(edgeId);
1386
1390
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1387
1391
  const tgt = graph.getNodeAttributes(e.target);
@@ -1398,9 +1402,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1398
1402
  }
1399
1403
  return id < curId;
1400
1404
  }
1401
- function dominantFailingCall(graph, serviceId4, visited) {
1405
+ function dominantFailingCall(graph, serviceId5, visited) {
1402
1406
  let best = null;
1403
- for (const src of callSourcesForService(graph, serviceId4)) {
1407
+ for (const src of callSourcesForService(graph, serviceId5)) {
1404
1408
  for (const edgeId of graph.outboundEdges(src)) {
1405
1409
  const e = graph.getEdgeAttributes(edgeId);
1406
1410
  if (!isFailingCallEdge(e)) continue;
@@ -1415,26 +1419,26 @@ function dominantFailingCall(graph, serviceId4, visited) {
1415
1419
  return best;
1416
1420
  }
1417
1421
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1418
- const path48 = [originServiceId];
1422
+ const path50 = [originServiceId];
1419
1423
  const edges = [];
1420
1424
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1421
1425
  let current = originServiceId;
1422
1426
  for (let depth = 0; depth < maxDepth; depth++) {
1423
1427
  const hop = dominantFailingCall(graph, current, visited);
1424
1428
  if (!hop) break;
1425
- path48.push(hop.nextService);
1429
+ path50.push(hop.nextService);
1426
1430
  edges.push(hop.edge);
1427
1431
  visited.add(hop.nextService);
1428
1432
  current = hop.nextService;
1429
1433
  }
1430
1434
  if (edges.length === 0) return null;
1431
- return { path: path48, edges, culprit: current };
1435
+ return { path: path50, edges, culprit: current };
1432
1436
  }
1433
1437
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1434
1438
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1435
1439
  if (!chain) return null;
1436
1440
  const culprit = chain.culprit;
1437
- const path48 = [...chain.path];
1441
+ const path50 = [...chain.path];
1438
1442
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1439
1443
  const baseConfidence = confidenceFromMix(chain.edges);
1440
1444
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1442,14 +1446,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1442
1446
  if (loc) {
1443
1447
  let rootCauseNode = culprit;
1444
1448
  if (loc.fileNode) {
1445
- path48.push(loc.fileNode);
1449
+ path50.push(loc.fileNode);
1446
1450
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1447
1451
  rootCauseNode = loc.fileNode;
1448
1452
  }
1449
1453
  return import_types.RootCauseResultSchema.parse({
1450
1454
  rootCauseNode,
1451
1455
  rootCauseReason: loc.rootCauseReason,
1452
- traversalPath: path48,
1456
+ traversalPath: path50,
1453
1457
  edgeProvenances,
1454
1458
  confidence,
1455
1459
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1461,7 +1465,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1461
1465
  return import_types.RootCauseResultSchema.parse({
1462
1466
  rootCauseNode: culprit,
1463
1467
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1464
- traversalPath: path48,
1468
+ traversalPath: path50,
1465
1469
  edgeProvenances,
1466
1470
  confidence,
1467
1471
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2538,6 +2542,19 @@ function ensureServiceNode(graph, serviceName, env) {
2538
2542
  graph.addNode(id, node);
2539
2543
  return id;
2540
2544
  }
2545
+ function ensureInfraNode(graph, kind, name, provider) {
2546
+ const id = (0, import_types3.infraId)(kind, name);
2547
+ if (graph.hasNode(id)) return id;
2548
+ const node = {
2549
+ id,
2550
+ type: import_types3.NodeType.InfraNode,
2551
+ name,
2552
+ provider,
2553
+ kind
2554
+ };
2555
+ graph.addNode(id, node);
2556
+ return id;
2557
+ }
2541
2558
  function ensureDatabaseNode(graph, host, engine) {
2542
2559
  const id = (0, import_types3.databaseId)(host);
2543
2560
  if (graph.hasNode(id)) return id;
@@ -3017,29 +3034,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3017
3034
  toPromote.push({ frontierId: id, serviceId: target });
3018
3035
  });
3019
3036
  let promoted = 0;
3020
- for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
3037
+ for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
3021
3038
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3022
3039
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3023
3040
  if (!gate.allowed) {
3024
3041
  continue;
3025
3042
  }
3026
3043
  }
3027
- rewireFrontierEdges(graph, frontierId2, serviceId4);
3044
+ rewireFrontierEdges(graph, frontierId2, serviceId5);
3028
3045
  graph.dropNode(frontierId2);
3029
3046
  promoted++;
3030
3047
  }
3031
3048
  return promoted;
3032
3049
  }
3033
- function rewireFrontierEdges(graph, frontierId2, serviceId4) {
3050
+ function rewireFrontierEdges(graph, frontierId2, serviceId5) {
3034
3051
  const inbound = [...graph.inboundEdges(frontierId2)];
3035
3052
  const outbound = [...graph.outboundEdges(frontierId2)];
3036
3053
  for (const edgeId of inbound) {
3037
3054
  const edge = graph.getEdgeAttributes(edgeId);
3038
- rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
3055
+ rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
3039
3056
  }
3040
3057
  for (const edgeId of outbound) {
3041
3058
  const edge = graph.getEdgeAttributes(edgeId);
3042
- rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
3059
+ rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
3043
3060
  }
3044
3061
  }
3045
3062
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3187,24 +3204,58 @@ function dedupeIncidents(events) {
3187
3204
  return !hasRealFailure.has(groupKey(ev));
3188
3205
  });
3189
3206
  }
3207
+ var SnapshotValidationError = class extends Error {
3208
+ constructor(issues) {
3209
+ super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
3210
+ this.issues = issues;
3211
+ this.name = "SnapshotValidationError";
3212
+ }
3213
+ issues;
3214
+ };
3215
+ function describeZodIssues(error) {
3216
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
3217
+ }
3190
3218
  function mergeSnapshot(graph, snapshot) {
3191
3219
  const exported = snapshot.graph;
3220
+ const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
3221
+ const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
3222
+ const issues = [];
3223
+ const validNodes = [];
3224
+ const validEdges = [];
3225
+ for (const node of incomingNodes) {
3226
+ if (node.attributes === void 0) continue;
3227
+ const parsed = import_types3.GraphNodeSchema.safeParse(node.attributes);
3228
+ if (!parsed.success) {
3229
+ issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
3230
+ continue;
3231
+ }
3232
+ validNodes.push({ key: node.key, attributes: parsed.data });
3233
+ }
3234
+ for (const edge of incomingEdges) {
3235
+ if (edge.attributes === void 0) continue;
3236
+ const parsed = import_types3.GraphEdgeSchema.safeParse(edge.attributes);
3237
+ if (!parsed.success) {
3238
+ const label = edge.key ?? `${edge.source}->${edge.target}`;
3239
+ issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
3240
+ continue;
3241
+ }
3242
+ const id = edge.key ?? parsed.data.id;
3243
+ validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
3244
+ }
3245
+ if (issues.length > 0) {
3246
+ throw new SnapshotValidationError(issues);
3247
+ }
3192
3248
  let nodesAdded = 0;
3193
3249
  let edgesAdded = 0;
3194
- for (const node of exported.nodes ?? []) {
3250
+ for (const node of validNodes) {
3195
3251
  if (graph.hasNode(node.key)) continue;
3196
- if (!node.attributes) continue;
3197
3252
  graph.addNode(node.key, node.attributes);
3198
3253
  nodesAdded++;
3199
3254
  }
3200
- for (const edge of exported.edges ?? []) {
3201
- const attrs = edge.attributes;
3202
- if (!attrs) continue;
3203
- const id = edge.key ?? attrs.id;
3204
- if (!id) continue;
3205
- if (graph.hasEdge(id)) continue;
3255
+ for (const edge of validEdges) {
3256
+ if (graph.hasEdge(edge.key)) continue;
3206
3257
  if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
3207
- graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
3258
+ graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
3208
3259
  edgesAdded++;
3209
3260
  }
3210
3261
  return { nodesAdded, edgesAdded };
@@ -3798,9 +3849,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3798
3849
  "StatefulSet",
3799
3850
  "DaemonSet"
3800
3851
  ]);
3801
- function addAliases(graph, serviceId4, candidates) {
3802
- if (!graph.hasNode(serviceId4)) return;
3803
- const node = graph.getNodeAttributes(serviceId4);
3852
+ function addAliases(graph, serviceId5, candidates) {
3853
+ if (!graph.hasNode(serviceId5)) return;
3854
+ const node = graph.getNodeAttributes(serviceId5);
3804
3855
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3805
3856
  const set = new Set(node.aliases ?? []);
3806
3857
  for (const c of candidates) {
@@ -3810,7 +3861,7 @@ function addAliases(graph, serviceId4, candidates) {
3810
3861
  }
3811
3862
  if (set.size === 0) return;
3812
3863
  const updated = { ...node, aliases: [...set].sort() };
3813
- graph.replaceNodeAttributes(serviceId4, updated);
3864
+ graph.replaceNodeAttributes(serviceId5, updated);
3814
3865
  }
3815
3866
  function indexServicesByName(services) {
3816
3867
  const map = /* @__PURE__ */ new Map();
@@ -3843,12 +3894,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3843
3894
  }
3844
3895
  if (!compose?.services) return;
3845
3896
  for (const [composeName, svc] of Object.entries(compose.services)) {
3846
- const serviceId4 = serviceIndex.get(composeName);
3847
- if (!serviceId4) continue;
3897
+ const serviceId5 = serviceIndex.get(composeName);
3898
+ if (!serviceId5) continue;
3848
3899
  const aliases = /* @__PURE__ */ new Set([composeName]);
3849
3900
  if (svc.container_name) aliases.add(svc.container_name);
3850
3901
  if (svc.hostname) aliases.add(svc.hostname);
3851
- addAliases(graph, serviceId4, aliases);
3902
+ addAliases(graph, serviceId5, aliases);
3852
3903
  }
3853
3904
  }
3854
3905
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -5259,10 +5310,10 @@ function fastifyRouteMethods(objNode) {
5259
5310
  }
5260
5311
  return [];
5261
5312
  }
5262
- function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5313
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
5263
5314
  const tree = parseSource2(parser, source);
5264
5315
  const out = [];
5265
- const framework = hasExpress ? "express" : "fastify";
5316
+ const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
5266
5317
  walk(tree.rootNode, (node) => {
5267
5318
  if (node.type !== "call_expression") return;
5268
5319
  const fn = node.childForFieldName("function");
@@ -5412,8 +5463,9 @@ async function addRoutes(graph, services) {
5412
5463
  };
5413
5464
  const hasExpress = deps["express"] !== void 0;
5414
5465
  const hasFastify = deps["fastify"] !== void 0;
5466
+ const hasHono = deps["hono"] !== void 0;
5415
5467
  const hasNext = deps["next"] !== void 0;
5416
- if (!hasExpress && !hasFastify && !hasNext) continue;
5468
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
5417
5469
  const files = await loadSourceFiles(service.dir);
5418
5470
  for (const file of files) {
5419
5471
  if (isTestPath(file.path)) continue;
@@ -5423,8 +5475,8 @@ async function addRoutes(graph, services) {
5423
5475
  try {
5424
5476
  if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5425
5477
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5426
- } else if (hasExpress || hasFastify) {
5427
- routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5478
+ } else if (hasExpress || hasFastify || hasHono) {
5479
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
5428
5480
  } else {
5429
5481
  routes = [];
5430
5482
  }
@@ -6748,30 +6800,264 @@ async function addK8sResources(graph, scanPath) {
6748
6800
  return { nodesAdded, edgesAdded: 0 };
6749
6801
  }
6750
6802
 
6803
+ // src/extract/infra/cloudflare.ts
6804
+ init_cjs_shims();
6805
+ var import_node_fs19 = require("fs");
6806
+ var import_node_path35 = __toESM(require("path"), 1);
6807
+ var import_smol_toml2 = require("smol-toml");
6808
+ var import_types25 = require("@neat.is/types");
6809
+ var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
6810
+ async function readWranglerConfig(dir) {
6811
+ for (const filename of WRANGLER_FILENAMES) {
6812
+ const abs = import_node_path35.default.join(dir, filename);
6813
+ if (!await exists(abs)) continue;
6814
+ const raw = await import_node_fs19.promises.readFile(abs, "utf8");
6815
+ const config = filename === "wrangler.toml" ? (0, import_smol_toml2.parse)(raw) : JSON.parse(maskCommentsInSource(raw));
6816
+ return { config, relFile: filename, raw };
6817
+ }
6818
+ return null;
6819
+ }
6820
+ function lineContaining(raw, needle) {
6821
+ if (!needle) return void 0;
6822
+ const idx = raw.indexOf(needle);
6823
+ if (idx === -1) return void 0;
6824
+ let line = 1;
6825
+ for (let i = 0; i < idx; i++) if (raw[i] === "\n") line++;
6826
+ return line;
6827
+ }
6828
+ function normalizeRoutes(config) {
6829
+ const out = [];
6830
+ const pushOne = (r) => {
6831
+ if (typeof r === "string") out.push(r);
6832
+ else if (r && typeof r === "object" && typeof r.pattern === "string") {
6833
+ out.push(r.pattern);
6834
+ }
6835
+ };
6836
+ if (Array.isArray(config.routes)) config.routes.forEach(pushOne);
6837
+ else if (config.route) pushOne(config.route);
6838
+ return out;
6839
+ }
6840
+ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, line) {
6841
+ let nodesAdded = 0;
6842
+ let edgesAdded = 0;
6843
+ const node = makeInfraNode(kind, name, "cloudflare");
6844
+ if (!graph.hasNode(node.id)) {
6845
+ graph.addNode(node.id, node);
6846
+ nodesAdded++;
6847
+ }
6848
+ if (node.id === anchorId) return { nodesAdded, edgesAdded };
6849
+ const edgeId = (0, import_types4.extractedEdgeId)(anchorId, node.id, edgeType);
6850
+ if (!graph.hasEdge(edgeId)) {
6851
+ const edge = {
6852
+ id: edgeId,
6853
+ source: anchorId,
6854
+ target: node.id,
6855
+ type: edgeType,
6856
+ provenance: import_types25.Provenance.EXTRACTED,
6857
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
6858
+ evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
6859
+ };
6860
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
6861
+ edgesAdded++;
6862
+ }
6863
+ return { nodesAdded, edgesAdded };
6864
+ }
6865
+ async function addCloudflareWorkers(graph, services, scanPath) {
6866
+ let nodesAdded = 0;
6867
+ let edgesAdded = 0;
6868
+ const discovered = [];
6869
+ const workerIndex = /* @__PURE__ */ new Map();
6870
+ for (const service of services) {
6871
+ let read;
6872
+ try {
6873
+ read = await readWranglerConfig(service.dir);
6874
+ } catch (err) {
6875
+ recordExtractionError("infra cloudflare", import_node_path35.default.relative(scanPath, service.dir), err);
6876
+ continue;
6877
+ }
6878
+ if (!read || !read.config.name) continue;
6879
+ const evidenceFile = toPosix2(import_node_path35.default.relative(scanPath, import_node_path35.default.join(service.dir, read.relFile)));
6880
+ discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
6881
+ }
6882
+ for (const worker of discovered) {
6883
+ const { service, config } = worker;
6884
+ const serviceNode = graph.getNodeAttributes(service.node.id);
6885
+ if (serviceNode.platform !== "cloudflare") {
6886
+ const updated = { ...serviceNode, platform: "cloudflare" };
6887
+ graph.replaceNodeAttributes(service.node.id, updated);
6888
+ }
6889
+ let anchorId = service.node.id;
6890
+ if (config.main) {
6891
+ const entryRelPath = toPosix2(import_node_path35.default.normalize(config.main));
6892
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6893
+ graph,
6894
+ service.pkg.name,
6895
+ service.node.id,
6896
+ entryRelPath
6897
+ );
6898
+ nodesAdded += fn;
6899
+ edgesAdded += fe;
6900
+ const fileNode = graph.getNodeAttributes(fileNodeId);
6901
+ if (fileNode.platform !== "cloudflare" || fileNode.platformName !== config.name) {
6902
+ const updated = { ...fileNode, platform: "cloudflare", platformName: config.name };
6903
+ graph.replaceNodeAttributes(fileNodeId, updated);
6904
+ }
6905
+ anchorId = fileNodeId;
6906
+ }
6907
+ workerIndex.set(config.name, { anchorId });
6908
+ }
6909
+ for (const worker of discovered) {
6910
+ const { config, evidenceFile, raw } = worker;
6911
+ const anchorId = workerIndex.get(config.name).anchorId;
6912
+ const runtimeNode = makeInfraNode("workerd", "cloudflare", "cloudflare");
6913
+ if (!graph.hasNode(runtimeNode.id)) {
6914
+ graph.addNode(runtimeNode.id, runtimeNode);
6915
+ nodesAdded++;
6916
+ }
6917
+ if (runtimeNode.id !== anchorId) {
6918
+ const runsOnId = (0, import_types4.extractedEdgeId)(anchorId, runtimeNode.id, import_types25.EdgeType.RUNS_ON);
6919
+ if (!graph.hasEdge(runsOnId)) {
6920
+ const edge = {
6921
+ id: runsOnId,
6922
+ source: anchorId,
6923
+ target: runtimeNode.id,
6924
+ type: import_types25.EdgeType.RUNS_ON,
6925
+ provenance: import_types25.Provenance.EXTRACTED,
6926
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
6927
+ evidence: {
6928
+ file: evidenceFile,
6929
+ ...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
6930
+ }
6931
+ };
6932
+ graph.addEdgeWithKey(runsOnId, edge.source, edge.target, edge);
6933
+ edgesAdded++;
6934
+ }
6935
+ }
6936
+ for (const route of normalizeRoutes(config)) {
6937
+ const result = addResourceEdge(
6938
+ graph,
6939
+ anchorId,
6940
+ import_types25.EdgeType.CONNECTS_TO,
6941
+ "cloudflare-route",
6942
+ route,
6943
+ evidenceFile,
6944
+ lineContaining(raw, route)
6945
+ );
6946
+ nodesAdded += result.nodesAdded;
6947
+ edgesAdded += result.edgesAdded;
6948
+ }
6949
+ const bindingGroups = [
6950
+ { kind: "cloudflare-kv", entries: config.kv_namespaces ?? [], nameOf: (b) => b.binding },
6951
+ { kind: "cloudflare-d1", entries: config.d1_databases ?? [], nameOf: (b) => b.binding },
6952
+ { kind: "cloudflare-r2", entries: config.r2_buckets ?? [], nameOf: (b) => b.binding },
6953
+ { kind: "cloudflare-durable-object", entries: config.durable_objects?.bindings ?? [], nameOf: (b) => b.name },
6954
+ { kind: "cloudflare-queue", entries: config.queues?.producers ?? [], nameOf: (b) => b.queue },
6955
+ { kind: "cloudflare-queue", entries: config.queues?.consumers ?? [], nameOf: (b) => b.queue }
6956
+ ];
6957
+ for (const group of bindingGroups) {
6958
+ for (const entry2 of group.entries) {
6959
+ const name = group.nameOf(entry2);
6960
+ if (!name) continue;
6961
+ const result = addResourceEdge(
6962
+ graph,
6963
+ anchorId,
6964
+ import_types25.EdgeType.DEPENDS_ON,
6965
+ group.kind,
6966
+ name,
6967
+ evidenceFile,
6968
+ lineContaining(raw, name)
6969
+ );
6970
+ nodesAdded += result.nodesAdded;
6971
+ edgesAdded += result.edgesAdded;
6972
+ }
6973
+ }
6974
+ for (const cron of config.triggers?.crons ?? []) {
6975
+ const result = addResourceEdge(
6976
+ graph,
6977
+ anchorId,
6978
+ import_types25.EdgeType.DEPENDS_ON,
6979
+ "cloudflare-cron",
6980
+ cron,
6981
+ evidenceFile,
6982
+ lineContaining(raw, cron)
6983
+ );
6984
+ nodesAdded += result.nodesAdded;
6985
+ edgesAdded += result.edgesAdded;
6986
+ }
6987
+ for (const varName of Object.keys(config.vars ?? {})) {
6988
+ const result = addResourceEdge(
6989
+ graph,
6990
+ anchorId,
6991
+ import_types25.EdgeType.DEPENDS_ON,
6992
+ "cloudflare-env-var",
6993
+ varName,
6994
+ evidenceFile,
6995
+ lineContaining(raw, varName)
6996
+ );
6997
+ nodesAdded += result.nodesAdded;
6998
+ edgesAdded += result.edgesAdded;
6999
+ }
7000
+ for (const svc of config.services ?? []) {
7001
+ if (!svc.service) continue;
7002
+ const target = workerIndex.get(svc.service);
7003
+ if (target && target.anchorId !== anchorId) {
7004
+ const edgeId = (0, import_types4.extractedEdgeId)(anchorId, target.anchorId, import_types25.EdgeType.CALLS);
7005
+ if (!graph.hasEdge(edgeId)) {
7006
+ const edge = {
7007
+ id: edgeId,
7008
+ source: anchorId,
7009
+ target: target.anchorId,
7010
+ type: import_types25.EdgeType.CALLS,
7011
+ provenance: import_types25.Provenance.EXTRACTED,
7012
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
7013
+ evidence: { file: evidenceFile, line: lineContaining(raw, svc.service) }
7014
+ };
7015
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
7016
+ edgesAdded++;
7017
+ }
7018
+ continue;
7019
+ }
7020
+ const result = addResourceEdge(
7021
+ graph,
7022
+ anchorId,
7023
+ import_types25.EdgeType.DEPENDS_ON,
7024
+ "cloudflare-service-binding",
7025
+ svc.service,
7026
+ evidenceFile,
7027
+ lineContaining(raw, svc.service)
7028
+ );
7029
+ nodesAdded += result.nodesAdded;
7030
+ edgesAdded += result.edgesAdded;
7031
+ }
7032
+ }
7033
+ return { nodesAdded, edgesAdded };
7034
+ }
7035
+
6751
7036
  // src/extract/infra/index.ts
6752
7037
  async function addInfra(graph, scanPath, services) {
6753
7038
  const compose = await addComposeInfra(graph, scanPath, services);
6754
7039
  const dockerfile = await addDockerfileRuntimes(graph, services, scanPath);
6755
7040
  const terraform = await addTerraformResources(graph, scanPath);
6756
7041
  const k8s = await addK8sResources(graph, scanPath);
7042
+ const cloudflare = await addCloudflareWorkers(graph, services, scanPath);
6757
7043
  return {
6758
- nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded,
6759
- edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded
7044
+ nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded,
7045
+ edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded
6760
7046
  };
6761
7047
  }
6762
7048
 
6763
7049
  // src/extract/index.ts
6764
- var import_node_path36 = __toESM(require("path"), 1);
7050
+ var import_node_path37 = __toESM(require("path"), 1);
6765
7051
 
6766
7052
  // src/extract/retire.ts
6767
7053
  init_cjs_shims();
6768
- var import_node_fs19 = require("fs");
6769
- var import_node_path35 = __toESM(require("path"), 1);
6770
- var import_types25 = require("@neat.is/types");
7054
+ var import_node_fs20 = require("fs");
7055
+ var import_node_path36 = __toESM(require("path"), 1);
7056
+ var import_types26 = require("@neat.is/types");
6771
7057
  function dropOrphanedFileNodes(graph) {
6772
7058
  const orphans = [];
6773
7059
  graph.forEachNode((id, attrs) => {
6774
- if (attrs.type !== import_types25.NodeType.FileNode) return;
7060
+ if (attrs.type !== import_types26.NodeType.FileNode) return;
6775
7061
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
6776
7062
  orphans.push(id);
6777
7063
  }
@@ -6784,14 +7070,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
6784
7070
  const bases = [scanPath, ...serviceDirs];
6785
7071
  graph.forEachEdge((id, attrs) => {
6786
7072
  const edge = attrs;
6787
- if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
7073
+ if (edge.provenance !== import_types26.Provenance.EXTRACTED) return;
6788
7074
  const evidenceFile = edge.evidence?.file;
6789
7075
  if (!evidenceFile) return;
6790
- if (import_node_path35.default.isAbsolute(evidenceFile)) {
6791
- if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
7076
+ if (import_node_path36.default.isAbsolute(evidenceFile)) {
7077
+ if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
6792
7078
  return;
6793
7079
  }
6794
- const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path35.default.join(base, evidenceFile)));
7080
+ const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path36.default.join(base, evidenceFile)));
6795
7081
  if (!found) toDrop.push(id);
6796
7082
  });
6797
7083
  for (const id of toDrop) graph.dropEdge(id);
@@ -6833,7 +7119,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6833
7119
  }
6834
7120
  const droppedEntries = drainDroppedExtracted();
6835
7121
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
6836
- const rejectedPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.errorsPath), "rejected.ndjson");
7122
+ const rejectedPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.errorsPath), "rejected.ndjson");
6837
7123
  try {
6838
7124
  await writeRejectedExtracted(droppedEntries, rejectedPath);
6839
7125
  } catch (err) {
@@ -6867,9 +7153,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6867
7153
 
6868
7154
  // src/persist.ts
6869
7155
  init_cjs_shims();
6870
- var import_node_fs20 = require("fs");
6871
- var import_node_path37 = __toESM(require("path"), 1);
6872
- var import_types26 = require("@neat.is/types");
7156
+ var import_node_fs21 = require("fs");
7157
+ var import_node_path38 = __toESM(require("path"), 1);
7158
+ var import_types27 = require("@neat.is/types");
6873
7159
  var SCHEMA_VERSION = 4;
6874
7160
  function migrateV1ToV2(payload) {
6875
7161
  const nodes = payload.graph.nodes;
@@ -6891,12 +7177,12 @@ function migrateV2ToV3(payload) {
6891
7177
  for (const edge of edges) {
6892
7178
  const attrs = edge.attributes;
6893
7179
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
6894
- attrs.provenance = import_types26.Provenance.OBSERVED;
7180
+ attrs.provenance = import_types27.Provenance.OBSERVED;
6895
7181
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
6896
7182
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
6897
7183
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
6898
7184
  if (type && source && target) {
6899
- const newId = (0, import_types26.observedEdgeId)(source, target, type);
7185
+ const newId = (0, import_types27.observedEdgeId)(source, target, type);
6900
7186
  attrs.id = newId;
6901
7187
  if (edge.key) edge.key = newId;
6902
7188
  }
@@ -6905,7 +7191,7 @@ function migrateV2ToV3(payload) {
6905
7191
  return { ...payload, schemaVersion: 3 };
6906
7192
  }
6907
7193
  async function ensureDir(filePath) {
6908
- await import_node_fs20.promises.mkdir(import_node_path37.default.dirname(filePath), { recursive: true });
7194
+ await import_node_fs21.promises.mkdir(import_node_path38.default.dirname(filePath), { recursive: true });
6909
7195
  }
6910
7196
  async function saveGraphToDisk(graph, outPath) {
6911
7197
  await ensureDir(outPath);
@@ -6915,13 +7201,13 @@ async function saveGraphToDisk(graph, outPath) {
6915
7201
  graph: graph.export()
6916
7202
  };
6917
7203
  const tmp = `${outPath}.tmp`;
6918
- await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6919
- await import_node_fs20.promises.rename(tmp, outPath);
7204
+ await import_node_fs21.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
7205
+ await import_node_fs21.promises.rename(tmp, outPath);
6920
7206
  }
6921
7207
  async function loadGraphFromDisk(graph, outPath) {
6922
7208
  let raw;
6923
7209
  try {
6924
- raw = await import_node_fs20.promises.readFile(outPath, "utf8");
7210
+ raw = await import_node_fs21.promises.readFile(outPath, "utf8");
6925
7211
  } catch (err) {
6926
7212
  if (err.code === "ENOENT") return;
6927
7213
  throw err;
@@ -6986,23 +7272,23 @@ function startPersistLoop(graph, outPath, opts = {}) {
6986
7272
 
6987
7273
  // src/projects.ts
6988
7274
  init_cjs_shims();
6989
- var import_node_path38 = __toESM(require("path"), 1);
7275
+ var import_node_path39 = __toESM(require("path"), 1);
6990
7276
  function pathsForProject(project, baseDir) {
6991
7277
  if (project === DEFAULT_PROJECT) {
6992
7278
  return {
6993
- snapshotPath: import_node_path38.default.join(baseDir, "graph.json"),
6994
- errorsPath: import_node_path38.default.join(baseDir, "errors.ndjson"),
6995
- staleEventsPath: import_node_path38.default.join(baseDir, "stale-events.ndjson"),
6996
- embeddingsCachePath: import_node_path38.default.join(baseDir, "embeddings.json"),
6997
- policyViolationsPath: import_node_path38.default.join(baseDir, "policy-violations.ndjson")
7279
+ snapshotPath: import_node_path39.default.join(baseDir, "graph.json"),
7280
+ errorsPath: import_node_path39.default.join(baseDir, "errors.ndjson"),
7281
+ staleEventsPath: import_node_path39.default.join(baseDir, "stale-events.ndjson"),
7282
+ embeddingsCachePath: import_node_path39.default.join(baseDir, "embeddings.json"),
7283
+ policyViolationsPath: import_node_path39.default.join(baseDir, "policy-violations.ndjson")
6998
7284
  };
6999
7285
  }
7000
7286
  return {
7001
- snapshotPath: import_node_path38.default.join(baseDir, `${project}.json`),
7002
- errorsPath: import_node_path38.default.join(baseDir, `errors.${project}.ndjson`),
7003
- staleEventsPath: import_node_path38.default.join(baseDir, `stale-events.${project}.ndjson`),
7004
- embeddingsCachePath: import_node_path38.default.join(baseDir, `embeddings.${project}.json`),
7005
- policyViolationsPath: import_node_path38.default.join(baseDir, `policy-violations.${project}.ndjson`)
7287
+ snapshotPath: import_node_path39.default.join(baseDir, `${project}.json`),
7288
+ errorsPath: import_node_path39.default.join(baseDir, `errors.${project}.ndjson`),
7289
+ staleEventsPath: import_node_path39.default.join(baseDir, `stale-events.${project}.ndjson`),
7290
+ embeddingsCachePath: import_node_path39.default.join(baseDir, `embeddings.${project}.json`),
7291
+ policyViolationsPath: import_node_path39.default.join(baseDir, `policy-violations.${project}.ndjson`)
7006
7292
  };
7007
7293
  }
7008
7294
  var Projects = class {
@@ -7040,19 +7326,19 @@ var Projects = class {
7040
7326
  init_cjs_shims();
7041
7327
  var import_fastify = __toESM(require("fastify"), 1);
7042
7328
  var import_cors = __toESM(require("@fastify/cors"), 1);
7043
- var import_types29 = require("@neat.is/types");
7329
+ var import_types30 = require("@neat.is/types");
7044
7330
 
7045
7331
  // src/extend/index.ts
7046
7332
  init_cjs_shims();
7047
- var import_node_fs22 = require("fs");
7048
- var import_node_path40 = __toESM(require("path"), 1);
7333
+ var import_node_fs23 = require("fs");
7334
+ var import_node_path41 = __toESM(require("path"), 1);
7049
7335
  var import_node_os2 = __toESM(require("os"), 1);
7050
7336
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
7051
7337
 
7052
7338
  // src/installers/package-manager.ts
7053
7339
  init_cjs_shims();
7054
- var import_node_fs21 = require("fs");
7055
- var import_node_path39 = __toESM(require("path"), 1);
7340
+ var import_node_fs22 = require("fs");
7341
+ var import_node_path40 = __toESM(require("path"), 1);
7056
7342
  var import_node_child_process = require("child_process");
7057
7343
  var LOCKFILE_PRIORITY = [
7058
7344
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -7067,29 +7353,29 @@ var LOCKFILE_PRIORITY = [
7067
7353
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
7068
7354
  async function exists2(p) {
7069
7355
  try {
7070
- await import_node_fs21.promises.access(p);
7356
+ await import_node_fs22.promises.access(p);
7071
7357
  return true;
7072
7358
  } catch {
7073
7359
  return false;
7074
7360
  }
7075
7361
  }
7076
7362
  async function detectPackageManager(serviceDir) {
7077
- let dir = import_node_path39.default.resolve(serviceDir);
7363
+ let dir = import_node_path40.default.resolve(serviceDir);
7078
7364
  const stops = /* @__PURE__ */ new Set();
7079
7365
  for (let i = 0; i < 64; i++) {
7080
7366
  if (stops.has(dir)) break;
7081
7367
  stops.add(dir);
7082
7368
  for (const candidate of LOCKFILE_PRIORITY) {
7083
- const lockPath = import_node_path39.default.join(dir, candidate.lockfile);
7369
+ const lockPath = import_node_path40.default.join(dir, candidate.lockfile);
7084
7370
  if (await exists2(lockPath)) {
7085
7371
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
7086
7372
  }
7087
7373
  }
7088
- const parent = import_node_path39.default.dirname(dir);
7374
+ const parent = import_node_path40.default.dirname(dir);
7089
7375
  if (parent === dir) break;
7090
7376
  dir = parent;
7091
7377
  }
7092
- return { pm: "npm", cwd: import_node_path39.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7378
+ return { pm: "npm", cwd: import_node_path40.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7093
7379
  }
7094
7380
  async function runPackageManagerInstall(cmd) {
7095
7381
  return new Promise((resolve) => {
@@ -7131,30 +7417,30 @@ ${err.message}`
7131
7417
  // src/extend/index.ts
7132
7418
  async function fileExists2(p) {
7133
7419
  try {
7134
- await import_node_fs22.promises.access(p);
7420
+ await import_node_fs23.promises.access(p);
7135
7421
  return true;
7136
7422
  } catch {
7137
7423
  return false;
7138
7424
  }
7139
7425
  }
7140
7426
  async function readPackageJson(scanPath) {
7141
- const pkgPath = import_node_path40.default.join(scanPath, "package.json");
7142
- const raw = await import_node_fs22.promises.readFile(pkgPath, "utf8");
7427
+ const pkgPath = import_node_path41.default.join(scanPath, "package.json");
7428
+ const raw = await import_node_fs23.promises.readFile(pkgPath, "utf8");
7143
7429
  return JSON.parse(raw);
7144
7430
  }
7145
7431
  async function findHookFiles(scanPath) {
7146
- const entries = await import_node_fs22.promises.readdir(scanPath);
7432
+ const entries = await import_node_fs23.promises.readdir(scanPath);
7147
7433
  return entries.filter(
7148
7434
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
7149
7435
  ).sort();
7150
7436
  }
7151
7437
  function extendLogPath() {
7152
- return process.env.NEAT_EXTEND_LOG ?? import_node_path40.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7438
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path41.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7153
7439
  }
7154
7440
  async function appendExtendLog(entry2) {
7155
7441
  const logPath = extendLogPath();
7156
- await import_node_fs22.promises.mkdir(import_node_path40.default.dirname(logPath), { recursive: true });
7157
- await import_node_fs22.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
7442
+ await import_node_fs23.promises.mkdir(import_node_path41.default.dirname(logPath), { recursive: true });
7443
+ await import_node_fs23.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
7158
7444
  }
7159
7445
  function splicedContent(fileContent, snippet2) {
7160
7446
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -7212,7 +7498,7 @@ function lookupInstrumentation(library, installedVersion) {
7212
7498
  }
7213
7499
  async function describeProjectInstrumentation(ctx) {
7214
7500
  const hookFiles = await findHookFiles(ctx.scanPath);
7215
- const envNeat = await fileExists2(import_node_path40.default.join(ctx.scanPath, ".env.neat"));
7501
+ const envNeat = await fileExists2(import_node_path41.default.join(ctx.scanPath, ".env.neat"));
7216
7502
  const registryInstrPackages = new Set(
7217
7503
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
7218
7504
  );
@@ -7234,31 +7520,31 @@ async function applyExtension(ctx, args, options) {
7234
7520
  );
7235
7521
  }
7236
7522
  for (const file of hookFiles) {
7237
- const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7523
+ const content = await import_node_fs23.promises.readFile(import_node_path41.default.join(ctx.scanPath, file), "utf8");
7238
7524
  if (content.includes(args.registration_snippet)) {
7239
7525
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
7240
7526
  }
7241
7527
  }
7242
7528
  const primaryFile = hookFiles[0];
7243
- const primaryPath = import_node_path40.default.join(ctx.scanPath, primaryFile);
7529
+ const primaryPath = import_node_path41.default.join(ctx.scanPath, primaryFile);
7244
7530
  const filesTouched = [];
7245
7531
  const depsAdded = [];
7246
- const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7532
+ const pkgPath = import_node_path41.default.join(ctx.scanPath, "package.json");
7247
7533
  const pkg = await readPackageJson(ctx.scanPath);
7248
7534
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
7249
7535
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
7250
- await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7536
+ await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7251
7537
  filesTouched.push("package.json");
7252
7538
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
7253
7539
  }
7254
- const hookContent = await import_node_fs22.promises.readFile(primaryPath, "utf8");
7540
+ const hookContent = await import_node_fs23.promises.readFile(primaryPath, "utf8");
7255
7541
  const patched = splicedContent(hookContent, args.registration_snippet);
7256
7542
  if (!patched) {
7257
7543
  throw new Error(
7258
7544
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
7259
7545
  );
7260
7546
  }
7261
- await import_node_fs22.promises.writeFile(primaryPath, patched, "utf8");
7547
+ await import_node_fs23.promises.writeFile(primaryPath, patched, "utf8");
7262
7548
  filesTouched.push(primaryFile);
7263
7549
  const cmd = await detectPackageManager(ctx.scanPath);
7264
7550
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -7289,7 +7575,7 @@ async function dryRunExtension(ctx, args) {
7289
7575
  };
7290
7576
  }
7291
7577
  for (const file of hookFiles) {
7292
- const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
7578
+ const content = await import_node_fs23.promises.readFile(import_node_path41.default.join(ctx.scanPath, file), "utf8");
7293
7579
  if (content.includes(args.registration_snippet)) {
7294
7580
  return {
7295
7581
  library: args.library,
@@ -7311,7 +7597,7 @@ async function dryRunExtension(ctx, args) {
7311
7597
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
7312
7598
  filesTouched.push("package.json");
7313
7599
  }
7314
- const hookContent = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, primaryFile), "utf8");
7600
+ const hookContent = await import_node_fs23.promises.readFile(import_node_path41.default.join(ctx.scanPath, primaryFile), "utf8");
7315
7601
  const patched = splicedContent(hookContent, args.registration_snippet);
7316
7602
  if (patched) {
7317
7603
  filesTouched.push(primaryFile);
@@ -7326,28 +7612,28 @@ async function rollbackExtension(ctx, args) {
7326
7612
  if (!await fileExists2(logPath)) {
7327
7613
  return { undone: false, message: "no apply found for library" };
7328
7614
  }
7329
- const raw = await import_node_fs22.promises.readFile(logPath, "utf8");
7615
+ const raw = await import_node_fs23.promises.readFile(logPath, "utf8");
7330
7616
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
7331
7617
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
7332
7618
  if (!match) {
7333
7619
  return { undone: false, message: "no apply found for library" };
7334
7620
  }
7335
- const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
7621
+ const pkgPath = import_node_path41.default.join(ctx.scanPath, "package.json");
7336
7622
  if (await fileExists2(pkgPath)) {
7337
7623
  const pkg = await readPackageJson(ctx.scanPath);
7338
7624
  if (pkg.dependencies?.[match.instrumentation_package]) {
7339
7625
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
7340
7626
  pkg.dependencies = rest;
7341
- await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7627
+ await import_node_fs23.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7342
7628
  }
7343
7629
  }
7344
7630
  const hookFiles = await findHookFiles(ctx.scanPath);
7345
7631
  for (const file of hookFiles) {
7346
- const filePath = import_node_path40.default.join(ctx.scanPath, file);
7347
- const content = await import_node_fs22.promises.readFile(filePath, "utf8");
7632
+ const filePath = import_node_path41.default.join(ctx.scanPath, file);
7633
+ const content = await import_node_fs23.promises.readFile(filePath, "utf8");
7348
7634
  if (content.includes(match.registration_snippet)) {
7349
7635
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
7350
- await import_node_fs22.promises.writeFile(filePath, filtered, "utf8");
7636
+ await import_node_fs23.promises.writeFile(filePath, filtered, "utf8");
7351
7637
  break;
7352
7638
  }
7353
7639
  }
@@ -7359,44 +7645,44 @@ async function rollbackExtension(ctx, args) {
7359
7645
 
7360
7646
  // src/divergences.ts
7361
7647
  init_cjs_shims();
7362
- var import_types27 = require("@neat.is/types");
7648
+ var import_types28 = require("@neat.is/types");
7363
7649
  function bucketKey(source, target, type) {
7364
7650
  return `${type}|${source}|${target}`;
7365
7651
  }
7366
7652
  function bucketEdges(graph) {
7367
- const buckets = /* @__PURE__ */ new Map();
7653
+ const buckets2 = /* @__PURE__ */ new Map();
7368
7654
  graph.forEachEdge((id, attrs) => {
7369
7655
  const e = attrs;
7370
- const parsed = (0, import_types27.parseEdgeId)(id);
7656
+ const parsed = (0, import_types28.parseEdgeId)(id);
7371
7657
  const provenance = parsed?.provenance ?? e.provenance;
7372
7658
  const key = bucketKey(e.source, e.target, e.type);
7373
- const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
7659
+ const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
7374
7660
  switch (provenance) {
7375
- case import_types27.Provenance.EXTRACTED:
7661
+ case import_types28.Provenance.EXTRACTED:
7376
7662
  cur.extracted = e;
7377
7663
  break;
7378
- case import_types27.Provenance.OBSERVED:
7664
+ case import_types28.Provenance.OBSERVED:
7379
7665
  cur.observed = e;
7380
7666
  break;
7381
- case import_types27.Provenance.INFERRED:
7667
+ case import_types28.Provenance.INFERRED:
7382
7668
  cur.inferred = e;
7383
7669
  break;
7384
7670
  default:
7385
- if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
7671
+ if (e.provenance === import_types28.Provenance.STALE) cur.stale = e;
7386
7672
  }
7387
- buckets.set(key, cur);
7673
+ buckets2.set(key, cur);
7388
7674
  });
7389
- return buckets;
7675
+ return buckets2;
7390
7676
  }
7391
7677
  function nodeIsFrontier(graph, nodeId) {
7392
7678
  if (!graph.hasNode(nodeId)) return false;
7393
7679
  const attrs = graph.getNodeAttributes(nodeId);
7394
- return attrs.type === import_types27.NodeType.FrontierNode;
7680
+ return attrs.type === import_types28.NodeType.FrontierNode;
7395
7681
  }
7396
7682
  function nodeIsWebsocketChannel(graph, nodeId) {
7397
7683
  if (!graph.hasNode(nodeId)) return false;
7398
7684
  const attrs = graph.getNodeAttributes(nodeId);
7399
- return attrs.type === import_types27.NodeType.WebSocketChannelNode;
7685
+ return attrs.type === import_types28.NodeType.WebSocketChannelNode;
7400
7686
  }
7401
7687
  function clampConfidence(n) {
7402
7688
  if (!Number.isFinite(n)) return 0;
@@ -7416,14 +7702,14 @@ function gradedConfidence(edge) {
7416
7702
  return clampConfidence(confidenceForEdge(edge));
7417
7703
  }
7418
7704
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
7419
- import_types27.EdgeType.CALLS,
7420
- import_types27.EdgeType.CONNECTS_TO,
7421
- import_types27.EdgeType.PUBLISHES_TO,
7422
- import_types27.EdgeType.CONSUMES_FROM
7705
+ import_types28.EdgeType.CALLS,
7706
+ import_types28.EdgeType.CONNECTS_TO,
7707
+ import_types28.EdgeType.PUBLISHES_TO,
7708
+ import_types28.EdgeType.CONSUMES_FROM
7423
7709
  ]);
7424
7710
  function detectMissingDivergences(graph, bucket) {
7425
7711
  const out = [];
7426
- if (bucket.type === import_types27.EdgeType.CONTAINS) return out;
7712
+ if (bucket.type === import_types28.EdgeType.CONTAINS) return out;
7427
7713
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
7428
7714
  if (!nodeIsFrontier(graph, bucket.target)) {
7429
7715
  out.push({
@@ -7464,7 +7750,7 @@ function declaredHostFor(svc) {
7464
7750
  function hasExtractedConfiguredBy(graph, svcId) {
7465
7751
  for (const edgeId of graph.outboundEdges(svcId)) {
7466
7752
  const e = graph.getEdgeAttributes(edgeId);
7467
- if (e.type === import_types27.EdgeType.CONFIGURED_BY && e.provenance === import_types27.Provenance.EXTRACTED) {
7753
+ if (e.type === import_types28.EdgeType.CONFIGURED_BY && e.provenance === import_types28.Provenance.EXTRACTED) {
7468
7754
  return true;
7469
7755
  }
7470
7756
  }
@@ -7477,10 +7763,10 @@ function detectHostMismatch(graph, svcId, svc) {
7477
7763
  const out = [];
7478
7764
  for (const edgeId of graph.outboundEdges(svcId)) {
7479
7765
  const edge = graph.getEdgeAttributes(edgeId);
7480
- if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7481
- if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
7766
+ if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7767
+ if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
7482
7768
  const target = graph.getNodeAttributes(edge.target);
7483
- if (target.type !== import_types27.NodeType.DatabaseNode) continue;
7769
+ if (target.type !== import_types28.NodeType.DatabaseNode) continue;
7484
7770
  const observedHost = target.host?.trim();
7485
7771
  if (!observedHost) continue;
7486
7772
  if (observedHost === declaredHost) continue;
@@ -7502,10 +7788,10 @@ function detectCompatDivergences(graph, svcId, svc) {
7502
7788
  const deps = svc.dependencies ?? {};
7503
7789
  for (const edgeId of graph.outboundEdges(svcId)) {
7504
7790
  const edge = graph.getEdgeAttributes(edgeId);
7505
- if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7506
- if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
7791
+ if (edge.type !== import_types28.EdgeType.CONNECTS_TO) continue;
7792
+ if (edge.provenance !== import_types28.Provenance.OBSERVED) continue;
7507
7793
  const target = graph.getNodeAttributes(edge.target);
7508
- if (target.type !== import_types27.NodeType.DatabaseNode) continue;
7794
+ if (target.type !== import_types28.NodeType.DatabaseNode) continue;
7509
7795
  for (const pair of compatPairs()) {
7510
7796
  if (pair.engine !== target.engine) continue;
7511
7797
  const declared = deps[pair.driver];
@@ -7564,7 +7850,7 @@ function suppressHostMismatchHalves(all) {
7564
7850
  for (const d of all) {
7565
7851
  if (d.type !== "host-mismatch") continue;
7566
7852
  observedHalf.add(`${d.source}->${d.target}`);
7567
- declaredHalf.add((0, import_types27.databaseId)(d.extractedHost));
7853
+ declaredHalf.add((0, import_types28.databaseId)(d.extractedHost));
7568
7854
  }
7569
7855
  if (observedHalf.size === 0) return all;
7570
7856
  return all.filter((d) => {
@@ -7577,13 +7863,13 @@ function suppressHostMismatchHalves(all) {
7577
7863
  }
7578
7864
  function computeDivergences(graph, opts = {}) {
7579
7865
  const all = [];
7580
- const buckets = bucketEdges(graph);
7581
- for (const bucket of buckets.values()) {
7866
+ const buckets2 = bucketEdges(graph);
7867
+ for (const bucket of buckets2.values()) {
7582
7868
  for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
7583
7869
  }
7584
7870
  graph.forEachNode((nodeId, attrs) => {
7585
7871
  const n = attrs;
7586
- if (n.type !== import_types27.NodeType.ServiceNode) return;
7872
+ if (n.type !== import_types28.NodeType.ServiceNode) return;
7587
7873
  const svc = n;
7588
7874
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
7589
7875
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -7617,16 +7903,61 @@ function computeDivergences(graph, opts = {}) {
7617
7903
  if (a.source !== b.source) return a.source.localeCompare(b.source);
7618
7904
  return a.target.localeCompare(b.target);
7619
7905
  });
7620
- return import_types27.DivergenceResultSchema.parse({
7906
+ return import_types28.DivergenceResultSchema.parse({
7621
7907
  divergences: filtered,
7622
7908
  totalAffected: filtered.length,
7623
7909
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
7624
7910
  });
7625
7911
  }
7626
7912
 
7913
+ // src/logs-store.ts
7914
+ init_cjs_shims();
7915
+ var LOGS_STORE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7916
+ var LOGS_QUERY_DEFAULT_LIMIT = 100;
7917
+ var LOGS_QUERY_MAX_LIMIT = 1e3;
7918
+ var buffers = /* @__PURE__ */ new Map();
7919
+ var KEY_SEP = "::";
7920
+ function bufferKey(projectName, source) {
7921
+ return `${projectName}${KEY_SEP}${source}`;
7922
+ }
7923
+ function sourcesForProject(projectName) {
7924
+ const prefix = `${projectName}${KEY_SEP}`;
7925
+ const sources = [];
7926
+ for (const key of buffers.keys()) {
7927
+ if (key.startsWith(prefix)) sources.push(key.slice(prefix.length));
7928
+ }
7929
+ return sources;
7930
+ }
7931
+ function queryLogEntries(opts) {
7932
+ const { projectName, source, service, since, limit } = opts;
7933
+ const sources = source && source.length > 0 ? source : sourcesForProject(projectName);
7934
+ let merged = [];
7935
+ for (const src of sources) {
7936
+ const buf = buffers.get(bufferKey(projectName, src));
7937
+ if (buf) merged = merged.concat(buf);
7938
+ }
7939
+ if (service) {
7940
+ merged = merged.filter((e) => e.serviceName === service);
7941
+ }
7942
+ if (since) {
7943
+ const sinceMs = Date.parse(since);
7944
+ if (!Number.isNaN(sinceMs)) {
7945
+ merged = merged.filter((e) => {
7946
+ const t = Date.parse(e.timestamp);
7947
+ return Number.isNaN(t) || t >= sinceMs;
7948
+ });
7949
+ }
7950
+ }
7951
+ merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
7952
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
7953
+ return merged.slice(0, limit);
7954
+ }
7955
+ return merged;
7956
+ }
7957
+
7627
7958
  // src/diff.ts
7628
7959
  init_cjs_shims();
7629
- var import_node_fs23 = require("fs");
7960
+ var import_node_fs24 = require("fs");
7630
7961
  async function loadSnapshotForDiff(target) {
7631
7962
  if (/^https?:\/\//i.test(target)) {
7632
7963
  const res = await fetch(target);
@@ -7635,7 +7966,7 @@ async function loadSnapshotForDiff(target) {
7635
7966
  }
7636
7967
  return await res.json();
7637
7968
  }
7638
- const raw = await import_node_fs23.promises.readFile(target, "utf8");
7969
+ const raw = await import_node_fs24.promises.readFile(target, "utf8");
7639
7970
  return JSON.parse(raw);
7640
7971
  }
7641
7972
  function indexEntries(entries) {
@@ -7703,25 +8034,25 @@ function canonicalJson(value) {
7703
8034
 
7704
8035
  // src/registry.ts
7705
8036
  init_cjs_shims();
7706
- var import_node_fs24 = require("fs");
8037
+ var import_node_fs25 = require("fs");
7707
8038
  var import_node_os3 = __toESM(require("os"), 1);
7708
- var import_node_path41 = __toESM(require("path"), 1);
7709
- var import_types28 = require("@neat.is/types");
8039
+ var import_node_path42 = __toESM(require("path"), 1);
8040
+ var import_types29 = require("@neat.is/types");
7710
8041
  var LOCK_TIMEOUT_MS = 5e3;
7711
8042
  var LOCK_RETRY_MS = 50;
7712
8043
  function neatHome() {
7713
8044
  const override = process.env.NEAT_HOME;
7714
- if (override && override.length > 0) return import_node_path41.default.resolve(override);
7715
- return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
8045
+ if (override && override.length > 0) return import_node_path42.default.resolve(override);
8046
+ return import_node_path42.default.join(import_node_os3.default.homedir(), ".neat");
7716
8047
  }
7717
8048
  function registryPath() {
7718
- return import_node_path41.default.join(neatHome(), "projects.json");
8049
+ return import_node_path42.default.join(neatHome(), "projects.json");
7719
8050
  }
7720
8051
  function registryLockPath() {
7721
- return import_node_path41.default.join(neatHome(), "projects.json.lock");
8052
+ return import_node_path42.default.join(neatHome(), "projects.json.lock");
7722
8053
  }
7723
8054
  function daemonPidPath() {
7724
- return import_node_path41.default.join(neatHome(), "neatd.pid");
8055
+ return import_node_path42.default.join(neatHome(), "neatd.pid");
7725
8056
  }
7726
8057
  function isPidAliveDefault(pid) {
7727
8058
  try {
@@ -7733,7 +8064,7 @@ function isPidAliveDefault(pid) {
7733
8064
  }
7734
8065
  async function readPidFile(file) {
7735
8066
  try {
7736
- const raw = await import_node_fs24.promises.readFile(file, "utf8");
8067
+ const raw = await import_node_fs25.promises.readFile(file, "utf8");
7737
8068
  const pid = Number.parseInt(raw.trim(), 10);
7738
8069
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
7739
8070
  } catch {
@@ -7781,24 +8112,24 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
7781
8112
  }
7782
8113
  }
7783
8114
  async function writeAtomically(target, contents) {
7784
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(target), { recursive: true });
8115
+ await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(target), { recursive: true });
7785
8116
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
7786
- const fd = await import_node_fs24.promises.open(tmp, "w");
8117
+ const fd = await import_node_fs25.promises.open(tmp, "w");
7787
8118
  try {
7788
8119
  await fd.writeFile(contents, "utf8");
7789
8120
  await fd.sync();
7790
8121
  } finally {
7791
8122
  await fd.close();
7792
8123
  }
7793
- await import_node_fs24.promises.rename(tmp, target);
8124
+ await import_node_fs25.promises.rename(tmp, target);
7794
8125
  }
7795
8126
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
7796
8127
  const deadline = Date.now() + timeoutMs;
7797
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(lockPath), { recursive: true });
8128
+ await import_node_fs25.promises.mkdir(import_node_path42.default.dirname(lockPath), { recursive: true });
7798
8129
  let probedHolder = false;
7799
8130
  while (true) {
7800
8131
  try {
7801
- const fd = await import_node_fs24.promises.open(lockPath, "wx");
8132
+ const fd = await import_node_fs25.promises.open(lockPath, "wx");
7802
8133
  try {
7803
8134
  await fd.writeFile(`${process.pid}
7804
8135
  `, "utf8");
@@ -7823,7 +8154,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
7823
8154
  }
7824
8155
  }
7825
8156
  async function releaseLock(lockPath) {
7826
- await import_node_fs24.promises.unlink(lockPath).catch(() => {
8157
+ await import_node_fs25.promises.unlink(lockPath).catch(() => {
7827
8158
  });
7828
8159
  }
7829
8160
  async function withLock(fn) {
@@ -7839,7 +8170,7 @@ async function readRegistry() {
7839
8170
  const file = registryPath();
7840
8171
  let raw;
7841
8172
  try {
7842
- raw = await import_node_fs24.promises.readFile(file, "utf8");
8173
+ raw = await import_node_fs25.promises.readFile(file, "utf8");
7843
8174
  } catch (err) {
7844
8175
  if (err.code === "ENOENT") {
7845
8176
  return { version: 1, projects: [] };
@@ -7847,10 +8178,10 @@ async function readRegistry() {
7847
8178
  throw err;
7848
8179
  }
7849
8180
  const parsed = JSON.parse(raw);
7850
- return import_types28.RegistryFileSchema.parse(parsed);
8181
+ return import_types29.RegistryFileSchema.parse(parsed);
7851
8182
  }
7852
8183
  async function writeRegistry(reg) {
7853
- const validated = import_types28.RegistryFileSchema.parse(reg);
8184
+ const validated = import_types29.RegistryFileSchema.parse(reg);
7854
8185
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
7855
8186
  }
7856
8187
  async function getProject(name) {
@@ -7894,7 +8225,7 @@ function pruneTtlMs() {
7894
8225
  }
7895
8226
  async function statPathStatus(p) {
7896
8227
  try {
7897
- const stat = await import_node_fs24.promises.stat(p);
8228
+ const stat = await import_node_fs25.promises.stat(p);
7898
8229
  return stat.isDirectory() ? "present" : "unknown";
7899
8230
  } catch (err) {
7900
8231
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -8141,11 +8472,11 @@ function registerRoutes(scope, ctx) {
8141
8472
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
8142
8473
  const parsed = [];
8143
8474
  for (const c of candidates) {
8144
- const r = import_types29.DivergenceTypeSchema.safeParse(c);
8475
+ const r = import_types30.DivergenceTypeSchema.safeParse(c);
8145
8476
  if (!r.success) {
8146
8477
  return reply.code(400).send({
8147
8478
  error: `unknown divergence type "${c}"`,
8148
- allowed: import_types29.DivergenceTypeSchema.options
8479
+ allowed: import_types30.DivergenceTypeSchema.options
8149
8480
  });
8150
8481
  }
8151
8482
  parsed.push(r.data);
@@ -8193,6 +8524,23 @@ function registerRoutes(scope, ctx) {
8193
8524
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
8194
8525
  return { count: sliced.length, total, events: sliced };
8195
8526
  });
8527
+ scope.get("/logs", async (req2, reply) => {
8528
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
8529
+ if (!proj) return;
8530
+ const rawSource = req2.query.source;
8531
+ const sources = rawSource === void 0 ? void 0 : Array.isArray(rawSource) ? rawSource : [rawSource];
8532
+ const filtered = queryLogEntries({
8533
+ projectName: proj.name,
8534
+ ...sources && sources.length > 0 ? { source: sources } : {},
8535
+ ...req2.query.service ? { service: req2.query.service } : {},
8536
+ ...req2.query.since ? { since: req2.query.since } : {}
8537
+ });
8538
+ const total = filtered.length;
8539
+ const limit = req2.query.limit ? Number(req2.query.limit) : LOGS_QUERY_DEFAULT_LIMIT;
8540
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, LOGS_QUERY_MAX_LIMIT) : LOGS_QUERY_DEFAULT_LIMIT;
8541
+ const sliced = filtered.slice(0, safeLimit);
8542
+ return { count: sliced.length, total, logs: sliced };
8543
+ });
8196
8544
  const incidentHistoryHandler = async (req2, reply) => {
8197
8545
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
8198
8546
  if (!proj) return;
@@ -8288,8 +8636,16 @@ function registerRoutes(scope, ctx) {
8288
8636
  if (!against) {
8289
8637
  return reply.code(400).send({ error: "query parameter `against` is required" });
8290
8638
  }
8639
+ const targetProjectName = against === "self" ? proj.name : against;
8640
+ const targetProject = registry.get(targetProjectName);
8641
+ if (!targetProject) {
8642
+ return reply.code(400).send({
8643
+ error: "unknown snapshot id \u2014 `against` must be `self` or the name of a project this server has a snapshot for",
8644
+ against
8645
+ });
8646
+ }
8291
8647
  try {
8292
- const snapshot = await loadSnapshotForDiff(against);
8648
+ const snapshot = await loadSnapshotForDiff(targetProject.paths.snapshotPath);
8293
8649
  return computeGraphDiff(proj.graph, snapshot);
8294
8650
  } catch (err) {
8295
8651
  return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
@@ -8319,6 +8675,13 @@ function registerRoutes(scope, ctx) {
8319
8675
  edgeCount: proj.graph.size
8320
8676
  };
8321
8677
  } catch (err) {
8678
+ if (err instanceof SnapshotValidationError) {
8679
+ return reply.code(400).send({
8680
+ error: "snapshot merge failed",
8681
+ details: err.message,
8682
+ issues: err.issues
8683
+ });
8684
+ }
8322
8685
  return reply.code(400).send({
8323
8686
  error: "snapshot merge failed",
8324
8687
  details: err.message
@@ -8364,7 +8727,7 @@ function registerRoutes(scope, ctx) {
8364
8727
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
8365
8728
  let violations = await log.readAll();
8366
8729
  if (req2.query.severity) {
8367
- const sev = import_types29.PolicySeveritySchema.safeParse(req2.query.severity);
8730
+ const sev = import_types30.PolicySeveritySchema.safeParse(req2.query.severity);
8368
8731
  if (!sev.success) {
8369
8732
  return reply.code(400).send({
8370
8733
  error: "invalid severity",
@@ -8403,7 +8766,7 @@ function registerRoutes(scope, ctx) {
8403
8766
  scope.post("/policies/check", async (req2, reply) => {
8404
8767
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
8405
8768
  if (!proj) return;
8406
- const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
8769
+ const parsed = import_types30.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
8407
8770
  if (!parsed.success) {
8408
8771
  return reply.code(400).send({
8409
8772
  error: "invalid /policies/check body",
@@ -8676,6 +9039,10 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
8676
9039
  unresolved++;
8677
9040
  continue;
8678
9041
  }
9042
+ if (resolved.ensureInfraNode) {
9043
+ const { kind, name, provider } = resolved.ensureInfraNode;
9044
+ ensureInfraNode(graph, kind, name, provider);
9045
+ }
8679
9046
  const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
8680
9047
  const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
8681
9048
  const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
@@ -8739,110 +9106,1698 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
8739
9106
  };
8740
9107
  }
8741
9108
 
8742
- // src/daemon.ts
8743
- init_auth();
8744
-
8745
- // src/unrouted.ts
9109
+ // src/connectors/registry.ts
8746
9110
  init_cjs_shims();
8747
- var import_node_fs25 = require("fs");
8748
- var import_node_path44 = __toESM(require("path"), 1);
8749
- function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
8750
- return {
8751
- timestamp: now.toISOString(),
8752
- reason: "no-project-match",
8753
- service_name: serviceName ?? null,
8754
- traceId: traceId ?? null
8755
- };
8756
- }
8757
- async function appendUnroutedSpan(neatHome3, record) {
8758
- const target = import_node_path44.default.join(neatHome3, "errors.ndjson");
8759
- await import_node_fs25.promises.mkdir(neatHome3, { recursive: true });
8760
- await import_node_fs25.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
8761
- }
8762
- function unroutedErrorsPath(neatHome3) {
8763
- return import_node_path44.default.join(neatHome3, "errors.ndjson");
8764
- }
8765
9111
 
8766
- // src/daemon.ts
8767
- var import_types30 = require("@neat.is/types");
8768
- function daemonJsonPath(scanPath) {
8769
- return import_node_path45.default.join(scanPath, "neat-out", "daemon.json");
8770
- }
8771
- function daemonsDiscoveryDir(home) {
8772
- const base = home && home.length > 0 ? home : neatHomeFromEnv();
8773
- return import_node_path45.default.join(base, "daemons");
8774
- }
8775
- function daemonDiscoveryPath(project, home) {
8776
- return import_node_path45.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8777
- }
8778
- function sanitizeDiscoveryName(project) {
8779
- return project.replace(/[^A-Za-z0-9._-]/g, "_");
8780
- }
8781
- function neatHomeFromEnv() {
8782
- const env = process.env.NEAT_HOME;
8783
- if (env && env.length > 0) return import_node_path45.default.resolve(env);
8784
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8785
- return import_node_path45.default.join(home, ".neat");
8786
- }
8787
- function resolveNeatVersion() {
8788
- if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
8789
- return process.env.NEAT_LOCAL_VERSION;
8790
- }
8791
- try {
8792
- const req2 = (0, import_node_module.createRequire)(importMetaUrl);
8793
- const pkg = req2("../package.json");
8794
- return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8795
- } catch {
8796
- return "0.0.0";
8797
- }
8798
- }
8799
- async function writeDaemonRecord(record, home) {
8800
- const body = JSON.stringify(record, null, 2) + "\n";
8801
- await writeAtomically(daemonJsonPath(record.projectPath), body);
8802
- try {
8803
- await writeAtomically(daemonDiscoveryPath(record.project, home), body);
8804
- } catch (err) {
8805
- console.warn(
8806
- `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
9112
+ // src/connectors/junction.ts
9113
+ init_cjs_shims();
9114
+ var buckets = /* @__PURE__ */ new Map();
9115
+ function bucketMapKey(provider, accountKey) {
9116
+ return `${provider}\0${accountKey}`;
9117
+ }
9118
+ function getBucket(provider, accountKey, config) {
9119
+ const key = bucketMapKey(provider, accountKey);
9120
+ const existing = buckets.get(key);
9121
+ if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {
9122
+ return existing;
9123
+ }
9124
+ const fresh = { ...config, tokens: config.capacity, updatedAt: Date.now() };
9125
+ buckets.set(key, fresh);
9126
+ return fresh;
9127
+ }
9128
+ function refillBucket(bucket, now) {
9129
+ if (now <= bucket.updatedAt) return;
9130
+ const elapsed = now - bucket.updatedAt;
9131
+ const grant = elapsed / bucket.refillMs;
9132
+ if (grant <= 0) return;
9133
+ bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant);
9134
+ bucket.updatedAt = now;
9135
+ }
9136
+ var RateLimitExceededError = class extends Error {
9137
+ constructor(provider, accountKey) {
9138
+ super(
9139
+ `junction: rate limit exceeded for ${provider}:${accountKey} \u2014 waiting for the next token would exceed this call's wall-clock budget`
8807
9140
  );
9141
+ this.name = "RateLimitExceededError";
8808
9142
  }
9143
+ };
9144
+ function delay(ms) {
9145
+ if (ms <= 0) return Promise.resolve();
9146
+ return new Promise((resolve) => {
9147
+ const timer = setTimeout(resolve, ms);
9148
+ if (typeof timer.unref === "function") timer.unref();
9149
+ });
8809
9150
  }
8810
- async function clearDaemonRecord(record, home) {
8811
- try {
8812
- const stopped = { ...record, status: "stopped" };
8813
- await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
8814
- } catch {
8815
- }
9151
+ async function acquireToken(provider, accountKey, config, remainingBudgetMs) {
9152
+ const bucket = getBucket(provider, accountKey, config);
9153
+ refillBucket(bucket, Date.now());
9154
+ if (bucket.tokens >= 1) {
9155
+ bucket.tokens -= 1;
9156
+ return 0;
9157
+ }
9158
+ const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs);
9159
+ if (waitMs > remainingBudgetMs) {
9160
+ throw new RateLimitExceededError(provider, accountKey);
9161
+ }
9162
+ await delay(waitMs);
9163
+ refillBucket(bucket, Date.now());
9164
+ bucket.tokens = Math.max(0, bucket.tokens - 1);
9165
+ return waitMs;
9166
+ }
9167
+ var JUNCTION_DEFAULT_RATE_LIMITS = {
9168
+ // ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of
9169
+ // that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)
9170
+ // stays well clear of the documented limit even under sustained polling.
9171
+ cloudflare: { capacity: 100, refillMs: 3e3 },
9172
+ // Placeholder pending a live project confirming the real cap
9173
+ // (docs/connectors/railway.md: "does not appear to publish one as of this
9174
+ // writing").
9175
+ railway: { capacity: 30, refillMs: 1e4 },
9176
+ // Placeholder pending a live rate-limit check (docs/connectors/
9177
+ // firebase.md: "needs-endpoint-testing against entries.list's live rate
9178
+ // limits").
9179
+ firebase: { capacity: 30, refillMs: 1e4 },
9180
+ // Placeholder pending a live rate-limit check (docs/connectors/supabase.md:
9181
+ // "the documented rate limit for this specific endpoint is unconfirmed").
9182
+ supabase: { capacity: 30, refillMs: 1e4 },
9183
+ // Not a documented API limit at all — a self-imposed ceiling on the raw
9184
+ // pg_stat_statements connection (see module header above).
9185
+ "supabase-postgres": { capacity: 20, refillMs: 3e3 }
9186
+ };
9187
+ var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
9188
+ function defaultRateLimitFor(provider) {
9189
+ return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT;
9190
+ }
9191
+ var JUNCTION_DEFAULT_TIMEOUT_MS = 1e4;
9192
+ var JUNCTION_DEFAULT_MAX_ATTEMPTS = 3;
9193
+ var JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200;
9194
+ var JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4;
9195
+ var JUNCTION_DEFAULT_MAX_ELAPSED_MS = 3e4;
9196
+ var JUNCTION_DEFAULT_DB_TIMEOUT_MS = 1e4;
9197
+ async function backoff(attempt, initialBackoffMs, backoffMultiplier, remainingBudgetMs) {
9198
+ const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1);
9199
+ const capped = Math.max(0, Math.min(raw, remainingBudgetMs));
9200
+ await delay(capped);
9201
+ }
9202
+ function safeUrlLabel(url) {
8816
9203
  try {
8817
- await import_node_fs26.promises.unlink(daemonDiscoveryPath(record.project, home));
9204
+ const u = typeof url === "string" ? new URL(url) : url;
9205
+ return `${u.origin}${u.pathname}`;
8818
9206
  } catch {
9207
+ return String(url);
8819
9208
  }
8820
9209
  }
8821
- function reconcileDaemonRecordSync(record, home) {
8822
- try {
8823
- const stopped = { ...record, status: "stopped" };
8824
- const target = daemonJsonPath(record.projectPath);
8825
- const tmp = `${target}.${process.pid}.tmp`;
8826
- (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
8827
- (0, import_node_fs26.renameSync)(tmp, target);
8828
- } catch {
8829
- }
8830
- try {
8831
- (0, import_node_fs26.unlinkSync)(daemonDiscoveryPath(record.project, home));
8832
- } catch {
9210
+ function logOutcome(provider, accountKey, outcome, method, label, attempt, startedAt) {
9211
+ const elapsedMs = Date.now() - startedAt;
9212
+ const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} \u2014 ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`;
9213
+ if (outcome === "success" || outcome === "retried-then-succeeded") {
9214
+ console.log(line);
9215
+ } else if (outcome === "rate-limited") {
9216
+ console.warn(line);
9217
+ } else {
9218
+ console.error(line);
8833
9219
  }
8834
9220
  }
8835
- function teardownSlot(slot) {
8836
- try {
8837
- slot.stopPersist();
8838
- } catch {
8839
- }
8840
- try {
8841
- slot.stopStaleness();
8842
- } catch {
8843
- }
8844
- try {
8845
- slot.stopConnectors();
9221
+ function bearerAuthHeader(token) {
9222
+ return { Authorization: `Bearer ${token}` };
9223
+ }
9224
+ async function junctionFetch(url, init = {}, policy) {
9225
+ const {
9226
+ provider,
9227
+ accountKey,
9228
+ timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,
9229
+ maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
9230
+ maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
9231
+ initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
9232
+ backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
9233
+ rateLimit = defaultRateLimitFor(provider),
9234
+ fetchImpl = fetch
9235
+ } = policy;
9236
+ const method = (init.method ?? "GET").toUpperCase();
9237
+ const label = safeUrlLabel(url);
9238
+ const startedAt = Date.now();
9239
+ let attempt = 0;
9240
+ let sawRetry = false;
9241
+ for (; ; ) {
9242
+ attempt++;
9243
+ const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
9244
+ if (remainingBudget <= 0) {
9245
+ logOutcome(provider, accountKey, "retried-then-failed", method, label, attempt - 1, startedAt);
9246
+ throw new Error(
9247
+ `junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
9248
+ );
9249
+ }
9250
+ try {
9251
+ await acquireToken(provider, accountKey, rateLimit, remainingBudget);
9252
+ } catch (err) {
9253
+ if (err instanceof RateLimitExceededError) {
9254
+ logOutcome(provider, accountKey, "rate-limited", method, label, attempt, startedAt);
9255
+ }
9256
+ throw err;
9257
+ }
9258
+ const controller = new AbortController();
9259
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
9260
+ if (typeof timer.unref === "function") timer.unref();
9261
+ try {
9262
+ const res = await fetchImpl(url, { ...init, signal: controller.signal });
9263
+ clearTimeout(timer);
9264
+ if (res.ok || res.status < 500) {
9265
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", method, label, attempt, startedAt);
9266
+ return res;
9267
+ }
9268
+ if (attempt >= maxAttempts) {
9269
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
9270
+ return res;
9271
+ }
9272
+ sawRetry = true;
9273
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9274
+ } catch (err) {
9275
+ clearTimeout(timer);
9276
+ if (attempt >= maxAttempts) {
9277
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
9278
+ throw err;
9279
+ }
9280
+ sawRetry = true;
9281
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9282
+ }
9283
+ }
9284
+ }
9285
+ var DbJunctionTimeoutError = class extends Error {
9286
+ constructor(ms) {
9287
+ super(`junction: db query exceeded its ${ms}ms timeout`);
9288
+ this.name = "DbJunctionTimeoutError";
9289
+ }
9290
+ };
9291
+ function withTimeout(run, timeoutMs) {
9292
+ return new Promise((resolve, reject) => {
9293
+ const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs);
9294
+ if (typeof timer.unref === "function") timer.unref();
9295
+ run().then(
9296
+ (value) => {
9297
+ clearTimeout(timer);
9298
+ resolve(value);
9299
+ },
9300
+ (err) => {
9301
+ clearTimeout(timer);
9302
+ reject(err);
9303
+ }
9304
+ );
9305
+ });
9306
+ }
9307
+ var RETRYABLE_NODE_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN", "EPIPE"]);
9308
+ function isRetryableDbError(err) {
9309
+ if (err instanceof DbJunctionTimeoutError) return true;
9310
+ const code = err?.code;
9311
+ if (typeof code !== "string") return false;
9312
+ if (code.startsWith("08") || code === "57P03") return true;
9313
+ return RETRYABLE_NODE_ERROR_CODES.has(code);
9314
+ }
9315
+ async function dbJunction(run, policy) {
9316
+ const {
9317
+ provider,
9318
+ accountKey,
9319
+ timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,
9320
+ maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
9321
+ maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
9322
+ initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
9323
+ backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
9324
+ rateLimit = defaultRateLimitFor(provider)
9325
+ } = policy;
9326
+ const startedAt = Date.now();
9327
+ let attempt = 0;
9328
+ let sawRetry = false;
9329
+ for (; ; ) {
9330
+ attempt++;
9331
+ const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
9332
+ if (remainingBudget <= 0) {
9333
+ logOutcome(provider, accountKey, "retried-then-failed", "QUERY", "db", attempt - 1, startedAt);
9334
+ throw new Error(
9335
+ `junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
9336
+ );
9337
+ }
9338
+ try {
9339
+ await acquireToken(provider, accountKey, rateLimit, remainingBudget);
9340
+ } catch (err) {
9341
+ if (err instanceof RateLimitExceededError) {
9342
+ logOutcome(provider, accountKey, "rate-limited", "QUERY", "db", attempt, startedAt);
9343
+ }
9344
+ throw err;
9345
+ }
9346
+ try {
9347
+ const result = await withTimeout(run, timeoutMs);
9348
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", "QUERY", "db", attempt, startedAt);
9349
+ return result;
9350
+ } catch (err) {
9351
+ if (!isRetryableDbError(err) || attempt >= maxAttempts) {
9352
+ logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", "QUERY", "db", attempt, startedAt);
9353
+ throw err;
9354
+ }
9355
+ sawRetry = true;
9356
+ await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
9357
+ }
9358
+ }
9359
+ }
9360
+
9361
+ // src/connectors/supabase/index.ts
9362
+ init_cjs_shims();
9363
+
9364
+ // src/connectors/supabase/client.ts
9365
+ init_cjs_shims();
9366
+ var DEFAULT_SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com";
9367
+ var DEFAULT_LOG_LIMIT = 1e3;
9368
+ var SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1e3;
9369
+ function boundedSupabaseLogWindow(since, now, maxLookbackMs) {
9370
+ const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS);
9371
+ const floor = new Date(now.getTime() - window);
9372
+ const endIso = now.toISOString();
9373
+ if (!since) return { startIso: floor.toISOString(), endIso, truncated: false };
9374
+ const sinceMs = new Date(since).getTime();
9375
+ if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false };
9376
+ if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true };
9377
+ return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false };
9378
+ }
9379
+ function buildEdgeLogsQuery(limit) {
9380
+ const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT);
9381
+ return [
9382
+ "select",
9383
+ " format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,",
9384
+ " request.method as method,",
9385
+ " request.path as path,",
9386
+ " response.status_code as status_code",
9387
+ "from edge_logs",
9388
+ "cross join unnest(metadata) as metadata",
9389
+ "cross join unnest(metadata.request) as request",
9390
+ "cross join unnest(metadata.response) as response",
9391
+ "where regexp_contains(request.path, '^/rest/v1/')",
9392
+ "order by timestamp asc",
9393
+ `limit ${safeLimit}`
9394
+ ].join("\n");
9395
+ }
9396
+ async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
9397
+ const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
9398
+ const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
9399
+ url.searchParams.set("sql", buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT));
9400
+ url.searchParams.set("iso_timestamp_start", startIso);
9401
+ url.searchParams.set("iso_timestamp_end", endIso);
9402
+ const res = await junctionFetch(
9403
+ url,
9404
+ { method: "GET", headers: bearerAuthHeader(token) },
9405
+ // accountKey: the Supabase project ref (ADR-131's own worked example) —
9406
+ // the Management API's rate limit is enforced per project.
9407
+ { provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
9408
+ );
9409
+ if (!res.ok) {
9410
+ throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
9411
+ }
9412
+ const body = await res.json();
9413
+ if (body.error) {
9414
+ const message = typeof body.error === "string" ? body.error : body.error.message;
9415
+ throw new Error(`supabase connector: logs.all returned an error (${message})`);
9416
+ }
9417
+ return body.result ?? [];
9418
+ }
9419
+
9420
+ // src/connectors/supabase/map.ts
9421
+ init_cjs_shims();
9422
+
9423
+ // src/connectors/supabase/types.ts
9424
+ init_cjs_shims();
9425
+ function readSupabaseCredentials(raw) {
9426
+ const managementToken = raw["managementToken"];
9427
+ if (typeof managementToken !== "string" || managementToken.length === 0) {
9428
+ throw new Error("supabase connector: credentials.managementToken must be a non-empty string");
9429
+ }
9430
+ const postgresConnectionString = raw["postgresConnectionString"];
9431
+ if (postgresConnectionString !== void 0 && (typeof postgresConnectionString !== "string" || postgresConnectionString.length === 0)) {
9432
+ throw new Error(
9433
+ "supabase connector: credentials.postgresConnectionString must be a non-empty string when present"
9434
+ );
9435
+ }
9436
+ return {
9437
+ managementToken,
9438
+ ...postgresConnectionString ? { postgresConnectionString } : {}
9439
+ };
9440
+ }
9441
+ var SUPABASE_TABLE_TARGET_KIND = "supabase-table";
9442
+ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
9443
+
9444
+ // src/connectors/supabase/map.ts
9445
+ var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
9446
+ var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
9447
+ function targetFromRestPath(path50) {
9448
+ const rpcMatch = REST_RPC_PATH_RE.exec(path50);
9449
+ if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
9450
+ const tableMatch = REST_TABLE_PATH_RE.exec(path50);
9451
+ if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
9452
+ return null;
9453
+ }
9454
+ var ERROR_STATUS_THRESHOLD = 500;
9455
+ function mapEdgeLogRowsToSignals(rows) {
9456
+ const buckets2 = /* @__PURE__ */ new Map();
9457
+ for (const row of rows) {
9458
+ const target = targetFromRestPath(row.path);
9459
+ if (!target) continue;
9460
+ const key = `${target.targetKind}:${target.name}`;
9461
+ const isError = row.status_code >= ERROR_STATUS_THRESHOLD;
9462
+ const existing = buckets2.get(key);
9463
+ if (existing) {
9464
+ existing.callCount += 1;
9465
+ if (isError) existing.errorCount += 1;
9466
+ if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp;
9467
+ } else {
9468
+ buckets2.set(key, {
9469
+ targetKind: target.targetKind,
9470
+ targetName: target.name,
9471
+ callCount: 1,
9472
+ errorCount: isError ? 1 : 0,
9473
+ lastObservedIso: row.timestamp
9474
+ });
9475
+ }
9476
+ }
9477
+ return [...buckets2.values()].map((b) => ({
9478
+ targetKind: b.targetKind,
9479
+ targetName: b.targetName,
9480
+ callCount: b.callCount,
9481
+ errorCount: b.errorCount,
9482
+ lastObservedIso: b.lastObservedIso
9483
+ }));
9484
+ }
9485
+ var FROM_TABLE_RE = /\bfrom\s+"?(?:[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i;
9486
+ var SYSTEM_SCHEMA_PREFIXES = ["pg_", "information_schema"];
9487
+ function tableNameFromQueryText(query) {
9488
+ const match = FROM_TABLE_RE.exec(query);
9489
+ if (!match) return null;
9490
+ const name = match[1];
9491
+ const lower = name.toLowerCase();
9492
+ if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
9493
+ return name;
9494
+ }
9495
+ function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
9496
+ const signals = [];
9497
+ const seen = /* @__PURE__ */ new Set();
9498
+ for (const row of rows) {
9499
+ seen.add(row.queryid);
9500
+ const calls = Number(row.calls);
9501
+ const prior = previous.get(row.queryid);
9502
+ previous.set(row.queryid, { calls });
9503
+ if (!prior || calls < prior.calls) continue;
9504
+ const delta = calls - prior.calls;
9505
+ if (delta <= 0) continue;
9506
+ const table = tableNameFromQueryText(row.query);
9507
+ if (!table) continue;
9508
+ signals.push({
9509
+ targetKind: SUPABASE_TABLE_TARGET_KIND,
9510
+ targetName: table,
9511
+ callCount: delta,
9512
+ errorCount: 0,
9513
+ lastObservedIso: nowIso2
9514
+ });
9515
+ }
9516
+ for (const queryid of [...previous.keys()]) {
9517
+ if (!seen.has(queryid)) previous.delete(queryid);
9518
+ }
9519
+ return signals;
9520
+ }
9521
+
9522
+ // src/connectors/supabase/postgres-client.ts
9523
+ init_cjs_shims();
9524
+ var import_pg = __toESM(require("pg"), 1);
9525
+ var { Client } = import_pg.default;
9526
+ var DEFAULT_STATEMENT_LIMIT = 500;
9527
+ var STATEMENTS_QUERY = `
9528
+ select queryid, query, calls, total_exec_time, rows
9529
+ from pg_stat_statements
9530
+ where query ~* '^\\s*select\\b'
9531
+ order by calls desc
9532
+ limit $1
9533
+ `;
9534
+ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT_LIMIT, accountKey = "unknown", clientFactory = (cs) => new Client({ connectionString: cs })) {
9535
+ return dbJunction(
9536
+ async () => {
9537
+ const client = clientFactory(connectionString);
9538
+ await client.connect();
9539
+ try {
9540
+ await client.query("SET default_transaction_read_only = on");
9541
+ const result = await client.query(STATEMENTS_QUERY, [limit]);
9542
+ return result.rows;
9543
+ } finally {
9544
+ await client.end();
9545
+ }
9546
+ },
9547
+ { provider: "supabase-postgres", accountKey }
9548
+ );
9549
+ }
9550
+
9551
+ // src/connectors/supabase/resolve.ts
9552
+ init_cjs_shims();
9553
+ var import_types32 = require("@neat.is/types");
9554
+ function createSupabaseResolveTarget(graph, config) {
9555
+ return (signal, _ctx) => {
9556
+ if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
9557
+ return null;
9558
+ }
9559
+ const subResourceId = (0, import_types32.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
9560
+ if (graph.hasNode(subResourceId)) {
9561
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
9562
+ }
9563
+ const projectLevelId = (0, import_types32.infraId)("supabase", config.nodeRef);
9564
+ if (graph.hasNode(projectLevelId)) {
9565
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
9566
+ }
9567
+ return null;
9568
+ };
9569
+ }
9570
+
9571
+ // src/connectors/supabase/index.ts
9572
+ var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
9573
+ var SupabaseConnector = class {
9574
+ // `deps.fetchPgStatStatements` defaults to the real Postgres-backed
9575
+ // implementation; tests override it to exercise the "both surfaces
9576
+ // combine" and "surface 2 only runs when a connection string is present"
9577
+ // behavior without a live database — the same dependency-injection seam
9578
+ // `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.
9579
+ constructor(config, deps = {}) {
9580
+ this.config = config;
9581
+ this.deps = deps;
9582
+ }
9583
+ config;
9584
+ deps;
9585
+ provider = "supabase";
9586
+ // pg_stat_statements.calls is cumulative, not per-window (map.ts's
9587
+ // diffPgStatStatementsToSignals doc comment) — this Map carries the
9588
+ // previous poll's counts across ticks, the same way
9589
+ // `startConnectorPollLoop` (connectors/index.ts) carries `since` across
9590
+ // ticks for every connector. Lives on the instance, not `ConnectorContext`,
9591
+ // because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's
9592
+ // `{ ...ctx, since }`) while this connector object is the one thing every
9593
+ // tick shares.
9594
+ statementBaselines = /* @__PURE__ */ new Map();
9595
+ async poll(ctx) {
9596
+ const creds = readSupabaseCredentials(ctx.credentials);
9597
+ const now = /* @__PURE__ */ new Date();
9598
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS;
9599
+ const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs);
9600
+ const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso);
9601
+ const signals = mapEdgeLogRowsToSignals(logRows);
9602
+ if (creds.postgresConnectionString) {
9603
+ const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
9604
+ const statementRows = await fetchStatements(
9605
+ creds.postgresConnectionString,
9606
+ this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
9607
+ this.config.apiProjectRef
9608
+ );
9609
+ signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
9610
+ }
9611
+ return signals;
9612
+ }
9613
+ };
9614
+ function createSupabaseConnector(graph, config, deps = {}) {
9615
+ return {
9616
+ connector: new SupabaseConnector(config, deps),
9617
+ resolveTarget: createSupabaseResolveTarget(graph, config)
9618
+ };
9619
+ }
9620
+
9621
+ // src/connectors/railway/index.ts
9622
+ init_cjs_shims();
9623
+ var import_types36 = require("@neat.is/types");
9624
+
9625
+ // src/connectors/railway/client.ts
9626
+ init_cjs_shims();
9627
+ var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
9628
+ var DEFAULT_MAX_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
9629
+ var DEFAULT_LOG_LIMIT2 = 1e3;
9630
+ function readRailwayToken(credentials) {
9631
+ const token = credentials.token;
9632
+ if (typeof token !== "string" || token.length === 0) {
9633
+ throw new Error(
9634
+ "Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)"
9635
+ );
9636
+ }
9637
+ return token;
9638
+ }
9639
+ async function railwayGraphQL(apiUrl, token, query, variables, accountKey) {
9640
+ const res = await junctionFetch(
9641
+ apiUrl,
9642
+ {
9643
+ method: "POST",
9644
+ headers: {
9645
+ "Content-Type": "application/json",
9646
+ // docs.railway.com/integrations/api/graphql-overview confirms
9647
+ // `Authorization: Bearer <token>` for an account/workspace token;
9648
+ // Railway also documents a dedicated `Project-Access-Token: <token>`
9649
+ // header for the project-scoped token ADR-127 targets specifically.
9650
+ // This connector sends the Bearer form — needs-endpoint-testing
9651
+ // whether a live Project-Access-Token requires the dedicated header
9652
+ // instead once this poller runs against a real project.
9653
+ ...bearerAuthHeader(token)
9654
+ },
9655
+ body: JSON.stringify({ query, variables })
9656
+ },
9657
+ { provider: "railway", accountKey }
9658
+ );
9659
+ if (!res.ok) {
9660
+ throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`);
9661
+ }
9662
+ const body = await res.json();
9663
+ if (body.errors && body.errors.length > 0) {
9664
+ throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
9665
+ }
9666
+ if (!body.data) throw new Error("Railway GraphQL response carried no data");
9667
+ return body.data;
9668
+ }
9669
+ var HTTP_LOGS_QUERY = `
9670
+ query HttpLogs(
9671
+ $environmentId: String!
9672
+ $serviceId: String!
9673
+ $startDate: String!
9674
+ $endDate: String!
9675
+ $limit: Int
9676
+ ) {
9677
+ httpLogs(
9678
+ environmentId: $environmentId
9679
+ serviceId: $serviceId
9680
+ startDate: $startDate
9681
+ endDate: $endDate
9682
+ limit: $limit
9683
+ ) {
9684
+ timestamp
9685
+ method
9686
+ path
9687
+ httpStatus
9688
+ totalDuration
9689
+ requestId
9690
+ deploymentId
9691
+ edgeRegion
9692
+ }
9693
+ }
9694
+ `;
9695
+ var NETWORK_FLOW_LOGS_QUERY = `
9696
+ query NetworkFlowLogs(
9697
+ $environmentId: String!
9698
+ $serviceId: String!
9699
+ $startDate: String!
9700
+ $endDate: String!
9701
+ $limit: Int
9702
+ ) {
9703
+ networkFlowLogs(
9704
+ environmentId: $environmentId
9705
+ serviceId: $serviceId
9706
+ startDate: $startDate
9707
+ endDate: $endDate
9708
+ limit: $limit
9709
+ ) {
9710
+ timestamp
9711
+ peerServiceId
9712
+ peerKind
9713
+ direction
9714
+ byteCount
9715
+ packetCount
9716
+ dropCause
9717
+ }
9718
+ }
9719
+ `;
9720
+ async function fetchRailwayHttpLogs(config, token, startDate, endDate) {
9721
+ const data = await railwayGraphQL(
9722
+ config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
9723
+ token,
9724
+ HTTP_LOGS_QUERY,
9725
+ {
9726
+ environmentId: config.environmentId,
9727
+ serviceId: config.serviceId,
9728
+ startDate,
9729
+ endDate,
9730
+ limit: config.limit ?? DEFAULT_LOG_LIMIT2
9731
+ },
9732
+ config.environmentId
9733
+ );
9734
+ return data.httpLogs;
9735
+ }
9736
+ async function fetchRailwayNetworkFlowLogs(config, token, startDate, endDate) {
9737
+ const data = await railwayGraphQL(
9738
+ config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
9739
+ token,
9740
+ NETWORK_FLOW_LOGS_QUERY,
9741
+ {
9742
+ environmentId: config.environmentId,
9743
+ serviceId: config.serviceId,
9744
+ startDate,
9745
+ endDate,
9746
+ limit: config.limit ?? DEFAULT_LOG_LIMIT2
9747
+ },
9748
+ config.environmentId
9749
+ );
9750
+ return data.networkFlowLogs;
9751
+ }
9752
+ function boundedRailwayStartDate(since, now, maxLookbackMs) {
9753
+ const floor = new Date(now.getTime() - maxLookbackMs);
9754
+ if (!since) return floor.toISOString();
9755
+ const sinceMs = new Date(since).getTime();
9756
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
9757
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
9758
+ }
9759
+
9760
+ // src/connectors/railway/index.ts
9761
+ var ROUTE_TARGET_KIND = "route";
9762
+ var UNMATCHED_ROUTE_TARGET_KIND = "unmatched-route";
9763
+ var PEER_SERVICE_TARGET_KIND = "peer-service";
9764
+ function buildRailwayRouteIndex(graph, serviceName) {
9765
+ const out = [];
9766
+ graph.forEachNode((_id, attrs) => {
9767
+ const node = attrs;
9768
+ if (node.type !== import_types36.NodeType.RouteNode) return;
9769
+ const route = attrs;
9770
+ if (route.service !== serviceName) return;
9771
+ out.push({
9772
+ method: route.method.toUpperCase(),
9773
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
9774
+ routeNodeId: route.id,
9775
+ path: route.path,
9776
+ line: route.line
9777
+ });
9778
+ });
9779
+ return out;
9780
+ }
9781
+ function findRailwayRoute(entries, method, normalizedPath) {
9782
+ return entries.find(
9783
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
9784
+ );
9785
+ }
9786
+ function bucketKey2(method, normalizedPath) {
9787
+ return `${method} ${normalizedPath}`;
9788
+ }
9789
+ function isHttpErrorStatus(status2) {
9790
+ return status2 >= 400;
9791
+ }
9792
+ function upsertBucket(buckets2, key, isError, timestamp, build) {
9793
+ const existing = buckets2.get(key);
9794
+ if (existing) {
9795
+ existing.callCount += 1;
9796
+ if (isError) existing.errorCount += 1;
9797
+ if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
9798
+ return;
9799
+ }
9800
+ buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
9801
+ }
9802
+ function mapRailwayHttpLogsToSignals(entries, routeIndex) {
9803
+ const buckets2 = /* @__PURE__ */ new Map();
9804
+ for (const entry2 of entries) {
9805
+ const method = entry2.method.toUpperCase();
9806
+ const normalizedPath = normalizePathTemplate(entry2.path);
9807
+ const match = findRailwayRoute(routeIndex, method, normalizedPath);
9808
+ const isError = isHttpErrorStatus(entry2.httpStatus);
9809
+ if (match) {
9810
+ upsertBucket(buckets2, `route:${match.routeNodeId}`, isError, entry2.timestamp, () => ({
9811
+ targetKind: ROUTE_TARGET_KIND,
9812
+ targetName: match.routeNodeId,
9813
+ // RouteNode.line is optional in the schema (packages/types/src/
9814
+ // nodes.ts) even though routes.ts always sets it today — skip the
9815
+ // callSite rather than fabricate a line when it's ever absent
9816
+ // (file-awareness.md §6).
9817
+ ...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
9818
+ }));
9819
+ } else {
9820
+ upsertBucket(
9821
+ buckets2,
9822
+ `unmatched:${bucketKey2(method, normalizedPath)}`,
9823
+ isError,
9824
+ entry2.timestamp,
9825
+ () => ({
9826
+ targetKind: UNMATCHED_ROUTE_TARGET_KIND,
9827
+ targetName: bucketKey2(method, normalizedPath)
9828
+ })
9829
+ );
9830
+ }
9831
+ }
9832
+ return [...buckets2.values()].map((b) => ({
9833
+ targetKind: b.targetKind,
9834
+ targetName: b.targetName,
9835
+ callCount: b.callCount,
9836
+ errorCount: b.errorCount,
9837
+ lastObservedIso: b.lastObservedIso,
9838
+ ...b.callSite ? { callSite: b.callSite } : {}
9839
+ }));
9840
+ }
9841
+ function mapRailwayNetworkFlowLogsToSignals(entries) {
9842
+ const buckets2 = /* @__PURE__ */ new Map();
9843
+ for (const entry2 of entries) {
9844
+ if (!entry2.peerServiceId) continue;
9845
+ const isError = entry2.dropCause !== null && entry2.dropCause !== "";
9846
+ upsertBucket(buckets2, entry2.peerServiceId, isError, entry2.timestamp, () => ({
9847
+ targetKind: PEER_SERVICE_TARGET_KIND,
9848
+ targetName: entry2.peerServiceId
9849
+ }));
9850
+ }
9851
+ return [...buckets2.values()].map((b) => ({
9852
+ targetKind: b.targetKind,
9853
+ targetName: b.targetName,
9854
+ callCount: b.callCount,
9855
+ errorCount: b.errorCount,
9856
+ lastObservedIso: b.lastObservedIso
9857
+ }));
9858
+ }
9859
+ function createRailwayResolveTarget(config) {
9860
+ return (signal) => {
9861
+ const serviceName = config.serviceNameById[config.serviceId];
9862
+ if (!serviceName) return null;
9863
+ if (signal.targetKind === ROUTE_TARGET_KIND) {
9864
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types36.EdgeType.CALLS };
9865
+ }
9866
+ if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
9867
+ const peerName = config.serviceNameById[signal.targetName];
9868
+ if (!peerName) return null;
9869
+ return { targetNodeId: (0, import_types36.serviceId)(peerName), serviceName, edgeType: import_types36.EdgeType.CONNECTS_TO };
9870
+ }
9871
+ return null;
9872
+ };
9873
+ }
9874
+ function createRailwayConnector(graph, config) {
9875
+ return {
9876
+ provider: "railway",
9877
+ async poll(ctx) {
9878
+ const token = readRailwayToken(ctx.credentials);
9879
+ const now = /* @__PURE__ */ new Date();
9880
+ const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS2;
9881
+ const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs);
9882
+ const endDate = now.toISOString();
9883
+ const [httpLogs, flowLogs] = await Promise.all([
9884
+ fetchRailwayHttpLogs(config, token, startDate, endDate),
9885
+ fetchRailwayNetworkFlowLogs(config, token, startDate, endDate)
9886
+ ]);
9887
+ const serviceName = config.serviceNameById[config.serviceId];
9888
+ const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : [];
9889
+ return [
9890
+ ...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),
9891
+ ...mapRailwayNetworkFlowLogsToSignals(flowLogs)
9892
+ ];
9893
+ }
9894
+ };
9895
+ }
9896
+
9897
+ // src/connectors/firebase/index.ts
9898
+ init_cjs_shims();
9899
+
9900
+ // src/connectors/firebase/logging-api.ts
9901
+ init_cjs_shims();
9902
+ function readFirebaseCredentials(raw) {
9903
+ const projectId = raw["projectId"];
9904
+ const accessToken = raw["accessToken"];
9905
+ if (typeof projectId !== "string" || projectId.length === 0) {
9906
+ throw new Error("firebase connector: credentials.projectId must be a non-empty string");
9907
+ }
9908
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
9909
+ throw new Error("firebase connector: credentials.accessToken must be a non-empty string");
9910
+ }
9911
+ return { projectId, accessToken };
9912
+ }
9913
+ var RESOURCE_TYPES = [
9914
+ "cloud_function",
9915
+ "cloud_run_revision",
9916
+ "firebase_domain"
9917
+ ];
9918
+ function isFirebaseResourceType(value) {
9919
+ return RESOURCE_TYPES.includes(value);
9920
+ }
9921
+ function buildEntriesFilter(sinceIso) {
9922
+ return [
9923
+ 'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
9924
+ "httpRequest:*",
9925
+ `timestamp >= "${sinceIso}"`
9926
+ ].join(" AND ");
9927
+ }
9928
+ var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
9929
+ var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
9930
+ var PAGE_SIZE = 1e3;
9931
+ var MAX_PAGES = 20;
9932
+ async function fetchHttpRequestLogEntries(creds, sinceIso) {
9933
+ const filter = buildEntriesFilter(sinceIso);
9934
+ const out = [];
9935
+ let pageToken;
9936
+ for (let page = 0; page < MAX_PAGES; page++) {
9937
+ const body = {
9938
+ resourceNames: [`projects/${creds.projectId}`],
9939
+ filter,
9940
+ orderBy: "timestamp asc",
9941
+ pageSize: PAGE_SIZE,
9942
+ ...pageToken ? { pageToken } : {}
9943
+ };
9944
+ const res = await junctionFetch(
9945
+ ENTRIES_LIST_URL,
9946
+ {
9947
+ method: "POST",
9948
+ headers: {
9949
+ ...bearerAuthHeader(creds.accessToken),
9950
+ "Content-Type": "application/json"
9951
+ },
9952
+ body: JSON.stringify(body)
9953
+ },
9954
+ // accountKey: the GCP project id (ADR-131's own worked example for
9955
+ // Firebase) — one customer's Cloud Logging quota is scoped per GCP
9956
+ // project, not per Firebase site/function.
9957
+ { provider: "firebase", accountKey: creds.projectId }
9958
+ );
9959
+ if (!res.ok) {
9960
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
9961
+ }
9962
+ const json = await res.json();
9963
+ out.push(...json.entries ?? []);
9964
+ if (!json.nextPageToken) break;
9965
+ pageToken = json.nextPageToken;
9966
+ }
9967
+ return out;
9968
+ }
9969
+
9970
+ // src/connectors/firebase/map.ts
9971
+ init_cjs_shims();
9972
+ var FIELD_SEP = "\0";
9973
+ function packFirebaseTargetName(identity) {
9974
+ return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP);
9975
+ }
9976
+ function parseFirebaseTargetName(targetName) {
9977
+ const firstSep = targetName.indexOf(FIELD_SEP);
9978
+ if (firstSep === -1) return null;
9979
+ const resourceName = targetName.slice(0, firstSep);
9980
+ const rest = targetName.slice(firstSep + 1);
9981
+ const secondSep = rest.indexOf(FIELD_SEP);
9982
+ if (secondSep === -1) return null;
9983
+ const method = rest.slice(0, secondSep);
9984
+ const path50 = rest.slice(secondSep + 1);
9985
+ if (!resourceName || !method || !path50) return null;
9986
+ return { resourceName, method, path: path50 };
9987
+ }
9988
+ function resourceNameFor(type, labels) {
9989
+ if (!labels) return null;
9990
+ switch (type) {
9991
+ case "cloud_function":
9992
+ return labels["function_name"] ?? null;
9993
+ case "cloud_run_revision":
9994
+ return labels["service_name"] ?? null;
9995
+ case "firebase_domain":
9996
+ return labels["site_name"] ?? null;
9997
+ }
9998
+ }
9999
+ function pathFromRequestUrl(requestUrl) {
10000
+ if (!requestUrl) return null;
10001
+ if (requestUrl.startsWith("/")) {
10002
+ const withoutQuery = requestUrl.split("?")[0];
10003
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
10004
+ }
10005
+ try {
10006
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
10007
+ const parsed = new URL(candidate);
10008
+ return parsed.pathname || "/";
10009
+ } catch {
10010
+ return null;
10011
+ }
10012
+ }
10013
+ var ERROR_STATUS_THRESHOLD2 = 500;
10014
+ function mapLogEntryToSignal(entry2) {
10015
+ const resourceType = entry2.resource?.type;
10016
+ if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
10017
+ const resourceName = resourceNameFor(resourceType, entry2.resource?.labels);
10018
+ if (!resourceName) return null;
10019
+ const req2 = entry2.httpRequest;
10020
+ if (!req2) return null;
10021
+ if (!req2.requestMethod) return null;
10022
+ const method = req2.requestMethod.toUpperCase();
10023
+ const path50 = pathFromRequestUrl(req2.requestUrl);
10024
+ if (path50 === null) return null;
10025
+ const timestamp = entry2.timestamp;
10026
+ if (!timestamp) return null;
10027
+ const isError = typeof req2.status === "number" && req2.status >= ERROR_STATUS_THRESHOLD2;
10028
+ return {
10029
+ targetKind: resourceType,
10030
+ targetName: packFirebaseTargetName({ resourceName, method, path: path50 }),
10031
+ callCount: 1,
10032
+ errorCount: isError ? 1 : 0,
10033
+ lastObservedIso: timestamp
10034
+ };
10035
+ }
10036
+ function mapLogEntriesToSignals(entries) {
10037
+ const out = [];
10038
+ for (const entry2 of entries) {
10039
+ const signal = mapLogEntryToSignal(entry2);
10040
+ if (signal) out.push(signal);
10041
+ }
10042
+ return out;
10043
+ }
10044
+
10045
+ // src/connectors/firebase/resolve.ts
10046
+ init_cjs_shims();
10047
+ var import_types37 = require("@neat.is/types");
10048
+ function neatServiceNameFor(resourceType, resourceName, serviceMap) {
10049
+ switch (resourceType) {
10050
+ case "cloud_function":
10051
+ return serviceMap.functions?.[resourceName] ?? null;
10052
+ case "cloud_run_revision":
10053
+ return serviceMap.cloudRun?.[resourceName] ?? null;
10054
+ case "firebase_domain":
10055
+ return serviceMap.hosting?.[resourceName] ?? null;
10056
+ }
10057
+ }
10058
+ function routeEntriesFor(graph, serviceName) {
10059
+ const entries = [];
10060
+ graph.forEachNode((_id, attrs) => {
10061
+ const node = attrs;
10062
+ if (node.type !== import_types37.NodeType.RouteNode) return;
10063
+ const route = attrs;
10064
+ if (route.service !== serviceName) return;
10065
+ entries.push({
10066
+ method: route.method.toUpperCase(),
10067
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
10068
+ routeNodeId: route.id
10069
+ });
10070
+ });
10071
+ return entries;
10072
+ }
10073
+ function findRoute2(entries, method, normalizedPath) {
10074
+ return entries.find(
10075
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
10076
+ );
10077
+ }
10078
+ function createFirebaseResolveTarget(graph, serviceMap) {
10079
+ return (signal, _ctx) => {
10080
+ const resourceType = signal.targetKind;
10081
+ if (resourceType !== "cloud_function" && resourceType !== "cloud_run_revision" && resourceType !== "firebase_domain") {
10082
+ return null;
10083
+ }
10084
+ const identity = parseFirebaseTargetName(signal.targetName);
10085
+ if (!identity) return null;
10086
+ const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
10087
+ if (!serviceName) return null;
10088
+ const normalizedPath = normalizePathTemplate(identity.path);
10089
+ const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
10090
+ if (!match) return null;
10091
+ return {
10092
+ targetNodeId: match.routeNodeId,
10093
+ serviceName,
10094
+ edgeType: import_types37.EdgeType.CALLS
10095
+ };
10096
+ };
10097
+ }
10098
+
10099
+ // src/connectors/firebase/index.ts
10100
+ var FirebaseConnector = class {
10101
+ provider = "firebase";
10102
+ async poll(ctx) {
10103
+ const creds = readFirebaseCredentials(ctx.credentials);
10104
+ const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
10105
+ const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
10106
+ return mapLogEntriesToSignals(entries);
10107
+ }
10108
+ };
10109
+ function createFirebaseConnector(graph, serviceMap) {
10110
+ return {
10111
+ connector: new FirebaseConnector(),
10112
+ resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
10113
+ };
10114
+ }
10115
+
10116
+ // src/connectors/cloudflare/index.ts
10117
+ init_cjs_shims();
10118
+
10119
+ // src/connectors/cloudflare/connector.ts
10120
+ init_cjs_shims();
10121
+ var import_types39 = require("@neat.is/types");
10122
+
10123
+ // src/connectors/cloudflare/client.ts
10124
+ init_cjs_shims();
10125
+ var import_node_crypto3 = require("crypto");
10126
+ var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
10127
+ var DEFAULT_EVENT_LIMIT = 1e3;
10128
+ async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
10129
+ const token = ctx.credentials.apiToken;
10130
+ if (typeof token !== "string" || token.length === 0) {
10131
+ throw new Error("cloudflare connector: ctx.credentials.apiToken must be a non-empty string");
10132
+ }
10133
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
10134
+ const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`;
10135
+ const body = {
10136
+ // Cloudflare's schema requires an identifier per query even for an
10137
+ // ad-hoc, unsaved one — a fresh id per tick, never reused.
10138
+ queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
10139
+ timeframe: { from: window.fromMs, to: window.toMs },
10140
+ view: "events",
10141
+ limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
10142
+ // Execute without persisting — this is a read, not a saved query
10143
+ // (connectors.md §2's "never writes on the read path" applies to
10144
+ // Cloudflare's own query-history state too).
10145
+ dry: true
10146
+ };
10147
+ const res = await junctionFetch(
10148
+ url,
10149
+ {
10150
+ method: "POST",
10151
+ headers: {
10152
+ "Content-Type": "application/json",
10153
+ ...bearerAuthHeader(token)
10154
+ },
10155
+ body: JSON.stringify(body)
10156
+ },
10157
+ // accountKey: the Cloudflare account id (ADR-131's own worked example) —
10158
+ // the Telemetry Query API's ~300/5min limit is enforced per account.
10159
+ { provider: "cloudflare", accountKey: config.accountId, fetchImpl }
10160
+ );
10161
+ if (!res.ok) {
10162
+ throw new Error(
10163
+ `cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`
10164
+ );
10165
+ }
10166
+ const payload = await res.json();
10167
+ if (!payload.success) {
10168
+ const message = payload.errors?.map((e) => e.message).join("; ") || "unknown error";
10169
+ throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
10170
+ }
10171
+ const events = payload.result?.events?.events;
10172
+ if (events === void 0) {
10173
+ console.warn(
10174
+ "[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"
10175
+ );
10176
+ return [];
10177
+ }
10178
+ return events;
10179
+ }
10180
+
10181
+ // src/connectors/cloudflare/map.ts
10182
+ init_cjs_shims();
10183
+
10184
+ // src/connectors/cloudflare/types.ts
10185
+ init_cjs_shims();
10186
+ var CLOUDFLARE_TARGET_KIND = "cloudflare-worker-invocation";
10187
+
10188
+ // src/connectors/cloudflare/map.ts
10189
+ var HTTP_METHODS = /* @__PURE__ */ new Set([
10190
+ "GET",
10191
+ "POST",
10192
+ "PUT",
10193
+ "PATCH",
10194
+ "DELETE",
10195
+ "HEAD",
10196
+ "OPTIONS",
10197
+ "TRACE",
10198
+ "CONNECT"
10199
+ ]);
10200
+ var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
10201
+ function parseHttpMethodFromTrigger(trigger) {
10202
+ if (!trigger) return null;
10203
+ const match = LEADING_TOKEN_RE.exec(trigger.trim());
10204
+ const token = match?.[1];
10205
+ if (!token) return null;
10206
+ const method = token.toUpperCase();
10207
+ return HTTP_METHODS.has(method) ? method : null;
10208
+ }
10209
+ function parsePathFromTrigger(trigger) {
10210
+ const trimmed = trigger.trim();
10211
+ const spaceIdx = trimmed.indexOf(" ");
10212
+ if (spaceIdx === -1) return void 0;
10213
+ const rest = trimmed.slice(spaceIdx + 1).trim();
10214
+ return rest.length > 0 ? rest : void 0;
10215
+ }
10216
+ var ERROR_STATUS_THRESHOLD3 = 500;
10217
+ function mapEventToSignal(event) {
10218
+ const metadata = event.$metadata;
10219
+ const workers = event.$workers;
10220
+ const method = parseHttpMethodFromTrigger(metadata?.trigger);
10221
+ if (!method) return null;
10222
+ const scriptName = workers?.scriptName ?? metadata?.service;
10223
+ if (!scriptName) return null;
10224
+ const timestampMs = event.timestamp ?? metadata?.startTime;
10225
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
10226
+ const statusCode = metadata?.statusCode;
10227
+ const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
10228
+ const path50 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
10229
+ return {
10230
+ targetKind: CLOUDFLARE_TARGET_KIND,
10231
+ targetName: scriptName,
10232
+ callCount: 1,
10233
+ errorCount: isError ? 1 : 0,
10234
+ lastObservedIso: new Date(timestampMs).toISOString(),
10235
+ method,
10236
+ ...path50 ? { path: path50 } : {},
10237
+ ...typeof statusCode === "number" ? { statusCode } : {},
10238
+ ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
10239
+ };
10240
+ }
10241
+
10242
+ // src/connectors/cloudflare/connector.ts
10243
+ var DEFAULT_MAX_LOOKBACK_MS3 = 60 * 60 * 1e3;
10244
+ function resolveFromMs(since, maxLookbackMs) {
10245
+ const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS3;
10246
+ const now = Date.now();
10247
+ const floor = now - cap;
10248
+ if (!since) return floor;
10249
+ const parsed = Date.parse(since);
10250
+ if (Number.isNaN(parsed)) return floor;
10251
+ return Math.max(parsed, floor);
10252
+ }
10253
+ var CloudflareConnector = class {
10254
+ constructor(config) {
10255
+ this.config = config;
10256
+ }
10257
+ config;
10258
+ provider = "cloudflare";
10259
+ async poll(ctx) {
10260
+ const toMs = Date.now();
10261
+ const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs);
10262
+ const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs });
10263
+ const signals = [];
10264
+ for (const event of events) {
10265
+ const signal = mapEventToSignal(event);
10266
+ if (signal) signals.push(signal);
10267
+ }
10268
+ return signals;
10269
+ }
10270
+ };
10271
+ function findTaggedWorkerFileNode(graph, workerName) {
10272
+ let found = null;
10273
+ graph.forEachNode((id, attrs) => {
10274
+ if (found) return;
10275
+ const a = attrs;
10276
+ if (a.type === import_types39.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
10277
+ found = id;
10278
+ }
10279
+ });
10280
+ return found;
10281
+ }
10282
+ function findMatchingRouteNode(graph, serviceName, method, path50) {
10283
+ const normalizedPath = normalizePathTemplate(path50);
10284
+ let found = null;
10285
+ graph.forEachNode((id, attrs) => {
10286
+ if (found) return;
10287
+ const a = attrs;
10288
+ if (a.type !== import_types39.NodeType.RouteNode || a.service !== serviceName) return;
10289
+ if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
10290
+ const routeMethod = (a.method ?? "").toUpperCase();
10291
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
10292
+ found = id;
10293
+ });
10294
+ return found;
10295
+ }
10296
+ function createCloudflareResolveTarget(config, graph) {
10297
+ return (signal) => {
10298
+ if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
10299
+ const scriptName = signal.targetName;
10300
+ const { method, path: path50 } = signal;
10301
+ const resolveRouteGrain = (serviceName, wholeFileId) => {
10302
+ if (!method || !path50) return wholeFileId;
10303
+ return findMatchingRouteNode(graph, serviceName, method, path50) ?? wholeFileId;
10304
+ };
10305
+ const mapping = config.workers?.[scriptName];
10306
+ if (mapping) {
10307
+ const wholeFileId = (0, import_types39.fileId)(mapping.service, mapping.entryFile);
10308
+ return {
10309
+ targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
10310
+ serviceName: mapping.service,
10311
+ edgeType: import_types39.EdgeType.CALLS
10312
+ };
10313
+ }
10314
+ const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
10315
+ if (taggedFileId) {
10316
+ const fileNode = graph.getNodeAttributes(taggedFileId);
10317
+ return {
10318
+ targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
10319
+ serviceName: fileNode.service,
10320
+ edgeType: import_types39.EdgeType.CALLS
10321
+ };
10322
+ }
10323
+ return {
10324
+ targetNodeId: (0, import_types39.infraId)("cloudflare-worker", scriptName),
10325
+ serviceName: scriptName,
10326
+ edgeType: import_types39.EdgeType.CALLS,
10327
+ ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
10328
+ };
10329
+ };
10330
+ }
10331
+
10332
+ // src/connectors-config.ts
10333
+ init_cjs_shims();
10334
+ var import_node_os4 = __toESM(require("os"), 1);
10335
+ var import_node_path45 = __toESM(require("path"), 1);
10336
+ var import_node_fs26 = require("fs");
10337
+ var CONNECTORS_CONFIG_VERSION = 1;
10338
+ var EnvRefUnsetError = class extends Error {
10339
+ ref;
10340
+ varName;
10341
+ constructor(ref, varName) {
10342
+ super(`${ref} is unset`);
10343
+ this.name = "EnvRefUnsetError";
10344
+ this.ref = ref;
10345
+ this.varName = varName;
10346
+ }
10347
+ };
10348
+ function neatHome2() {
10349
+ const override = process.env.NEAT_HOME;
10350
+ if (override && override.length > 0) return import_node_path45.default.resolve(override);
10351
+ return import_node_path45.default.join(import_node_os4.default.homedir(), ".neat");
10352
+ }
10353
+ function connectorsConfigPath(home = neatHome2()) {
10354
+ return import_node_path45.default.join(home, "connectors.json");
10355
+ }
10356
+ var MODE_MASK_LOOSER_THAN_0600 = 63;
10357
+ async function warnIfModeLooserThan0600(file) {
10358
+ if (process.platform === "win32") return;
10359
+ try {
10360
+ const stat = await import_node_fs26.promises.stat(file);
10361
+ if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
10362
+ const mode = (stat.mode & 511).toString(8).padStart(3, "0");
10363
+ console.warn(
10364
+ `[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
10365
+ );
10366
+ }
10367
+ } catch {
10368
+ }
10369
+ }
10370
+ async function readConnectorsConfig(home = neatHome2()) {
10371
+ const file = connectorsConfigPath(home);
10372
+ let raw;
10373
+ try {
10374
+ raw = await import_node_fs26.promises.readFile(file, "utf8");
10375
+ } catch (err) {
10376
+ if (err.code === "ENOENT") {
10377
+ return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
10378
+ }
10379
+ throw err;
10380
+ }
10381
+ await warnIfModeLooserThan0600(file);
10382
+ let parsed;
10383
+ try {
10384
+ parsed = JSON.parse(raw);
10385
+ } catch (err) {
10386
+ throw new Error(`${file} is not valid JSON: ${err.message}`);
10387
+ }
10388
+ return validateConfig(parsed, file);
10389
+ }
10390
+ function validateConfig(parsed, file) {
10391
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10392
+ throw new Error(`${file} must be a JSON object with a "connectors" array`);
10393
+ }
10394
+ const obj = parsed;
10395
+ const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
10396
+ if (typeof version !== "number" || !Number.isInteger(version)) {
10397
+ throw new Error(`${file}: "version" must be an integer`);
10398
+ }
10399
+ const rawConnectors = obj.connectors;
10400
+ if (!Array.isArray(rawConnectors)) {
10401
+ throw new Error(`${file}: "connectors" must be an array`);
10402
+ }
10403
+ const connectors = rawConnectors.map((entry2, i) => validateEntry(entry2, i, file));
10404
+ return { version, connectors };
10405
+ }
10406
+ function validateEntry(entry2, index, file) {
10407
+ const where = `${file}: connectors[${index}]`;
10408
+ if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
10409
+ throw new Error(`${where} must be an object`);
10410
+ }
10411
+ const e = entry2;
10412
+ const id = e.id;
10413
+ if (typeof id !== "string" || id.length === 0) {
10414
+ throw new Error(`${where}.id must be a non-empty string`);
10415
+ }
10416
+ const provider = e.provider;
10417
+ if (typeof provider !== "string" || provider.length === 0) {
10418
+ throw new Error(`${where}.provider must be a non-empty string`);
10419
+ }
10420
+ if (e.project !== void 0 && typeof e.project !== "string") {
10421
+ throw new Error(`${where}.project must be a string when present`);
10422
+ }
10423
+ const credential = validateCredentialRef(e.credential, `${where}.credential`);
10424
+ let options;
10425
+ if (e.options !== void 0) {
10426
+ if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
10427
+ throw new Error(`${where}.options must be an object when present`);
10428
+ }
10429
+ options = e.options;
10430
+ }
10431
+ return {
10432
+ id,
10433
+ provider,
10434
+ ...typeof e.project === "string" ? { project: e.project } : {},
10435
+ credential,
10436
+ ...options ? { options } : {}
10437
+ };
10438
+ }
10439
+ function validateCredentialRef(raw, where) {
10440
+ if (typeof raw === "string") {
10441
+ if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
10442
+ return raw;
10443
+ }
10444
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
10445
+ const fields = raw;
10446
+ const keys = Object.keys(fields);
10447
+ if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
10448
+ for (const key of keys) {
10449
+ const v = fields[key];
10450
+ if (typeof v !== "string" || v.length === 0) {
10451
+ throw new Error(`${where}.${key} must be a non-empty string`);
10452
+ }
10453
+ }
10454
+ return fields;
10455
+ }
10456
+ throw new Error(`${where} must be a string or an object of strings`);
10457
+ }
10458
+ function resolveCredential(ref, env = process.env) {
10459
+ if (typeof ref === "string") {
10460
+ return { kind: "single", value: resolveRef(ref, env) };
10461
+ }
10462
+ const fields = {};
10463
+ for (const [key, value] of Object.entries(ref)) {
10464
+ fields[key] = resolveRef(value, env);
10465
+ }
10466
+ return { kind: "fields", fields };
10467
+ }
10468
+ function resolveRef(value, env) {
10469
+ if (value.length > 1 && value.startsWith("$")) {
10470
+ const varName = value.slice(1);
10471
+ const resolved = env[varName];
10472
+ if (resolved === void 0 || resolved.length === 0) {
10473
+ throw new EnvRefUnsetError(value, varName);
10474
+ }
10475
+ return resolved;
10476
+ }
10477
+ return value;
10478
+ }
10479
+ function connectorMatchesProject(entry2, project) {
10480
+ return entry2.project === void 0 || entry2.project === project;
10481
+ }
10482
+
10483
+ // src/connectors/registry.ts
10484
+ var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
10485
+ async function authProbe(input) {
10486
+ const { provider, accountKey, url, token, init, fetchImpl } = input;
10487
+ try {
10488
+ const res = await junctionFetch(
10489
+ url,
10490
+ {
10491
+ ...init ?? {},
10492
+ headers: { ...bearerAuthHeader(token), ...init?.headers ?? {} }
10493
+ },
10494
+ { provider, accountKey, ...fetchImpl ? { fetchImpl } : {} }
10495
+ );
10496
+ if (res.ok) return { ok: true };
10497
+ if (res.status === 401 || res.status === 403) {
10498
+ return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` };
10499
+ }
10500
+ return {
10501
+ ok: false,
10502
+ reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} \u2014 could not confirm the credential`
10503
+ };
10504
+ } catch (err) {
10505
+ return {
10506
+ ok: false,
10507
+ reason: `${provider} auth check could not reach the provider: ${err.message}`
10508
+ };
10509
+ }
10510
+ }
10511
+ var PROVIDER_DISPATCH = {
10512
+ supabase: {
10513
+ provider: "supabase",
10514
+ primaryCredentialKey: "managementToken",
10515
+ requiredCredentialFields: ["managementToken"],
10516
+ requiredOptionFields: ["apiProjectRef", "nodeRef", "serviceName"],
10517
+ build(graph, options) {
10518
+ return createSupabaseConnector(graph, options);
10519
+ },
10520
+ // GET /v1/projects — the Management API's own auth-gated list endpoint, the
10521
+ // cheapest confirmation the management token is live (the same surface
10522
+ // client.ts polls, minus the heavy log query).
10523
+ validate({ credentials, options, fetchImpl }) {
10524
+ const cfg = options;
10525
+ const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
10526
+ return authProbe({
10527
+ provider: "supabase",
10528
+ accountKey: cfg.apiProjectRef ?? "validate",
10529
+ url: `${baseUrl}/v1/projects`,
10530
+ token: String(credentials.managementToken ?? ""),
10531
+ ...fetchImpl ? { fetchImpl } : {}
10532
+ });
10533
+ }
10534
+ },
10535
+ railway: {
10536
+ provider: "railway",
10537
+ primaryCredentialKey: "token",
10538
+ requiredCredentialFields: ["token"],
10539
+ requiredOptionFields: ["environmentId", "serviceId", "serviceNameById"],
10540
+ build(graph, options) {
10541
+ const config = options;
10542
+ return {
10543
+ connector: createRailwayConnector(graph, config),
10544
+ resolveTarget: createRailwayResolveTarget(config)
10545
+ };
10546
+ },
10547
+ // A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call
10548
+ // at the HTTP layer, so a 2xx confirms the token was accepted regardless of
10549
+ // the query's own shape (the connector's real queries are still
10550
+ // needs-endpoint-testing per railway/types.ts — auth is what we check).
10551
+ validate({ credentials, options, fetchImpl }) {
10552
+ const cfg = options;
10553
+ return authProbe({
10554
+ provider: "railway",
10555
+ accountKey: cfg.environmentId ?? "validate",
10556
+ url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,
10557
+ token: String(credentials.token ?? ""),
10558
+ init: {
10559
+ method: "POST",
10560
+ headers: { "Content-Type": "application/json" },
10561
+ body: JSON.stringify({ query: "{ __typename }" })
10562
+ },
10563
+ ...fetchImpl ? { fetchImpl } : {}
10564
+ });
10565
+ }
10566
+ },
10567
+ firebase: {
10568
+ provider: "firebase",
10569
+ // Firebase reads both projectId and accessToken from the credential; the
10570
+ // single-string form maps to the secret, and the required-fields check
10571
+ // below catches a projectId that was never supplied.
10572
+ primaryCredentialKey: "accessToken",
10573
+ requiredCredentialFields: ["projectId", "accessToken"],
10574
+ requiredOptionFields: [],
10575
+ build(graph, options) {
10576
+ return createFirebaseConnector(graph, options);
10577
+ },
10578
+ // GET the project's Cloud Logging log-name list (pageSize 1) — within the
10579
+ // same `roles/logging.viewer` grant the connector polls under, and the
10580
+ // lightest call that still fails 401/403 on a bad or wrong-scoped token.
10581
+ validate({ credentials, fetchImpl }) {
10582
+ const projectId = String(credentials.projectId ?? "");
10583
+ return authProbe({
10584
+ provider: "firebase",
10585
+ accountKey: projectId || "validate",
10586
+ url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,
10587
+ token: String(credentials.accessToken ?? ""),
10588
+ ...fetchImpl ? { fetchImpl } : {}
10589
+ });
10590
+ }
10591
+ },
10592
+ cloudflare: {
10593
+ provider: "cloudflare",
10594
+ primaryCredentialKey: "apiToken",
10595
+ requiredCredentialFields: ["apiToken"],
10596
+ // `workers` dropped as a required field (ADR-133) — the mapping is now
10597
+ // derived from the extracted graph's platform tag; an `options.workers`
10598
+ // entry still works as an explicit override
10599
+ // (CloudflareConnectorConfig.workers).
10600
+ requiredOptionFields: ["accountId"],
10601
+ build(graph, options) {
10602
+ const config = options;
10603
+ return {
10604
+ connector: new CloudflareConnector(config),
10605
+ resolveTarget: createCloudflareResolveTarget(config, graph)
10606
+ };
10607
+ },
10608
+ // GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
10609
+ // token live" endpoint. 200 on a valid token, 401 on an invalid one.
10610
+ validate({ credentials, options, fetchImpl }) {
10611
+ const cfg = options;
10612
+ const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
10613
+ return authProbe({
10614
+ provider: "cloudflare",
10615
+ accountKey: cfg.accountId ?? "validate",
10616
+ url: `${baseUrl}/user/tokens/verify`,
10617
+ token: String(credentials.apiToken ?? ""),
10618
+ ...fetchImpl ? { fetchImpl } : {}
10619
+ });
10620
+ }
10621
+ }
10622
+ };
10623
+ function resolveEntryCredentials(dispatch, entry2, env) {
10624
+ let credentials;
10625
+ try {
10626
+ const resolved = resolveCredential(entry2.credential, env);
10627
+ credentials = resolved.kind === "single" ? { [dispatch.primaryCredentialKey]: resolved.value } : { ...resolved.fields };
10628
+ } catch (err) {
10629
+ if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
10630
+ return { ok: false, kind: "error", reason: err.message };
10631
+ }
10632
+ const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
10633
+ if (missingCreds.length > 0) {
10634
+ return {
10635
+ ok: false,
10636
+ kind: "missing-field",
10637
+ reason: `credential missing required field(s): ${missingCreds.join(", ")}`
10638
+ };
10639
+ }
10640
+ return { ok: true, credentials };
10641
+ }
10642
+ function buildRegistration(entry2, graph, env = process.env) {
10643
+ const dispatch = PROVIDER_DISPATCH[entry2.provider];
10644
+ if (!dispatch) {
10645
+ return { ok: false, reason: `unknown provider "${entry2.provider}"` };
10646
+ }
10647
+ const creds = resolveEntryCredentials(dispatch, entry2, env);
10648
+ if (!creds.ok) return { ok: false, reason: creds.reason };
10649
+ const credentials = creds.credentials;
10650
+ const options = entry2.options ?? {};
10651
+ const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
10652
+ if (missingOpts.length > 0) {
10653
+ return {
10654
+ ok: false,
10655
+ reason: `options missing required field(s): ${missingOpts.join(", ")}`
10656
+ };
10657
+ }
10658
+ let built;
10659
+ try {
10660
+ built = dispatch.build(graph, options);
10661
+ } catch (err) {
10662
+ return { ok: false, reason: err.message };
10663
+ }
10664
+ const intervalMs = typeof options.intervalMs === "number" ? options.intervalMs : void 0;
10665
+ return {
10666
+ ok: true,
10667
+ registration: {
10668
+ connector: built.connector,
10669
+ credentials,
10670
+ resolveTarget: built.resolveTarget,
10671
+ ...intervalMs !== void 0 ? { intervalMs } : {}
10672
+ }
10673
+ };
10674
+ }
10675
+ async function loadConnectorRegistrations(input) {
10676
+ const { project, graph, home, env = process.env, onSkip } = input;
10677
+ let connectors;
10678
+ try {
10679
+ connectors = (await readConnectorsConfig(home)).connectors;
10680
+ } catch (err) {
10681
+ onSkip?.(
10682
+ { id: "(file)", provider: "(all)", credential: "" },
10683
+ `connectors.json unreadable \u2014 ${err.message}`
10684
+ );
10685
+ return [];
10686
+ }
10687
+ const registrations = [];
10688
+ for (const entry2 of connectors) {
10689
+ if (!connectorMatchesProject(entry2, project)) continue;
10690
+ const result = buildRegistration(entry2, graph, env);
10691
+ if (result.ok) registrations.push(result.registration);
10692
+ else onSkip?.(entry2, result.reason);
10693
+ }
10694
+ return registrations;
10695
+ }
10696
+
10697
+ // src/daemon.ts
10698
+ init_auth();
10699
+
10700
+ // src/unrouted.ts
10701
+ init_cjs_shims();
10702
+ var import_node_fs27 = require("fs");
10703
+ var import_node_path46 = __toESM(require("path"), 1);
10704
+ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
10705
+ return {
10706
+ timestamp: now.toISOString(),
10707
+ reason: "no-project-match",
10708
+ service_name: serviceName ?? null,
10709
+ traceId: traceId ?? null
10710
+ };
10711
+ }
10712
+ async function appendUnroutedSpan(neatHome4, record) {
10713
+ const target = import_node_path46.default.join(neatHome4, "errors.ndjson");
10714
+ await import_node_fs27.promises.mkdir(neatHome4, { recursive: true });
10715
+ await import_node_fs27.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
10716
+ }
10717
+ function unroutedErrorsPath(neatHome4) {
10718
+ return import_node_path46.default.join(neatHome4, "errors.ndjson");
10719
+ }
10720
+
10721
+ // src/daemon.ts
10722
+ var import_types42 = require("@neat.is/types");
10723
+ function daemonJsonPath(scanPath) {
10724
+ return import_node_path47.default.join(scanPath, "neat-out", "daemon.json");
10725
+ }
10726
+ function daemonsDiscoveryDir(home) {
10727
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
10728
+ return import_node_path47.default.join(base, "daemons");
10729
+ }
10730
+ function daemonDiscoveryPath(project, home) {
10731
+ return import_node_path47.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
10732
+ }
10733
+ function sanitizeDiscoveryName(project) {
10734
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
10735
+ }
10736
+ function neatHomeFromEnv() {
10737
+ const env = process.env.NEAT_HOME;
10738
+ if (env && env.length > 0) return import_node_path47.default.resolve(env);
10739
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10740
+ return import_node_path47.default.join(home, ".neat");
10741
+ }
10742
+ function resolveNeatVersion() {
10743
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
10744
+ return process.env.NEAT_LOCAL_VERSION;
10745
+ }
10746
+ try {
10747
+ const req2 = (0, import_node_module.createRequire)(importMetaUrl);
10748
+ const pkg = req2("../package.json");
10749
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
10750
+ } catch {
10751
+ return "0.0.0";
10752
+ }
10753
+ }
10754
+ async function writeDaemonRecord(record, home) {
10755
+ const body = JSON.stringify(record, null, 2) + "\n";
10756
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
10757
+ try {
10758
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
10759
+ } catch (err) {
10760
+ console.warn(
10761
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
10762
+ );
10763
+ }
10764
+ }
10765
+ async function clearDaemonRecord(record, home) {
10766
+ try {
10767
+ const stopped = { ...record, status: "stopped" };
10768
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
10769
+ } catch {
10770
+ }
10771
+ try {
10772
+ await import_node_fs28.promises.unlink(daemonDiscoveryPath(record.project, home));
10773
+ } catch {
10774
+ }
10775
+ }
10776
+ function reconcileDaemonRecordSync(record, home) {
10777
+ try {
10778
+ const stopped = { ...record, status: "stopped" };
10779
+ const target = daemonJsonPath(record.projectPath);
10780
+ const tmp = `${target}.${process.pid}.tmp`;
10781
+ (0, import_node_fs28.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
10782
+ (0, import_node_fs28.renameSync)(tmp, target);
10783
+ } catch {
10784
+ }
10785
+ try {
10786
+ (0, import_node_fs28.unlinkSync)(daemonDiscoveryPath(record.project, home));
10787
+ } catch {
10788
+ }
10789
+ }
10790
+ function teardownSlot(slot) {
10791
+ try {
10792
+ slot.stopPersist();
10793
+ } catch {
10794
+ }
10795
+ try {
10796
+ slot.stopStaleness();
10797
+ } catch {
10798
+ }
10799
+ try {
10800
+ slot.stopConnectors();
8846
10801
  } catch {
8847
10802
  }
8848
10803
  try {
@@ -8851,11 +10806,11 @@ function teardownSlot(slot) {
8851
10806
  }
8852
10807
  }
8853
10808
  function neatHomeFor(opts) {
8854
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path45.default.resolve(opts.neatHome);
10809
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path47.default.resolve(opts.neatHome);
8855
10810
  const env = process.env.NEAT_HOME;
8856
- if (env && env.length > 0) return import_node_path45.default.resolve(env);
10811
+ if (env && env.length > 0) return import_node_path47.default.resolve(env);
8857
10812
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8858
- return import_node_path45.default.join(home, ".neat");
10813
+ return import_node_path47.default.join(home, ".neat");
8859
10814
  }
8860
10815
  function routeSpanToProject(serviceName, projects) {
8861
10816
  if (!serviceName) return DEFAULT_PROJECT;
@@ -8899,13 +10854,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
8899
10854
  if (!serviceName) return true;
8900
10855
  if (serviceNameMatchesProject(serviceName, project)) return true;
8901
10856
  return graph.someNode(
8902
- (_id, attrs) => attrs.type === import_types30.NodeType.ServiceNode && attrs.name === serviceName
10857
+ (_id, attrs) => attrs.type === import_types42.NodeType.ServiceNode && attrs.name === serviceName
8903
10858
  );
8904
10859
  }
8905
- async function bootstrapProject(entry2, connectors = []) {
8906
- const paths = pathsForProject(entry2.name, import_node_path45.default.join(entry2.path, "neat-out"));
10860
+ async function bootstrapProject(entry2, connectors = [], neatHome4) {
10861
+ const paths = pathsForProject(entry2.name, import_node_path47.default.join(entry2.path, "neat-out"));
8907
10862
  try {
8908
- const stat = await import_node_fs26.promises.stat(entry2.path);
10863
+ const stat = await import_node_fs28.promises.stat(entry2.path);
8909
10864
  if (!stat.isDirectory()) {
8910
10865
  throw new Error(`registered path ${entry2.path} is not a directory`);
8911
10866
  }
@@ -8943,7 +10898,16 @@ async function bootstrapProject(entry2, connectors = []) {
8943
10898
  staleEventsPath: paths.staleEventsPath,
8944
10899
  project: entry2.name
8945
10900
  });
8946
- const stopFns = connectors.map(
10901
+ const fileConnectors = neatHome4 ? await loadConnectorRegistrations({
10902
+ project: entry2.name,
10903
+ graph,
10904
+ home: neatHome4,
10905
+ onSkip: (skipped, reason) => console.warn(
10906
+ `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry2.name}" \u2014 ${reason}`
10907
+ )
10908
+ }) : [];
10909
+ const allConnectors = [...connectors, ...fileConnectors];
10910
+ const stopFns = allConnectors.map(
8947
10911
  (registration) => startConnectorPollLoop(
8948
10912
  registration.connector,
8949
10913
  { projectDir: entry2.path, credentials: registration.credentials },
@@ -9021,7 +10985,7 @@ async function startDaemon(opts = {}) {
9021
10985
  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;
9022
10986
  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;
9023
10987
  const singleProject = projectArg;
9024
- const singleProjectPath = singleProject && projectPathArg ? import_node_path45.default.resolve(projectPathArg) : null;
10988
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path47.default.resolve(projectPathArg) : null;
9025
10989
  if (singleProject && !singleProjectPath) {
9026
10990
  throw new Error(
9027
10991
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -9029,14 +10993,14 @@ async function startDaemon(opts = {}) {
9029
10993
  }
9030
10994
  if (!singleProject) {
9031
10995
  try {
9032
- await import_node_fs26.promises.access(regPath);
10996
+ await import_node_fs28.promises.access(regPath);
9033
10997
  } catch {
9034
10998
  throw new Error(
9035
10999
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
9036
11000
  );
9037
11001
  }
9038
11002
  }
9039
- const pidPath = import_node_path45.default.join(home, "neatd.pid");
11003
+ const pidPath = import_node_path47.default.join(home, "neatd.pid");
9040
11004
  await writeAtomically(pidPath, `${process.pid}
9041
11005
  `);
9042
11006
  const slots = /* @__PURE__ */ new Map();
@@ -9080,7 +11044,7 @@ async function startDaemon(opts = {}) {
9080
11044
  }
9081
11045
  async function tryRecoverSlot(entry2) {
9082
11046
  try {
9083
- const fresh = await bootstrapProject(entry2, opts.connectors ?? []);
11047
+ const fresh = await bootstrapProject(entry2, opts.connectors ?? [], home);
9084
11048
  const prior = slots.get(entry2.name);
9085
11049
  if (prior) teardownSlot(prior);
9086
11050
  slots.set(entry2.name, fresh);
@@ -9104,7 +11068,7 @@ async function startDaemon(opts = {}) {
9104
11068
  bootstrapStatus.set(entry2.name, "bootstrapping");
9105
11069
  bootstrapStartedAt.set(entry2.name, Date.now());
9106
11070
  try {
9107
- const slot = await bootstrapProject(entry2, opts.connectors ?? []);
11071
+ const slot = await bootstrapProject(entry2, opts.connectors ?? [], home);
9108
11072
  const prior = slots.get(entry2.name);
9109
11073
  if (prior) teardownSlot(prior);
9110
11074
  slots.set(entry2.name, slot);
@@ -9230,7 +11194,7 @@ async function startDaemon(opts = {}) {
9230
11194
  }
9231
11195
  if (restApp) await restApp.close().catch(() => {
9232
11196
  });
9233
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11197
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9234
11198
  });
9235
11199
  throw new Error(
9236
11200
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -9356,7 +11320,7 @@ async function startDaemon(opts = {}) {
9356
11320
  });
9357
11321
  if (otlpApp) await otlpApp.close().catch(() => {
9358
11322
  });
9359
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11323
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9360
11324
  });
9361
11325
  throw new Error(
9362
11326
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -9391,7 +11355,7 @@ async function startDaemon(opts = {}) {
9391
11355
  });
9392
11356
  if (otlpApp) await otlpApp.close().catch(() => {
9393
11357
  });
9394
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11358
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9395
11359
  });
9396
11360
  throw new Error(
9397
11361
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -9438,9 +11402,9 @@ async function startDaemon(opts = {}) {
9438
11402
  let registryWatcher = null;
9439
11403
  let reloadTimer = null;
9440
11404
  if (!singleProject) try {
9441
- const regDir = import_node_path45.default.dirname(regPath);
9442
- const regBase = import_node_path45.default.basename(regPath);
9443
- registryWatcher = (0, import_node_fs26.watch)(regDir, (_eventType, filename) => {
11405
+ const regDir = import_node_path47.default.dirname(regPath);
11406
+ const regBase = import_node_path47.default.basename(regPath);
11407
+ registryWatcher = (0, import_node_fs28.watch)(regDir, (_eventType, filename) => {
9444
11408
  if (filename !== null && filename !== regBase) return;
9445
11409
  if (reloadTimer) clearTimeout(reloadTimer);
9446
11410
  reloadTimer = setTimeout(() => {
@@ -9489,7 +11453,7 @@ async function startDaemon(opts = {}) {
9489
11453
  if (daemonRecord) {
9490
11454
  await clearDaemonRecord(daemonRecord, home);
9491
11455
  }
9492
- await import_node_fs26.promises.unlink(pidPath).catch(() => {
11456
+ await import_node_fs28.promises.unlink(pidPath).catch(() => {
9493
11457
  });
9494
11458
  };
9495
11459
  return {
@@ -9512,9 +11476,9 @@ init_auth();
9512
11476
  // src/web-spawn.ts
9513
11477
  init_cjs_shims();
9514
11478
  var import_node_child_process2 = require("child_process");
9515
- var import_node_fs27 = require("fs");
11479
+ var import_node_fs29 = require("fs");
9516
11480
  var import_node_net = __toESM(require("net"), 1);
9517
- var import_node_path46 = __toESM(require("path"), 1);
11481
+ var import_node_path48 = __toESM(require("path"), 1);
9518
11482
  var DEFAULT_WEB_PORT = 6328;
9519
11483
  var DEFAULT_REST_PORT = 8080;
9520
11484
  function asValidPort(value) {
@@ -9523,11 +11487,11 @@ function asValidPort(value) {
9523
11487
  }
9524
11488
  function projectRoot() {
9525
11489
  const fromEnv = process.env.NEAT_SCAN_PATH;
9526
- return import_node_path46.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
11490
+ return import_node_path48.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
9527
11491
  }
9528
11492
  async function readDaemonPorts(root) {
9529
11493
  try {
9530
- const raw = await import_node_fs27.promises.readFile(import_node_path46.default.join(root, "neat-out", "daemon.json"), "utf8");
11494
+ const raw = await import_node_fs29.promises.readFile(import_node_path48.default.join(root, "neat-out", "daemon.json"), "utf8");
9531
11495
  const parsed = JSON.parse(raw);
9532
11496
  const ports = parsed?.ports ?? {};
9533
11497
  return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
@@ -9572,10 +11536,10 @@ function resolveWebPackageDir() {
9572
11536
  eval("require")
9573
11537
  );
9574
11538
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
9575
- return import_node_path46.default.dirname(pkgJsonPath);
11539
+ return import_node_path48.default.dirname(pkgJsonPath);
9576
11540
  }
9577
11541
  function resolveStandaloneServerEntry(webDir) {
9578
- return import_node_path46.default.join(webDir, ".next/standalone/packages/web/server.js");
11542
+ return import_node_path48.default.join(webDir, ".next/standalone/packages/web/server.js");
9579
11543
  }
9580
11544
  async function pickInternalPort() {
9581
11545
  return new Promise((resolve, reject) => {
@@ -9628,7 +11592,7 @@ async function spawnWebUI(restPort, opts = {}) {
9628
11592
  NEAT_API_URL: apiUrl
9629
11593
  };
9630
11594
  child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
9631
- cwd: import_node_path46.default.dirname(serverEntry),
11595
+ cwd: import_node_path48.default.dirname(serverEntry),
9632
11596
  env,
9633
11597
  stdio: ["ignore", "inherit", "inherit"],
9634
11598
  detached: false
@@ -9802,16 +11766,16 @@ function localVersion() {
9802
11766
  return "0.0.0";
9803
11767
  }
9804
11768
  }
9805
- function neatHome2() {
11769
+ function neatHome3() {
9806
11770
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
9807
- return import_node_path47.default.resolve(process.env.NEAT_HOME);
11771
+ return import_node_path49.default.resolve(process.env.NEAT_HOME);
9808
11772
  }
9809
11773
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
9810
- return import_node_path47.default.join(home, ".neat");
11774
+ return import_node_path49.default.join(home, ".neat");
9811
11775
  }
9812
11776
  async function readPid() {
9813
11777
  try {
9814
- const raw = await import_node_fs28.promises.readFile(import_node_path47.default.join(neatHome2(), "neatd.pid"), "utf8");
11778
+ const raw = await import_node_fs30.promises.readFile(import_node_path49.default.join(neatHome3(), "neatd.pid"), "utf8");
9815
11779
  const n = Number.parseInt(raw.trim(), 10);
9816
11780
  return Number.isFinite(n) ? n : null;
9817
11781
  } catch {