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