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