@neat.is/core 0.4.27 → 0.4.28-dev.20260708
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-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-NY5Q6NE2.js +2566 -0
- package/dist/chunk-NY5Q6NE2.js.map +1 -0
- package/dist/{chunk-QM6BMPVJ.js → chunk-O4MRDWD7.js} +125 -12
- package/dist/chunk-O4MRDWD7.js.map +1 -0
- package/dist/cli.cjs +2454 -378
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +358 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1744 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +1760 -116
- 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 +133 -18
- 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 path49 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
65
|
+
if (exactUnauthPaths.has(path49) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path49)) {
|
|
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
|
|
|
@@ -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 path49 = q === -1 ? v : v.slice(0, q);
|
|
366
|
+
if (path49.length > 0) return path49;
|
|
363
367
|
}
|
|
364
368
|
}
|
|
365
369
|
return void 0;
|
|
@@ -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_fs29 = require("fs");
|
|
680
|
+
var import_node_path48 = __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_fs27 = require("fs");
|
|
686
|
+
var import_node_path46 = __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, path49, edges) {
|
|
1217
|
+
if (path49.length > best.path.length) {
|
|
1218
|
+
best = { path: [...path49], edges: [...edges] };
|
|
1215
1219
|
}
|
|
1216
|
-
if (
|
|
1220
|
+
if (path49.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
|
+
path49.push(srcId);
|
|
1222
1226
|
edges.push(edge);
|
|
1223
|
-
step(srcId,
|
|
1224
|
-
|
|
1227
|
+
step(srcId, path49, edges);
|
|
1228
|
+
path49.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 path49 = [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
|
+
path49.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: path49, 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 path49 = [...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
|
+
path49.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: path49,
|
|
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: path49,
|
|
1465
1469
|
edgeProvenances,
|
|
1466
1470
|
confidence,
|
|
1467
1471
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -3017,29 +3021,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
3017
3021
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
3018
3022
|
});
|
|
3019
3023
|
let promoted = 0;
|
|
3020
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
3024
|
+
for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
|
|
3021
3025
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
3022
3026
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
3023
3027
|
if (!gate.allowed) {
|
|
3024
3028
|
continue;
|
|
3025
3029
|
}
|
|
3026
3030
|
}
|
|
3027
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
3031
|
+
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
3028
3032
|
graph.dropNode(frontierId2);
|
|
3029
3033
|
promoted++;
|
|
3030
3034
|
}
|
|
3031
3035
|
return promoted;
|
|
3032
3036
|
}
|
|
3033
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
3037
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId5) {
|
|
3034
3038
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
3035
3039
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
3036
3040
|
for (const edgeId of inbound) {
|
|
3037
3041
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3038
|
-
rebuildEdge(graph, edge, edge.source,
|
|
3042
|
+
rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
|
|
3039
3043
|
}
|
|
3040
3044
|
for (const edgeId of outbound) {
|
|
3041
3045
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3042
|
-
rebuildEdge(graph, edge,
|
|
3046
|
+
rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
|
|
3043
3047
|
}
|
|
3044
3048
|
}
|
|
3045
3049
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -3187,24 +3191,58 @@ function dedupeIncidents(events) {
|
|
|
3187
3191
|
return !hasRealFailure.has(groupKey(ev));
|
|
3188
3192
|
});
|
|
3189
3193
|
}
|
|
3194
|
+
var SnapshotValidationError = class extends Error {
|
|
3195
|
+
constructor(issues) {
|
|
3196
|
+
super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
|
|
3197
|
+
this.issues = issues;
|
|
3198
|
+
this.name = "SnapshotValidationError";
|
|
3199
|
+
}
|
|
3200
|
+
issues;
|
|
3201
|
+
};
|
|
3202
|
+
function describeZodIssues(error) {
|
|
3203
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
3204
|
+
}
|
|
3190
3205
|
function mergeSnapshot(graph, snapshot) {
|
|
3191
3206
|
const exported = snapshot.graph;
|
|
3207
|
+
const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
|
|
3208
|
+
const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
|
|
3209
|
+
const issues = [];
|
|
3210
|
+
const validNodes = [];
|
|
3211
|
+
const validEdges = [];
|
|
3212
|
+
for (const node of incomingNodes) {
|
|
3213
|
+
if (node.attributes === void 0) continue;
|
|
3214
|
+
const parsed = import_types3.GraphNodeSchema.safeParse(node.attributes);
|
|
3215
|
+
if (!parsed.success) {
|
|
3216
|
+
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
validNodes.push({ key: node.key, attributes: parsed.data });
|
|
3220
|
+
}
|
|
3221
|
+
for (const edge of incomingEdges) {
|
|
3222
|
+
if (edge.attributes === void 0) continue;
|
|
3223
|
+
const parsed = import_types3.GraphEdgeSchema.safeParse(edge.attributes);
|
|
3224
|
+
if (!parsed.success) {
|
|
3225
|
+
const label = edge.key ?? `${edge.source}->${edge.target}`;
|
|
3226
|
+
issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
const id = edge.key ?? parsed.data.id;
|
|
3230
|
+
validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
|
|
3231
|
+
}
|
|
3232
|
+
if (issues.length > 0) {
|
|
3233
|
+
throw new SnapshotValidationError(issues);
|
|
3234
|
+
}
|
|
3192
3235
|
let nodesAdded = 0;
|
|
3193
3236
|
let edgesAdded = 0;
|
|
3194
|
-
for (const node of
|
|
3237
|
+
for (const node of validNodes) {
|
|
3195
3238
|
if (graph.hasNode(node.key)) continue;
|
|
3196
|
-
if (!node.attributes) continue;
|
|
3197
3239
|
graph.addNode(node.key, node.attributes);
|
|
3198
3240
|
nodesAdded++;
|
|
3199
3241
|
}
|
|
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;
|
|
3242
|
+
for (const edge of validEdges) {
|
|
3243
|
+
if (graph.hasEdge(edge.key)) continue;
|
|
3206
3244
|
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
3207
|
-
graph.addEdgeWithKey(
|
|
3245
|
+
graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
|
|
3208
3246
|
edgesAdded++;
|
|
3209
3247
|
}
|
|
3210
3248
|
return { nodesAdded, edgesAdded };
|
|
@@ -3798,9 +3836,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
3798
3836
|
"StatefulSet",
|
|
3799
3837
|
"DaemonSet"
|
|
3800
3838
|
]);
|
|
3801
|
-
function addAliases(graph,
|
|
3802
|
-
if (!graph.hasNode(
|
|
3803
|
-
const node = graph.getNodeAttributes(
|
|
3839
|
+
function addAliases(graph, serviceId5, candidates) {
|
|
3840
|
+
if (!graph.hasNode(serviceId5)) return;
|
|
3841
|
+
const node = graph.getNodeAttributes(serviceId5);
|
|
3804
3842
|
if (node.type !== import_types6.NodeType.ServiceNode) return;
|
|
3805
3843
|
const set = new Set(node.aliases ?? []);
|
|
3806
3844
|
for (const c of candidates) {
|
|
@@ -3810,7 +3848,7 @@ function addAliases(graph, serviceId4, candidates) {
|
|
|
3810
3848
|
}
|
|
3811
3849
|
if (set.size === 0) return;
|
|
3812
3850
|
const updated = { ...node, aliases: [...set].sort() };
|
|
3813
|
-
graph.replaceNodeAttributes(
|
|
3851
|
+
graph.replaceNodeAttributes(serviceId5, updated);
|
|
3814
3852
|
}
|
|
3815
3853
|
function indexServicesByName(services) {
|
|
3816
3854
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3843,12 +3881,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
3843
3881
|
}
|
|
3844
3882
|
if (!compose?.services) return;
|
|
3845
3883
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3846
|
-
const
|
|
3847
|
-
if (!
|
|
3884
|
+
const serviceId5 = serviceIndex.get(composeName);
|
|
3885
|
+
if (!serviceId5) continue;
|
|
3848
3886
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
3849
3887
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
3850
3888
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
3851
|
-
addAliases(graph,
|
|
3889
|
+
addAliases(graph, serviceId5, aliases);
|
|
3852
3890
|
}
|
|
3853
3891
|
}
|
|
3854
3892
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -7364,13 +7402,13 @@ function bucketKey(source, target, type) {
|
|
|
7364
7402
|
return `${type}|${source}|${target}`;
|
|
7365
7403
|
}
|
|
7366
7404
|
function bucketEdges(graph) {
|
|
7367
|
-
const
|
|
7405
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
7368
7406
|
graph.forEachEdge((id, attrs) => {
|
|
7369
7407
|
const e = attrs;
|
|
7370
7408
|
const parsed = (0, import_types27.parseEdgeId)(id);
|
|
7371
7409
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
7372
7410
|
const key = bucketKey(e.source, e.target, e.type);
|
|
7373
|
-
const cur =
|
|
7411
|
+
const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
7374
7412
|
switch (provenance) {
|
|
7375
7413
|
case import_types27.Provenance.EXTRACTED:
|
|
7376
7414
|
cur.extracted = e;
|
|
@@ -7384,9 +7422,9 @@ function bucketEdges(graph) {
|
|
|
7384
7422
|
default:
|
|
7385
7423
|
if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
|
|
7386
7424
|
}
|
|
7387
|
-
|
|
7425
|
+
buckets2.set(key, cur);
|
|
7388
7426
|
});
|
|
7389
|
-
return
|
|
7427
|
+
return buckets2;
|
|
7390
7428
|
}
|
|
7391
7429
|
function nodeIsFrontier(graph, nodeId) {
|
|
7392
7430
|
if (!graph.hasNode(nodeId)) return false;
|
|
@@ -7577,8 +7615,8 @@ function suppressHostMismatchHalves(all) {
|
|
|
7577
7615
|
}
|
|
7578
7616
|
function computeDivergences(graph, opts = {}) {
|
|
7579
7617
|
const all = [];
|
|
7580
|
-
const
|
|
7581
|
-
for (const bucket of
|
|
7618
|
+
const buckets2 = bucketEdges(graph);
|
|
7619
|
+
for (const bucket of buckets2.values()) {
|
|
7582
7620
|
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
7583
7621
|
}
|
|
7584
7622
|
graph.forEachNode((nodeId, attrs) => {
|
|
@@ -7624,6 +7662,51 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7624
7662
|
});
|
|
7625
7663
|
}
|
|
7626
7664
|
|
|
7665
|
+
// src/logs-store.ts
|
|
7666
|
+
init_cjs_shims();
|
|
7667
|
+
var LOGS_STORE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7668
|
+
var LOGS_QUERY_DEFAULT_LIMIT = 100;
|
|
7669
|
+
var LOGS_QUERY_MAX_LIMIT = 1e3;
|
|
7670
|
+
var buffers = /* @__PURE__ */ new Map();
|
|
7671
|
+
var KEY_SEP = "::";
|
|
7672
|
+
function bufferKey(projectName, source) {
|
|
7673
|
+
return `${projectName}${KEY_SEP}${source}`;
|
|
7674
|
+
}
|
|
7675
|
+
function sourcesForProject(projectName) {
|
|
7676
|
+
const prefix = `${projectName}${KEY_SEP}`;
|
|
7677
|
+
const sources = [];
|
|
7678
|
+
for (const key of buffers.keys()) {
|
|
7679
|
+
if (key.startsWith(prefix)) sources.push(key.slice(prefix.length));
|
|
7680
|
+
}
|
|
7681
|
+
return sources;
|
|
7682
|
+
}
|
|
7683
|
+
function queryLogEntries(opts) {
|
|
7684
|
+
const { projectName, source, service, since, limit } = opts;
|
|
7685
|
+
const sources = source && source.length > 0 ? source : sourcesForProject(projectName);
|
|
7686
|
+
let merged = [];
|
|
7687
|
+
for (const src of sources) {
|
|
7688
|
+
const buf = buffers.get(bufferKey(projectName, src));
|
|
7689
|
+
if (buf) merged = merged.concat(buf);
|
|
7690
|
+
}
|
|
7691
|
+
if (service) {
|
|
7692
|
+
merged = merged.filter((e) => e.serviceName === service);
|
|
7693
|
+
}
|
|
7694
|
+
if (since) {
|
|
7695
|
+
const sinceMs = Date.parse(since);
|
|
7696
|
+
if (!Number.isNaN(sinceMs)) {
|
|
7697
|
+
merged = merged.filter((e) => {
|
|
7698
|
+
const t = Date.parse(e.timestamp);
|
|
7699
|
+
return Number.isNaN(t) || t >= sinceMs;
|
|
7700
|
+
});
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
7704
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
|
|
7705
|
+
return merged.slice(0, limit);
|
|
7706
|
+
}
|
|
7707
|
+
return merged;
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7627
7710
|
// src/diff.ts
|
|
7628
7711
|
init_cjs_shims();
|
|
7629
7712
|
var import_node_fs23 = require("fs");
|
|
@@ -8193,6 +8276,23 @@ function registerRoutes(scope, ctx) {
|
|
|
8193
8276
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
8194
8277
|
return { count: sliced.length, total, events: sliced };
|
|
8195
8278
|
});
|
|
8279
|
+
scope.get("/logs", async (req2, reply) => {
|
|
8280
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
8281
|
+
if (!proj) return;
|
|
8282
|
+
const rawSource = req2.query.source;
|
|
8283
|
+
const sources = rawSource === void 0 ? void 0 : Array.isArray(rawSource) ? rawSource : [rawSource];
|
|
8284
|
+
const filtered = queryLogEntries({
|
|
8285
|
+
projectName: proj.name,
|
|
8286
|
+
...sources && sources.length > 0 ? { source: sources } : {},
|
|
8287
|
+
...req2.query.service ? { service: req2.query.service } : {},
|
|
8288
|
+
...req2.query.since ? { since: req2.query.since } : {}
|
|
8289
|
+
});
|
|
8290
|
+
const total = filtered.length;
|
|
8291
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8292
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, LOGS_QUERY_MAX_LIMIT) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8293
|
+
const sliced = filtered.slice(0, safeLimit);
|
|
8294
|
+
return { count: sliced.length, total, logs: sliced };
|
|
8295
|
+
});
|
|
8196
8296
|
const incidentHistoryHandler = async (req2, reply) => {
|
|
8197
8297
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
8198
8298
|
if (!proj) return;
|
|
@@ -8288,8 +8388,16 @@ function registerRoutes(scope, ctx) {
|
|
|
8288
8388
|
if (!against) {
|
|
8289
8389
|
return reply.code(400).send({ error: "query parameter `against` is required" });
|
|
8290
8390
|
}
|
|
8391
|
+
const targetProjectName = against === "self" ? proj.name : against;
|
|
8392
|
+
const targetProject = registry.get(targetProjectName);
|
|
8393
|
+
if (!targetProject) {
|
|
8394
|
+
return reply.code(400).send({
|
|
8395
|
+
error: "unknown snapshot id \u2014 `against` must be `self` or the name of a project this server has a snapshot for",
|
|
8396
|
+
against
|
|
8397
|
+
});
|
|
8398
|
+
}
|
|
8291
8399
|
try {
|
|
8292
|
-
const snapshot = await loadSnapshotForDiff(
|
|
8400
|
+
const snapshot = await loadSnapshotForDiff(targetProject.paths.snapshotPath);
|
|
8293
8401
|
return computeGraphDiff(proj.graph, snapshot);
|
|
8294
8402
|
} catch (err) {
|
|
8295
8403
|
return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
|
|
@@ -8319,6 +8427,13 @@ function registerRoutes(scope, ctx) {
|
|
|
8319
8427
|
edgeCount: proj.graph.size
|
|
8320
8428
|
};
|
|
8321
8429
|
} catch (err) {
|
|
8430
|
+
if (err instanceof SnapshotValidationError) {
|
|
8431
|
+
return reply.code(400).send({
|
|
8432
|
+
error: "snapshot merge failed",
|
|
8433
|
+
details: err.message,
|
|
8434
|
+
issues: err.issues
|
|
8435
|
+
});
|
|
8436
|
+
}
|
|
8322
8437
|
return reply.code(400).send({
|
|
8323
8438
|
error: "snapshot merge failed",
|
|
8324
8439
|
details: err.message
|
|
@@ -8739,13 +8854,1533 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
|
|
|
8739
8854
|
};
|
|
8740
8855
|
}
|
|
8741
8856
|
|
|
8857
|
+
// src/connectors/registry.ts
|
|
8858
|
+
init_cjs_shims();
|
|
8859
|
+
|
|
8860
|
+
// src/connectors/junction.ts
|
|
8861
|
+
init_cjs_shims();
|
|
8862
|
+
var buckets = /* @__PURE__ */ new Map();
|
|
8863
|
+
function bucketMapKey(provider, accountKey) {
|
|
8864
|
+
return `${provider}\0${accountKey}`;
|
|
8865
|
+
}
|
|
8866
|
+
function getBucket(provider, accountKey, config) {
|
|
8867
|
+
const key = bucketMapKey(provider, accountKey);
|
|
8868
|
+
const existing = buckets.get(key);
|
|
8869
|
+
if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {
|
|
8870
|
+
return existing;
|
|
8871
|
+
}
|
|
8872
|
+
const fresh = { ...config, tokens: config.capacity, updatedAt: Date.now() };
|
|
8873
|
+
buckets.set(key, fresh);
|
|
8874
|
+
return fresh;
|
|
8875
|
+
}
|
|
8876
|
+
function refillBucket(bucket, now) {
|
|
8877
|
+
if (now <= bucket.updatedAt) return;
|
|
8878
|
+
const elapsed = now - bucket.updatedAt;
|
|
8879
|
+
const grant = elapsed / bucket.refillMs;
|
|
8880
|
+
if (grant <= 0) return;
|
|
8881
|
+
bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant);
|
|
8882
|
+
bucket.updatedAt = now;
|
|
8883
|
+
}
|
|
8884
|
+
var RateLimitExceededError = class extends Error {
|
|
8885
|
+
constructor(provider, accountKey) {
|
|
8886
|
+
super(
|
|
8887
|
+
`junction: rate limit exceeded for ${provider}:${accountKey} \u2014 waiting for the next token would exceed this call's wall-clock budget`
|
|
8888
|
+
);
|
|
8889
|
+
this.name = "RateLimitExceededError";
|
|
8890
|
+
}
|
|
8891
|
+
};
|
|
8892
|
+
function delay(ms) {
|
|
8893
|
+
if (ms <= 0) return Promise.resolve();
|
|
8894
|
+
return new Promise((resolve) => {
|
|
8895
|
+
const timer = setTimeout(resolve, ms);
|
|
8896
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
8897
|
+
});
|
|
8898
|
+
}
|
|
8899
|
+
async function acquireToken(provider, accountKey, config, remainingBudgetMs) {
|
|
8900
|
+
const bucket = getBucket(provider, accountKey, config);
|
|
8901
|
+
refillBucket(bucket, Date.now());
|
|
8902
|
+
if (bucket.tokens >= 1) {
|
|
8903
|
+
bucket.tokens -= 1;
|
|
8904
|
+
return 0;
|
|
8905
|
+
}
|
|
8906
|
+
const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs);
|
|
8907
|
+
if (waitMs > remainingBudgetMs) {
|
|
8908
|
+
throw new RateLimitExceededError(provider, accountKey);
|
|
8909
|
+
}
|
|
8910
|
+
await delay(waitMs);
|
|
8911
|
+
refillBucket(bucket, Date.now());
|
|
8912
|
+
bucket.tokens = Math.max(0, bucket.tokens - 1);
|
|
8913
|
+
return waitMs;
|
|
8914
|
+
}
|
|
8915
|
+
var JUNCTION_DEFAULT_RATE_LIMITS = {
|
|
8916
|
+
// ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of
|
|
8917
|
+
// that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)
|
|
8918
|
+
// stays well clear of the documented limit even under sustained polling.
|
|
8919
|
+
cloudflare: { capacity: 100, refillMs: 3e3 },
|
|
8920
|
+
// Placeholder pending a live project confirming the real cap
|
|
8921
|
+
// (docs/connectors/railway.md: "does not appear to publish one as of this
|
|
8922
|
+
// writing").
|
|
8923
|
+
railway: { capacity: 30, refillMs: 1e4 },
|
|
8924
|
+
// Placeholder pending a live rate-limit check (docs/connectors/
|
|
8925
|
+
// firebase.md: "needs-endpoint-testing against entries.list's live rate
|
|
8926
|
+
// limits").
|
|
8927
|
+
firebase: { capacity: 30, refillMs: 1e4 },
|
|
8928
|
+
// Placeholder pending a live rate-limit check (docs/connectors/supabase.md:
|
|
8929
|
+
// "the documented rate limit for this specific endpoint is unconfirmed").
|
|
8930
|
+
supabase: { capacity: 30, refillMs: 1e4 },
|
|
8931
|
+
// Not a documented API limit at all — a self-imposed ceiling on the raw
|
|
8932
|
+
// pg_stat_statements connection (see module header above).
|
|
8933
|
+
"supabase-postgres": { capacity: 20, refillMs: 3e3 }
|
|
8934
|
+
};
|
|
8935
|
+
var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
|
|
8936
|
+
function defaultRateLimitFor(provider) {
|
|
8937
|
+
return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT;
|
|
8938
|
+
}
|
|
8939
|
+
var JUNCTION_DEFAULT_TIMEOUT_MS = 1e4;
|
|
8940
|
+
var JUNCTION_DEFAULT_MAX_ATTEMPTS = 3;
|
|
8941
|
+
var JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200;
|
|
8942
|
+
var JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4;
|
|
8943
|
+
var JUNCTION_DEFAULT_MAX_ELAPSED_MS = 3e4;
|
|
8944
|
+
var JUNCTION_DEFAULT_DB_TIMEOUT_MS = 1e4;
|
|
8945
|
+
async function backoff(attempt, initialBackoffMs, backoffMultiplier, remainingBudgetMs) {
|
|
8946
|
+
const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1);
|
|
8947
|
+
const capped = Math.max(0, Math.min(raw, remainingBudgetMs));
|
|
8948
|
+
await delay(capped);
|
|
8949
|
+
}
|
|
8950
|
+
function safeUrlLabel(url) {
|
|
8951
|
+
try {
|
|
8952
|
+
const u = typeof url === "string" ? new URL(url) : url;
|
|
8953
|
+
return `${u.origin}${u.pathname}`;
|
|
8954
|
+
} catch {
|
|
8955
|
+
return String(url);
|
|
8956
|
+
}
|
|
8957
|
+
}
|
|
8958
|
+
function logOutcome(provider, accountKey, outcome, method, label, attempt, startedAt) {
|
|
8959
|
+
const elapsedMs = Date.now() - startedAt;
|
|
8960
|
+
const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} \u2014 ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`;
|
|
8961
|
+
if (outcome === "success" || outcome === "retried-then-succeeded") {
|
|
8962
|
+
console.log(line);
|
|
8963
|
+
} else if (outcome === "rate-limited") {
|
|
8964
|
+
console.warn(line);
|
|
8965
|
+
} else {
|
|
8966
|
+
console.error(line);
|
|
8967
|
+
}
|
|
8968
|
+
}
|
|
8969
|
+
function bearerAuthHeader(token) {
|
|
8970
|
+
return { Authorization: `Bearer ${token}` };
|
|
8971
|
+
}
|
|
8972
|
+
async function junctionFetch(url, init = {}, policy) {
|
|
8973
|
+
const {
|
|
8974
|
+
provider,
|
|
8975
|
+
accountKey,
|
|
8976
|
+
timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,
|
|
8977
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
8978
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
8979
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
8980
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
8981
|
+
rateLimit = defaultRateLimitFor(provider),
|
|
8982
|
+
fetchImpl = fetch
|
|
8983
|
+
} = policy;
|
|
8984
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
8985
|
+
const label = safeUrlLabel(url);
|
|
8986
|
+
const startedAt = Date.now();
|
|
8987
|
+
let attempt = 0;
|
|
8988
|
+
let sawRetry = false;
|
|
8989
|
+
for (; ; ) {
|
|
8990
|
+
attempt++;
|
|
8991
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
8992
|
+
if (remainingBudget <= 0) {
|
|
8993
|
+
logOutcome(provider, accountKey, "retried-then-failed", method, label, attempt - 1, startedAt);
|
|
8994
|
+
throw new Error(
|
|
8995
|
+
`junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
8996
|
+
);
|
|
8997
|
+
}
|
|
8998
|
+
try {
|
|
8999
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9000
|
+
} catch (err) {
|
|
9001
|
+
if (err instanceof RateLimitExceededError) {
|
|
9002
|
+
logOutcome(provider, accountKey, "rate-limited", method, label, attempt, startedAt);
|
|
9003
|
+
}
|
|
9004
|
+
throw err;
|
|
9005
|
+
}
|
|
9006
|
+
const controller = new AbortController();
|
|
9007
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9008
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9009
|
+
try {
|
|
9010
|
+
const res = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
9011
|
+
clearTimeout(timer);
|
|
9012
|
+
if (res.ok || res.status < 500) {
|
|
9013
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", method, label, attempt, startedAt);
|
|
9014
|
+
return res;
|
|
9015
|
+
}
|
|
9016
|
+
if (attempt >= maxAttempts) {
|
|
9017
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9018
|
+
return res;
|
|
9019
|
+
}
|
|
9020
|
+
sawRetry = true;
|
|
9021
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9022
|
+
} catch (err) {
|
|
9023
|
+
clearTimeout(timer);
|
|
9024
|
+
if (attempt >= maxAttempts) {
|
|
9025
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9026
|
+
throw err;
|
|
9027
|
+
}
|
|
9028
|
+
sawRetry = true;
|
|
9029
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
}
|
|
9033
|
+
var DbJunctionTimeoutError = class extends Error {
|
|
9034
|
+
constructor(ms) {
|
|
9035
|
+
super(`junction: db query exceeded its ${ms}ms timeout`);
|
|
9036
|
+
this.name = "DbJunctionTimeoutError";
|
|
9037
|
+
}
|
|
9038
|
+
};
|
|
9039
|
+
function withTimeout(run, timeoutMs) {
|
|
9040
|
+
return new Promise((resolve, reject) => {
|
|
9041
|
+
const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs);
|
|
9042
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9043
|
+
run().then(
|
|
9044
|
+
(value) => {
|
|
9045
|
+
clearTimeout(timer);
|
|
9046
|
+
resolve(value);
|
|
9047
|
+
},
|
|
9048
|
+
(err) => {
|
|
9049
|
+
clearTimeout(timer);
|
|
9050
|
+
reject(err);
|
|
9051
|
+
}
|
|
9052
|
+
);
|
|
9053
|
+
});
|
|
9054
|
+
}
|
|
9055
|
+
var RETRYABLE_NODE_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN", "EPIPE"]);
|
|
9056
|
+
function isRetryableDbError(err) {
|
|
9057
|
+
if (err instanceof DbJunctionTimeoutError) return true;
|
|
9058
|
+
const code = err?.code;
|
|
9059
|
+
if (typeof code !== "string") return false;
|
|
9060
|
+
if (code.startsWith("08") || code === "57P03") return true;
|
|
9061
|
+
return RETRYABLE_NODE_ERROR_CODES.has(code);
|
|
9062
|
+
}
|
|
9063
|
+
async function dbJunction(run, policy) {
|
|
9064
|
+
const {
|
|
9065
|
+
provider,
|
|
9066
|
+
accountKey,
|
|
9067
|
+
timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,
|
|
9068
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
9069
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
9070
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
9071
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
9072
|
+
rateLimit = defaultRateLimitFor(provider)
|
|
9073
|
+
} = policy;
|
|
9074
|
+
const startedAt = Date.now();
|
|
9075
|
+
let attempt = 0;
|
|
9076
|
+
let sawRetry = false;
|
|
9077
|
+
for (; ; ) {
|
|
9078
|
+
attempt++;
|
|
9079
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
9080
|
+
if (remainingBudget <= 0) {
|
|
9081
|
+
logOutcome(provider, accountKey, "retried-then-failed", "QUERY", "db", attempt - 1, startedAt);
|
|
9082
|
+
throw new Error(
|
|
9083
|
+
`junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
9084
|
+
);
|
|
9085
|
+
}
|
|
9086
|
+
try {
|
|
9087
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9088
|
+
} catch (err) {
|
|
9089
|
+
if (err instanceof RateLimitExceededError) {
|
|
9090
|
+
logOutcome(provider, accountKey, "rate-limited", "QUERY", "db", attempt, startedAt);
|
|
9091
|
+
}
|
|
9092
|
+
throw err;
|
|
9093
|
+
}
|
|
9094
|
+
try {
|
|
9095
|
+
const result = await withTimeout(run, timeoutMs);
|
|
9096
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", "QUERY", "db", attempt, startedAt);
|
|
9097
|
+
return result;
|
|
9098
|
+
} catch (err) {
|
|
9099
|
+
if (!isRetryableDbError(err) || attempt >= maxAttempts) {
|
|
9100
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", "QUERY", "db", attempt, startedAt);
|
|
9101
|
+
throw err;
|
|
9102
|
+
}
|
|
9103
|
+
sawRetry = true;
|
|
9104
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9105
|
+
}
|
|
9106
|
+
}
|
|
9107
|
+
}
|
|
9108
|
+
|
|
9109
|
+
// src/connectors/supabase/index.ts
|
|
9110
|
+
init_cjs_shims();
|
|
9111
|
+
|
|
9112
|
+
// src/connectors/supabase/client.ts
|
|
9113
|
+
init_cjs_shims();
|
|
9114
|
+
var DEFAULT_SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com";
|
|
9115
|
+
var DEFAULT_LOG_LIMIT = 1e3;
|
|
9116
|
+
var SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
9117
|
+
function boundedSupabaseLogWindow(since, now, maxLookbackMs) {
|
|
9118
|
+
const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS);
|
|
9119
|
+
const floor = new Date(now.getTime() - window);
|
|
9120
|
+
const endIso = now.toISOString();
|
|
9121
|
+
if (!since) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9122
|
+
const sinceMs = new Date(since).getTime();
|
|
9123
|
+
if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9124
|
+
if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true };
|
|
9125
|
+
return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false };
|
|
9126
|
+
}
|
|
9127
|
+
function buildEdgeLogsQuery(limit) {
|
|
9128
|
+
const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT);
|
|
9129
|
+
return [
|
|
9130
|
+
"select",
|
|
9131
|
+
" format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,",
|
|
9132
|
+
" request.method as method,",
|
|
9133
|
+
" request.path as path,",
|
|
9134
|
+
" response.status_code as status_code",
|
|
9135
|
+
"from edge_logs",
|
|
9136
|
+
"cross join unnest(metadata) as metadata",
|
|
9137
|
+
"cross join unnest(metadata.request) as request",
|
|
9138
|
+
"cross join unnest(metadata.response) as response",
|
|
9139
|
+
"where regexp_contains(request.path, '^/rest/v1/')",
|
|
9140
|
+
"order by timestamp asc",
|
|
9141
|
+
`limit ${safeLimit}`
|
|
9142
|
+
].join("\n");
|
|
9143
|
+
}
|
|
9144
|
+
async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
|
|
9145
|
+
const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
9146
|
+
const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
|
|
9147
|
+
url.searchParams.set("sql", buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT));
|
|
9148
|
+
url.searchParams.set("iso_timestamp_start", startIso);
|
|
9149
|
+
url.searchParams.set("iso_timestamp_end", endIso);
|
|
9150
|
+
const res = await junctionFetch(
|
|
9151
|
+
url,
|
|
9152
|
+
{ method: "GET", headers: bearerAuthHeader(token) },
|
|
9153
|
+
// accountKey: the Supabase project ref (ADR-131's own worked example) —
|
|
9154
|
+
// the Management API's rate limit is enforced per project.
|
|
9155
|
+
{ provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
|
|
9156
|
+
);
|
|
9157
|
+
if (!res.ok) {
|
|
9158
|
+
throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
|
|
9159
|
+
}
|
|
9160
|
+
const body = await res.json();
|
|
9161
|
+
if (body.error) {
|
|
9162
|
+
const message = typeof body.error === "string" ? body.error : body.error.message;
|
|
9163
|
+
throw new Error(`supabase connector: logs.all returned an error (${message})`);
|
|
9164
|
+
}
|
|
9165
|
+
return body.result ?? [];
|
|
9166
|
+
}
|
|
9167
|
+
|
|
9168
|
+
// src/connectors/supabase/map.ts
|
|
9169
|
+
init_cjs_shims();
|
|
9170
|
+
|
|
9171
|
+
// src/connectors/supabase/types.ts
|
|
9172
|
+
init_cjs_shims();
|
|
9173
|
+
function readSupabaseCredentials(raw) {
|
|
9174
|
+
const managementToken = raw["managementToken"];
|
|
9175
|
+
if (typeof managementToken !== "string" || managementToken.length === 0) {
|
|
9176
|
+
throw new Error("supabase connector: credentials.managementToken must be a non-empty string");
|
|
9177
|
+
}
|
|
9178
|
+
const postgresConnectionString = raw["postgresConnectionString"];
|
|
9179
|
+
if (postgresConnectionString !== void 0 && (typeof postgresConnectionString !== "string" || postgresConnectionString.length === 0)) {
|
|
9180
|
+
throw new Error(
|
|
9181
|
+
"supabase connector: credentials.postgresConnectionString must be a non-empty string when present"
|
|
9182
|
+
);
|
|
9183
|
+
}
|
|
9184
|
+
return {
|
|
9185
|
+
managementToken,
|
|
9186
|
+
...postgresConnectionString ? { postgresConnectionString } : {}
|
|
9187
|
+
};
|
|
9188
|
+
}
|
|
9189
|
+
var SUPABASE_TABLE_TARGET_KIND = "supabase-table";
|
|
9190
|
+
var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
|
|
9191
|
+
|
|
9192
|
+
// src/connectors/supabase/map.ts
|
|
9193
|
+
var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
|
|
9194
|
+
var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
|
|
9195
|
+
function targetFromRestPath(path49) {
|
|
9196
|
+
const rpcMatch = REST_RPC_PATH_RE.exec(path49);
|
|
9197
|
+
if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
|
|
9198
|
+
const tableMatch = REST_TABLE_PATH_RE.exec(path49);
|
|
9199
|
+
if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
|
|
9200
|
+
return null;
|
|
9201
|
+
}
|
|
9202
|
+
var ERROR_STATUS_THRESHOLD = 500;
|
|
9203
|
+
function mapEdgeLogRowsToSignals(rows) {
|
|
9204
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9205
|
+
for (const row of rows) {
|
|
9206
|
+
const target = targetFromRestPath(row.path);
|
|
9207
|
+
if (!target) continue;
|
|
9208
|
+
const key = `${target.targetKind}:${target.name}`;
|
|
9209
|
+
const isError = row.status_code >= ERROR_STATUS_THRESHOLD;
|
|
9210
|
+
const existing = buckets2.get(key);
|
|
9211
|
+
if (existing) {
|
|
9212
|
+
existing.callCount += 1;
|
|
9213
|
+
if (isError) existing.errorCount += 1;
|
|
9214
|
+
if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp;
|
|
9215
|
+
} else {
|
|
9216
|
+
buckets2.set(key, {
|
|
9217
|
+
targetKind: target.targetKind,
|
|
9218
|
+
targetName: target.name,
|
|
9219
|
+
callCount: 1,
|
|
9220
|
+
errorCount: isError ? 1 : 0,
|
|
9221
|
+
lastObservedIso: row.timestamp
|
|
9222
|
+
});
|
|
9223
|
+
}
|
|
9224
|
+
}
|
|
9225
|
+
return [...buckets2.values()].map((b) => ({
|
|
9226
|
+
targetKind: b.targetKind,
|
|
9227
|
+
targetName: b.targetName,
|
|
9228
|
+
callCount: b.callCount,
|
|
9229
|
+
errorCount: b.errorCount,
|
|
9230
|
+
lastObservedIso: b.lastObservedIso
|
|
9231
|
+
}));
|
|
9232
|
+
}
|
|
9233
|
+
var FROM_TABLE_RE = /\bfrom\s+"?(?:[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i;
|
|
9234
|
+
var SYSTEM_SCHEMA_PREFIXES = ["pg_", "information_schema"];
|
|
9235
|
+
function tableNameFromQueryText(query) {
|
|
9236
|
+
const match = FROM_TABLE_RE.exec(query);
|
|
9237
|
+
if (!match) return null;
|
|
9238
|
+
const name = match[1];
|
|
9239
|
+
const lower = name.toLowerCase();
|
|
9240
|
+
if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
|
|
9241
|
+
return name;
|
|
9242
|
+
}
|
|
9243
|
+
function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
|
|
9244
|
+
const signals = [];
|
|
9245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9246
|
+
for (const row of rows) {
|
|
9247
|
+
seen.add(row.queryid);
|
|
9248
|
+
const calls = Number(row.calls);
|
|
9249
|
+
const prior = previous.get(row.queryid);
|
|
9250
|
+
previous.set(row.queryid, { calls });
|
|
9251
|
+
if (!prior || calls < prior.calls) continue;
|
|
9252
|
+
const delta = calls - prior.calls;
|
|
9253
|
+
if (delta <= 0) continue;
|
|
9254
|
+
const table = tableNameFromQueryText(row.query);
|
|
9255
|
+
if (!table) continue;
|
|
9256
|
+
signals.push({
|
|
9257
|
+
targetKind: SUPABASE_TABLE_TARGET_KIND,
|
|
9258
|
+
targetName: table,
|
|
9259
|
+
callCount: delta,
|
|
9260
|
+
errorCount: 0,
|
|
9261
|
+
lastObservedIso: nowIso2
|
|
9262
|
+
});
|
|
9263
|
+
}
|
|
9264
|
+
for (const queryid of [...previous.keys()]) {
|
|
9265
|
+
if (!seen.has(queryid)) previous.delete(queryid);
|
|
9266
|
+
}
|
|
9267
|
+
return signals;
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
// src/connectors/supabase/postgres-client.ts
|
|
9271
|
+
init_cjs_shims();
|
|
9272
|
+
var import_pg = __toESM(require("pg"), 1);
|
|
9273
|
+
var { Client } = import_pg.default;
|
|
9274
|
+
var DEFAULT_STATEMENT_LIMIT = 500;
|
|
9275
|
+
var STATEMENTS_QUERY = `
|
|
9276
|
+
select queryid, query, calls, total_exec_time, rows
|
|
9277
|
+
from pg_stat_statements
|
|
9278
|
+
where query ~* '^\\s*select\\b'
|
|
9279
|
+
order by calls desc
|
|
9280
|
+
limit $1
|
|
9281
|
+
`;
|
|
9282
|
+
async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT_LIMIT, accountKey = "unknown", clientFactory = (cs) => new Client({ connectionString: cs })) {
|
|
9283
|
+
return dbJunction(
|
|
9284
|
+
async () => {
|
|
9285
|
+
const client = clientFactory(connectionString);
|
|
9286
|
+
await client.connect();
|
|
9287
|
+
try {
|
|
9288
|
+
await client.query("SET default_transaction_read_only = on");
|
|
9289
|
+
const result = await client.query(STATEMENTS_QUERY, [limit]);
|
|
9290
|
+
return result.rows;
|
|
9291
|
+
} finally {
|
|
9292
|
+
await client.end();
|
|
9293
|
+
}
|
|
9294
|
+
},
|
|
9295
|
+
{ provider: "supabase-postgres", accountKey }
|
|
9296
|
+
);
|
|
9297
|
+
}
|
|
9298
|
+
|
|
9299
|
+
// src/connectors/supabase/resolve.ts
|
|
9300
|
+
init_cjs_shims();
|
|
9301
|
+
var import_types31 = require("@neat.is/types");
|
|
9302
|
+
function createSupabaseResolveTarget(graph, config) {
|
|
9303
|
+
return (signal, _ctx) => {
|
|
9304
|
+
if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
|
|
9305
|
+
return null;
|
|
9306
|
+
}
|
|
9307
|
+
const subResourceId = (0, import_types31.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
|
|
9308
|
+
if (graph.hasNode(subResourceId)) {
|
|
9309
|
+
return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types31.EdgeType.CALLS };
|
|
9310
|
+
}
|
|
9311
|
+
const projectLevelId = (0, import_types31.infraId)("supabase", config.nodeRef);
|
|
9312
|
+
if (graph.hasNode(projectLevelId)) {
|
|
9313
|
+
return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types31.EdgeType.CALLS };
|
|
9314
|
+
}
|
|
9315
|
+
return null;
|
|
9316
|
+
};
|
|
9317
|
+
}
|
|
9318
|
+
|
|
9319
|
+
// src/connectors/supabase/index.ts
|
|
9320
|
+
var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9321
|
+
var SupabaseConnector = class {
|
|
9322
|
+
// `deps.fetchPgStatStatements` defaults to the real Postgres-backed
|
|
9323
|
+
// implementation; tests override it to exercise the "both surfaces
|
|
9324
|
+
// combine" and "surface 2 only runs when a connection string is present"
|
|
9325
|
+
// behavior without a live database — the same dependency-injection seam
|
|
9326
|
+
// `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.
|
|
9327
|
+
constructor(config, deps = {}) {
|
|
9328
|
+
this.config = config;
|
|
9329
|
+
this.deps = deps;
|
|
9330
|
+
}
|
|
9331
|
+
config;
|
|
9332
|
+
deps;
|
|
9333
|
+
provider = "supabase";
|
|
9334
|
+
// pg_stat_statements.calls is cumulative, not per-window (map.ts's
|
|
9335
|
+
// diffPgStatStatementsToSignals doc comment) — this Map carries the
|
|
9336
|
+
// previous poll's counts across ticks, the same way
|
|
9337
|
+
// `startConnectorPollLoop` (connectors/index.ts) carries `since` across
|
|
9338
|
+
// ticks for every connector. Lives on the instance, not `ConnectorContext`,
|
|
9339
|
+
// because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's
|
|
9340
|
+
// `{ ...ctx, since }`) while this connector object is the one thing every
|
|
9341
|
+
// tick shares.
|
|
9342
|
+
statementBaselines = /* @__PURE__ */ new Map();
|
|
9343
|
+
async poll(ctx) {
|
|
9344
|
+
const creds = readSupabaseCredentials(ctx.credentials);
|
|
9345
|
+
const now = /* @__PURE__ */ new Date();
|
|
9346
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS;
|
|
9347
|
+
const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs);
|
|
9348
|
+
const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso);
|
|
9349
|
+
const signals = mapEdgeLogRowsToSignals(logRows);
|
|
9350
|
+
if (creds.postgresConnectionString) {
|
|
9351
|
+
const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
|
|
9352
|
+
const statementRows = await fetchStatements(
|
|
9353
|
+
creds.postgresConnectionString,
|
|
9354
|
+
this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
|
|
9355
|
+
this.config.apiProjectRef
|
|
9356
|
+
);
|
|
9357
|
+
signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
|
|
9358
|
+
}
|
|
9359
|
+
return signals;
|
|
9360
|
+
}
|
|
9361
|
+
};
|
|
9362
|
+
function createSupabaseConnector(graph, config, deps = {}) {
|
|
9363
|
+
return {
|
|
9364
|
+
connector: new SupabaseConnector(config, deps),
|
|
9365
|
+
resolveTarget: createSupabaseResolveTarget(graph, config)
|
|
9366
|
+
};
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
// src/connectors/railway/index.ts
|
|
9370
|
+
init_cjs_shims();
|
|
9371
|
+
var import_types35 = require("@neat.is/types");
|
|
9372
|
+
|
|
9373
|
+
// src/connectors/railway/client.ts
|
|
9374
|
+
init_cjs_shims();
|
|
9375
|
+
var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
9376
|
+
var DEFAULT_MAX_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
|
|
9377
|
+
var DEFAULT_LOG_LIMIT2 = 1e3;
|
|
9378
|
+
function readRailwayToken(credentials) {
|
|
9379
|
+
const token = credentials.token;
|
|
9380
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
9381
|
+
throw new Error(
|
|
9382
|
+
"Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)"
|
|
9383
|
+
);
|
|
9384
|
+
}
|
|
9385
|
+
return token;
|
|
9386
|
+
}
|
|
9387
|
+
async function railwayGraphQL(apiUrl, token, query, variables, accountKey) {
|
|
9388
|
+
const res = await junctionFetch(
|
|
9389
|
+
apiUrl,
|
|
9390
|
+
{
|
|
9391
|
+
method: "POST",
|
|
9392
|
+
headers: {
|
|
9393
|
+
"Content-Type": "application/json",
|
|
9394
|
+
// docs.railway.com/integrations/api/graphql-overview confirms
|
|
9395
|
+
// `Authorization: Bearer <token>` for an account/workspace token;
|
|
9396
|
+
// Railway also documents a dedicated `Project-Access-Token: <token>`
|
|
9397
|
+
// header for the project-scoped token ADR-127 targets specifically.
|
|
9398
|
+
// This connector sends the Bearer form — needs-endpoint-testing
|
|
9399
|
+
// whether a live Project-Access-Token requires the dedicated header
|
|
9400
|
+
// instead once this poller runs against a real project.
|
|
9401
|
+
...bearerAuthHeader(token)
|
|
9402
|
+
},
|
|
9403
|
+
body: JSON.stringify({ query, variables })
|
|
9404
|
+
},
|
|
9405
|
+
{ provider: "railway", accountKey }
|
|
9406
|
+
);
|
|
9407
|
+
if (!res.ok) {
|
|
9408
|
+
throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
9409
|
+
}
|
|
9410
|
+
const body = await res.json();
|
|
9411
|
+
if (body.errors && body.errors.length > 0) {
|
|
9412
|
+
throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
9413
|
+
}
|
|
9414
|
+
if (!body.data) throw new Error("Railway GraphQL response carried no data");
|
|
9415
|
+
return body.data;
|
|
9416
|
+
}
|
|
9417
|
+
var HTTP_LOGS_QUERY = `
|
|
9418
|
+
query HttpLogs(
|
|
9419
|
+
$environmentId: String!
|
|
9420
|
+
$serviceId: String!
|
|
9421
|
+
$startDate: String!
|
|
9422
|
+
$endDate: String!
|
|
9423
|
+
$limit: Int
|
|
9424
|
+
) {
|
|
9425
|
+
httpLogs(
|
|
9426
|
+
environmentId: $environmentId
|
|
9427
|
+
serviceId: $serviceId
|
|
9428
|
+
startDate: $startDate
|
|
9429
|
+
endDate: $endDate
|
|
9430
|
+
limit: $limit
|
|
9431
|
+
) {
|
|
9432
|
+
timestamp
|
|
9433
|
+
method
|
|
9434
|
+
path
|
|
9435
|
+
httpStatus
|
|
9436
|
+
totalDuration
|
|
9437
|
+
requestId
|
|
9438
|
+
deploymentId
|
|
9439
|
+
edgeRegion
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
`;
|
|
9443
|
+
var NETWORK_FLOW_LOGS_QUERY = `
|
|
9444
|
+
query NetworkFlowLogs(
|
|
9445
|
+
$environmentId: String!
|
|
9446
|
+
$serviceId: String!
|
|
9447
|
+
$startDate: String!
|
|
9448
|
+
$endDate: String!
|
|
9449
|
+
$limit: Int
|
|
9450
|
+
) {
|
|
9451
|
+
networkFlowLogs(
|
|
9452
|
+
environmentId: $environmentId
|
|
9453
|
+
serviceId: $serviceId
|
|
9454
|
+
startDate: $startDate
|
|
9455
|
+
endDate: $endDate
|
|
9456
|
+
limit: $limit
|
|
9457
|
+
) {
|
|
9458
|
+
timestamp
|
|
9459
|
+
peerServiceId
|
|
9460
|
+
peerKind
|
|
9461
|
+
direction
|
|
9462
|
+
byteCount
|
|
9463
|
+
packetCount
|
|
9464
|
+
dropCause
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
`;
|
|
9468
|
+
async function fetchRailwayHttpLogs(config, token, startDate, endDate) {
|
|
9469
|
+
const data = await railwayGraphQL(
|
|
9470
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9471
|
+
token,
|
|
9472
|
+
HTTP_LOGS_QUERY,
|
|
9473
|
+
{
|
|
9474
|
+
environmentId: config.environmentId,
|
|
9475
|
+
serviceId: config.serviceId,
|
|
9476
|
+
startDate,
|
|
9477
|
+
endDate,
|
|
9478
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9479
|
+
},
|
|
9480
|
+
config.environmentId
|
|
9481
|
+
);
|
|
9482
|
+
return data.httpLogs;
|
|
9483
|
+
}
|
|
9484
|
+
async function fetchRailwayNetworkFlowLogs(config, token, startDate, endDate) {
|
|
9485
|
+
const data = await railwayGraphQL(
|
|
9486
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9487
|
+
token,
|
|
9488
|
+
NETWORK_FLOW_LOGS_QUERY,
|
|
9489
|
+
{
|
|
9490
|
+
environmentId: config.environmentId,
|
|
9491
|
+
serviceId: config.serviceId,
|
|
9492
|
+
startDate,
|
|
9493
|
+
endDate,
|
|
9494
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9495
|
+
},
|
|
9496
|
+
config.environmentId
|
|
9497
|
+
);
|
|
9498
|
+
return data.networkFlowLogs;
|
|
9499
|
+
}
|
|
9500
|
+
function boundedRailwayStartDate(since, now, maxLookbackMs) {
|
|
9501
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
9502
|
+
if (!since) return floor.toISOString();
|
|
9503
|
+
const sinceMs = new Date(since).getTime();
|
|
9504
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
9505
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
9506
|
+
}
|
|
9507
|
+
|
|
9508
|
+
// src/connectors/railway/index.ts
|
|
9509
|
+
var ROUTE_TARGET_KIND = "route";
|
|
9510
|
+
var UNMATCHED_ROUTE_TARGET_KIND = "unmatched-route";
|
|
9511
|
+
var PEER_SERVICE_TARGET_KIND = "peer-service";
|
|
9512
|
+
function buildRailwayRouteIndex(graph, serviceName) {
|
|
9513
|
+
const out = [];
|
|
9514
|
+
graph.forEachNode((_id, attrs) => {
|
|
9515
|
+
const node = attrs;
|
|
9516
|
+
if (node.type !== import_types35.NodeType.RouteNode) return;
|
|
9517
|
+
const route = attrs;
|
|
9518
|
+
if (route.service !== serviceName) return;
|
|
9519
|
+
out.push({
|
|
9520
|
+
method: route.method.toUpperCase(),
|
|
9521
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
9522
|
+
routeNodeId: route.id,
|
|
9523
|
+
path: route.path,
|
|
9524
|
+
line: route.line
|
|
9525
|
+
});
|
|
9526
|
+
});
|
|
9527
|
+
return out;
|
|
9528
|
+
}
|
|
9529
|
+
function findRailwayRoute(entries, method, normalizedPath) {
|
|
9530
|
+
return entries.find(
|
|
9531
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
9532
|
+
);
|
|
9533
|
+
}
|
|
9534
|
+
function bucketKey2(method, normalizedPath) {
|
|
9535
|
+
return `${method} ${normalizedPath}`;
|
|
9536
|
+
}
|
|
9537
|
+
function isHttpErrorStatus(status2) {
|
|
9538
|
+
return status2 >= 400;
|
|
9539
|
+
}
|
|
9540
|
+
function upsertBucket(buckets2, key, isError, timestamp, build) {
|
|
9541
|
+
const existing = buckets2.get(key);
|
|
9542
|
+
if (existing) {
|
|
9543
|
+
existing.callCount += 1;
|
|
9544
|
+
if (isError) existing.errorCount += 1;
|
|
9545
|
+
if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
|
|
9549
|
+
}
|
|
9550
|
+
function mapRailwayHttpLogsToSignals(entries, routeIndex) {
|
|
9551
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9552
|
+
for (const entry2 of entries) {
|
|
9553
|
+
const method = entry2.method.toUpperCase();
|
|
9554
|
+
const normalizedPath = normalizePathTemplate(entry2.path);
|
|
9555
|
+
const match = findRailwayRoute(routeIndex, method, normalizedPath);
|
|
9556
|
+
const isError = isHttpErrorStatus(entry2.httpStatus);
|
|
9557
|
+
if (match) {
|
|
9558
|
+
upsertBucket(buckets2, `route:${match.routeNodeId}`, isError, entry2.timestamp, () => ({
|
|
9559
|
+
targetKind: ROUTE_TARGET_KIND,
|
|
9560
|
+
targetName: match.routeNodeId,
|
|
9561
|
+
// RouteNode.line is optional in the schema (packages/types/src/
|
|
9562
|
+
// nodes.ts) even though routes.ts always sets it today — skip the
|
|
9563
|
+
// callSite rather than fabricate a line when it's ever absent
|
|
9564
|
+
// (file-awareness.md §6).
|
|
9565
|
+
...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
|
|
9566
|
+
}));
|
|
9567
|
+
} else {
|
|
9568
|
+
upsertBucket(
|
|
9569
|
+
buckets2,
|
|
9570
|
+
`unmatched:${bucketKey2(method, normalizedPath)}`,
|
|
9571
|
+
isError,
|
|
9572
|
+
entry2.timestamp,
|
|
9573
|
+
() => ({
|
|
9574
|
+
targetKind: UNMATCHED_ROUTE_TARGET_KIND,
|
|
9575
|
+
targetName: bucketKey2(method, normalizedPath)
|
|
9576
|
+
})
|
|
9577
|
+
);
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
return [...buckets2.values()].map((b) => ({
|
|
9581
|
+
targetKind: b.targetKind,
|
|
9582
|
+
targetName: b.targetName,
|
|
9583
|
+
callCount: b.callCount,
|
|
9584
|
+
errorCount: b.errorCount,
|
|
9585
|
+
lastObservedIso: b.lastObservedIso,
|
|
9586
|
+
...b.callSite ? { callSite: b.callSite } : {}
|
|
9587
|
+
}));
|
|
9588
|
+
}
|
|
9589
|
+
function mapRailwayNetworkFlowLogsToSignals(entries) {
|
|
9590
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9591
|
+
for (const entry2 of entries) {
|
|
9592
|
+
if (!entry2.peerServiceId) continue;
|
|
9593
|
+
const isError = entry2.dropCause !== null && entry2.dropCause !== "";
|
|
9594
|
+
upsertBucket(buckets2, entry2.peerServiceId, isError, entry2.timestamp, () => ({
|
|
9595
|
+
targetKind: PEER_SERVICE_TARGET_KIND,
|
|
9596
|
+
targetName: entry2.peerServiceId
|
|
9597
|
+
}));
|
|
9598
|
+
}
|
|
9599
|
+
return [...buckets2.values()].map((b) => ({
|
|
9600
|
+
targetKind: b.targetKind,
|
|
9601
|
+
targetName: b.targetName,
|
|
9602
|
+
callCount: b.callCount,
|
|
9603
|
+
errorCount: b.errorCount,
|
|
9604
|
+
lastObservedIso: b.lastObservedIso
|
|
9605
|
+
}));
|
|
9606
|
+
}
|
|
9607
|
+
function createRailwayResolveTarget(config) {
|
|
9608
|
+
return (signal) => {
|
|
9609
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9610
|
+
if (!serviceName) return null;
|
|
9611
|
+
if (signal.targetKind === ROUTE_TARGET_KIND) {
|
|
9612
|
+
return { targetNodeId: signal.targetName, serviceName, edgeType: import_types35.EdgeType.CALLS };
|
|
9613
|
+
}
|
|
9614
|
+
if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
|
|
9615
|
+
const peerName = config.serviceNameById[signal.targetName];
|
|
9616
|
+
if (!peerName) return null;
|
|
9617
|
+
return { targetNodeId: (0, import_types35.serviceId)(peerName), serviceName, edgeType: import_types35.EdgeType.CONNECTS_TO };
|
|
9618
|
+
}
|
|
9619
|
+
return null;
|
|
9620
|
+
};
|
|
9621
|
+
}
|
|
9622
|
+
function createRailwayConnector(graph, config) {
|
|
9623
|
+
return {
|
|
9624
|
+
provider: "railway",
|
|
9625
|
+
async poll(ctx) {
|
|
9626
|
+
const token = readRailwayToken(ctx.credentials);
|
|
9627
|
+
const now = /* @__PURE__ */ new Date();
|
|
9628
|
+
const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS2;
|
|
9629
|
+
const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs);
|
|
9630
|
+
const endDate = now.toISOString();
|
|
9631
|
+
const [httpLogs, flowLogs] = await Promise.all([
|
|
9632
|
+
fetchRailwayHttpLogs(config, token, startDate, endDate),
|
|
9633
|
+
fetchRailwayNetworkFlowLogs(config, token, startDate, endDate)
|
|
9634
|
+
]);
|
|
9635
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9636
|
+
const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : [];
|
|
9637
|
+
return [
|
|
9638
|
+
...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),
|
|
9639
|
+
...mapRailwayNetworkFlowLogsToSignals(flowLogs)
|
|
9640
|
+
];
|
|
9641
|
+
}
|
|
9642
|
+
};
|
|
9643
|
+
}
|
|
9644
|
+
|
|
9645
|
+
// src/connectors/firebase/index.ts
|
|
9646
|
+
init_cjs_shims();
|
|
9647
|
+
|
|
9648
|
+
// src/connectors/firebase/logging-api.ts
|
|
9649
|
+
init_cjs_shims();
|
|
9650
|
+
function readFirebaseCredentials(raw) {
|
|
9651
|
+
const projectId = raw["projectId"];
|
|
9652
|
+
const accessToken = raw["accessToken"];
|
|
9653
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
9654
|
+
throw new Error("firebase connector: credentials.projectId must be a non-empty string");
|
|
9655
|
+
}
|
|
9656
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
9657
|
+
throw new Error("firebase connector: credentials.accessToken must be a non-empty string");
|
|
9658
|
+
}
|
|
9659
|
+
return { projectId, accessToken };
|
|
9660
|
+
}
|
|
9661
|
+
var RESOURCE_TYPES = [
|
|
9662
|
+
"cloud_function",
|
|
9663
|
+
"cloud_run_revision",
|
|
9664
|
+
"firebase_domain"
|
|
9665
|
+
];
|
|
9666
|
+
function isFirebaseResourceType(value) {
|
|
9667
|
+
return RESOURCE_TYPES.includes(value);
|
|
9668
|
+
}
|
|
9669
|
+
function buildEntriesFilter(sinceIso) {
|
|
9670
|
+
return [
|
|
9671
|
+
'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
|
|
9672
|
+
"httpRequest:*",
|
|
9673
|
+
`timestamp >= "${sinceIso}"`
|
|
9674
|
+
].join(" AND ");
|
|
9675
|
+
}
|
|
9676
|
+
var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9677
|
+
var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
|
|
9678
|
+
var PAGE_SIZE = 1e3;
|
|
9679
|
+
var MAX_PAGES = 20;
|
|
9680
|
+
async function fetchHttpRequestLogEntries(creds, sinceIso) {
|
|
9681
|
+
const filter = buildEntriesFilter(sinceIso);
|
|
9682
|
+
const out = [];
|
|
9683
|
+
let pageToken;
|
|
9684
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
9685
|
+
const body = {
|
|
9686
|
+
resourceNames: [`projects/${creds.projectId}`],
|
|
9687
|
+
filter,
|
|
9688
|
+
orderBy: "timestamp asc",
|
|
9689
|
+
pageSize: PAGE_SIZE,
|
|
9690
|
+
...pageToken ? { pageToken } : {}
|
|
9691
|
+
};
|
|
9692
|
+
const res = await junctionFetch(
|
|
9693
|
+
ENTRIES_LIST_URL,
|
|
9694
|
+
{
|
|
9695
|
+
method: "POST",
|
|
9696
|
+
headers: {
|
|
9697
|
+
...bearerAuthHeader(creds.accessToken),
|
|
9698
|
+
"Content-Type": "application/json"
|
|
9699
|
+
},
|
|
9700
|
+
body: JSON.stringify(body)
|
|
9701
|
+
},
|
|
9702
|
+
// accountKey: the GCP project id (ADR-131's own worked example for
|
|
9703
|
+
// Firebase) — one customer's Cloud Logging quota is scoped per GCP
|
|
9704
|
+
// project, not per Firebase site/function.
|
|
9705
|
+
{ provider: "firebase", accountKey: creds.projectId }
|
|
9706
|
+
);
|
|
9707
|
+
if (!res.ok) {
|
|
9708
|
+
throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
|
|
9709
|
+
}
|
|
9710
|
+
const json = await res.json();
|
|
9711
|
+
out.push(...json.entries ?? []);
|
|
9712
|
+
if (!json.nextPageToken) break;
|
|
9713
|
+
pageToken = json.nextPageToken;
|
|
9714
|
+
}
|
|
9715
|
+
return out;
|
|
9716
|
+
}
|
|
9717
|
+
|
|
9718
|
+
// src/connectors/firebase/map.ts
|
|
9719
|
+
init_cjs_shims();
|
|
9720
|
+
var FIELD_SEP = "\0";
|
|
9721
|
+
function packFirebaseTargetName(identity) {
|
|
9722
|
+
return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP);
|
|
9723
|
+
}
|
|
9724
|
+
function parseFirebaseTargetName(targetName) {
|
|
9725
|
+
const firstSep = targetName.indexOf(FIELD_SEP);
|
|
9726
|
+
if (firstSep === -1) return null;
|
|
9727
|
+
const resourceName = targetName.slice(0, firstSep);
|
|
9728
|
+
const rest = targetName.slice(firstSep + 1);
|
|
9729
|
+
const secondSep = rest.indexOf(FIELD_SEP);
|
|
9730
|
+
if (secondSep === -1) return null;
|
|
9731
|
+
const method = rest.slice(0, secondSep);
|
|
9732
|
+
const path49 = rest.slice(secondSep + 1);
|
|
9733
|
+
if (!resourceName || !method || !path49) return null;
|
|
9734
|
+
return { resourceName, method, path: path49 };
|
|
9735
|
+
}
|
|
9736
|
+
function resourceNameFor(type, labels) {
|
|
9737
|
+
if (!labels) return null;
|
|
9738
|
+
switch (type) {
|
|
9739
|
+
case "cloud_function":
|
|
9740
|
+
return labels["function_name"] ?? null;
|
|
9741
|
+
case "cloud_run_revision":
|
|
9742
|
+
return labels["service_name"] ?? null;
|
|
9743
|
+
case "firebase_domain":
|
|
9744
|
+
return labels["site_name"] ?? null;
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
function pathFromRequestUrl(requestUrl) {
|
|
9748
|
+
if (!requestUrl) return null;
|
|
9749
|
+
if (requestUrl.startsWith("/")) {
|
|
9750
|
+
const withoutQuery = requestUrl.split("?")[0];
|
|
9751
|
+
return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
|
|
9752
|
+
}
|
|
9753
|
+
try {
|
|
9754
|
+
const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
|
|
9755
|
+
const parsed = new URL(candidate);
|
|
9756
|
+
return parsed.pathname || "/";
|
|
9757
|
+
} catch {
|
|
9758
|
+
return null;
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
var ERROR_STATUS_THRESHOLD2 = 500;
|
|
9762
|
+
function mapLogEntryToSignal(entry2) {
|
|
9763
|
+
const resourceType = entry2.resource?.type;
|
|
9764
|
+
if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
|
|
9765
|
+
const resourceName = resourceNameFor(resourceType, entry2.resource?.labels);
|
|
9766
|
+
if (!resourceName) return null;
|
|
9767
|
+
const req2 = entry2.httpRequest;
|
|
9768
|
+
if (!req2) return null;
|
|
9769
|
+
if (!req2.requestMethod) return null;
|
|
9770
|
+
const method = req2.requestMethod.toUpperCase();
|
|
9771
|
+
const path49 = pathFromRequestUrl(req2.requestUrl);
|
|
9772
|
+
if (path49 === null) return null;
|
|
9773
|
+
const timestamp = entry2.timestamp;
|
|
9774
|
+
if (!timestamp) return null;
|
|
9775
|
+
const isError = typeof req2.status === "number" && req2.status >= ERROR_STATUS_THRESHOLD2;
|
|
9776
|
+
return {
|
|
9777
|
+
targetKind: resourceType,
|
|
9778
|
+
targetName: packFirebaseTargetName({ resourceName, method, path: path49 }),
|
|
9779
|
+
callCount: 1,
|
|
9780
|
+
errorCount: isError ? 1 : 0,
|
|
9781
|
+
lastObservedIso: timestamp
|
|
9782
|
+
};
|
|
9783
|
+
}
|
|
9784
|
+
function mapLogEntriesToSignals(entries) {
|
|
9785
|
+
const out = [];
|
|
9786
|
+
for (const entry2 of entries) {
|
|
9787
|
+
const signal = mapLogEntryToSignal(entry2);
|
|
9788
|
+
if (signal) out.push(signal);
|
|
9789
|
+
}
|
|
9790
|
+
return out;
|
|
9791
|
+
}
|
|
9792
|
+
|
|
9793
|
+
// src/connectors/firebase/resolve.ts
|
|
9794
|
+
init_cjs_shims();
|
|
9795
|
+
var import_types36 = require("@neat.is/types");
|
|
9796
|
+
function neatServiceNameFor(resourceType, resourceName, serviceMap) {
|
|
9797
|
+
switch (resourceType) {
|
|
9798
|
+
case "cloud_function":
|
|
9799
|
+
return serviceMap.functions?.[resourceName] ?? null;
|
|
9800
|
+
case "cloud_run_revision":
|
|
9801
|
+
return serviceMap.cloudRun?.[resourceName] ?? null;
|
|
9802
|
+
case "firebase_domain":
|
|
9803
|
+
return serviceMap.hosting?.[resourceName] ?? null;
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
function routeEntriesFor(graph, serviceName) {
|
|
9807
|
+
const entries = [];
|
|
9808
|
+
graph.forEachNode((_id, attrs) => {
|
|
9809
|
+
const node = attrs;
|
|
9810
|
+
if (node.type !== import_types36.NodeType.RouteNode) return;
|
|
9811
|
+
const route = attrs;
|
|
9812
|
+
if (route.service !== serviceName) return;
|
|
9813
|
+
entries.push({
|
|
9814
|
+
method: route.method.toUpperCase(),
|
|
9815
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
9816
|
+
routeNodeId: route.id
|
|
9817
|
+
});
|
|
9818
|
+
});
|
|
9819
|
+
return entries;
|
|
9820
|
+
}
|
|
9821
|
+
function findRoute2(entries, method, normalizedPath) {
|
|
9822
|
+
return entries.find(
|
|
9823
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
9824
|
+
);
|
|
9825
|
+
}
|
|
9826
|
+
function createFirebaseResolveTarget(graph, serviceMap) {
|
|
9827
|
+
return (signal, _ctx) => {
|
|
9828
|
+
const resourceType = signal.targetKind;
|
|
9829
|
+
if (resourceType !== "cloud_function" && resourceType !== "cloud_run_revision" && resourceType !== "firebase_domain") {
|
|
9830
|
+
return null;
|
|
9831
|
+
}
|
|
9832
|
+
const identity = parseFirebaseTargetName(signal.targetName);
|
|
9833
|
+
if (!identity) return null;
|
|
9834
|
+
const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
|
|
9835
|
+
if (!serviceName) return null;
|
|
9836
|
+
const normalizedPath = normalizePathTemplate(identity.path);
|
|
9837
|
+
const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
|
|
9838
|
+
if (!match) return null;
|
|
9839
|
+
return {
|
|
9840
|
+
targetNodeId: match.routeNodeId,
|
|
9841
|
+
serviceName,
|
|
9842
|
+
edgeType: import_types36.EdgeType.CALLS
|
|
9843
|
+
};
|
|
9844
|
+
};
|
|
9845
|
+
}
|
|
9846
|
+
|
|
9847
|
+
// src/connectors/firebase/index.ts
|
|
9848
|
+
var FirebaseConnector = class {
|
|
9849
|
+
provider = "firebase";
|
|
9850
|
+
async poll(ctx) {
|
|
9851
|
+
const creds = readFirebaseCredentials(ctx.credentials);
|
|
9852
|
+
const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
|
|
9853
|
+
const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
|
|
9854
|
+
return mapLogEntriesToSignals(entries);
|
|
9855
|
+
}
|
|
9856
|
+
};
|
|
9857
|
+
function createFirebaseConnector(graph, serviceMap) {
|
|
9858
|
+
return {
|
|
9859
|
+
connector: new FirebaseConnector(),
|
|
9860
|
+
resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
|
|
9861
|
+
};
|
|
9862
|
+
}
|
|
9863
|
+
|
|
9864
|
+
// src/connectors/cloudflare/index.ts
|
|
9865
|
+
init_cjs_shims();
|
|
9866
|
+
|
|
9867
|
+
// src/connectors/cloudflare/connector.ts
|
|
9868
|
+
init_cjs_shims();
|
|
9869
|
+
var import_types38 = require("@neat.is/types");
|
|
9870
|
+
|
|
9871
|
+
// src/connectors/cloudflare/client.ts
|
|
9872
|
+
init_cjs_shims();
|
|
9873
|
+
var import_node_crypto3 = require("crypto");
|
|
9874
|
+
var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
9875
|
+
var DEFAULT_EVENT_LIMIT = 1e3;
|
|
9876
|
+
async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
|
|
9877
|
+
const token = ctx.credentials.apiToken;
|
|
9878
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
9879
|
+
throw new Error("cloudflare connector: ctx.credentials.apiToken must be a non-empty string");
|
|
9880
|
+
}
|
|
9881
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
9882
|
+
const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`;
|
|
9883
|
+
const body = {
|
|
9884
|
+
// Cloudflare's schema requires an identifier per query even for an
|
|
9885
|
+
// ad-hoc, unsaved one — a fresh id per tick, never reused.
|
|
9886
|
+
queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
|
|
9887
|
+
timeframe: { from: window.fromMs, to: window.toMs },
|
|
9888
|
+
view: "events",
|
|
9889
|
+
limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
|
|
9890
|
+
// Execute without persisting — this is a read, not a saved query
|
|
9891
|
+
// (connectors.md §2's "never writes on the read path" applies to
|
|
9892
|
+
// Cloudflare's own query-history state too).
|
|
9893
|
+
dry: true
|
|
9894
|
+
};
|
|
9895
|
+
const res = await junctionFetch(
|
|
9896
|
+
url,
|
|
9897
|
+
{
|
|
9898
|
+
method: "POST",
|
|
9899
|
+
headers: {
|
|
9900
|
+
"Content-Type": "application/json",
|
|
9901
|
+
...bearerAuthHeader(token)
|
|
9902
|
+
},
|
|
9903
|
+
body: JSON.stringify(body)
|
|
9904
|
+
},
|
|
9905
|
+
// accountKey: the Cloudflare account id (ADR-131's own worked example) —
|
|
9906
|
+
// the Telemetry Query API's ~300/5min limit is enforced per account.
|
|
9907
|
+
{ provider: "cloudflare", accountKey: config.accountId, fetchImpl }
|
|
9908
|
+
);
|
|
9909
|
+
if (!res.ok) {
|
|
9910
|
+
throw new Error(
|
|
9911
|
+
`cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`
|
|
9912
|
+
);
|
|
9913
|
+
}
|
|
9914
|
+
const payload = await res.json();
|
|
9915
|
+
if (!payload.success) {
|
|
9916
|
+
const message = payload.errors?.map((e) => e.message).join("; ") || "unknown error";
|
|
9917
|
+
throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
|
|
9918
|
+
}
|
|
9919
|
+
return payload.result?.events?.events ?? [];
|
|
9920
|
+
}
|
|
9921
|
+
|
|
9922
|
+
// src/connectors/cloudflare/map.ts
|
|
9923
|
+
init_cjs_shims();
|
|
9924
|
+
|
|
9925
|
+
// src/connectors/cloudflare/types.ts
|
|
9926
|
+
init_cjs_shims();
|
|
9927
|
+
var CLOUDFLARE_TARGET_KIND = "cloudflare-worker-invocation";
|
|
9928
|
+
|
|
9929
|
+
// src/connectors/cloudflare/map.ts
|
|
9930
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
9931
|
+
"GET",
|
|
9932
|
+
"POST",
|
|
9933
|
+
"PUT",
|
|
9934
|
+
"PATCH",
|
|
9935
|
+
"DELETE",
|
|
9936
|
+
"HEAD",
|
|
9937
|
+
"OPTIONS",
|
|
9938
|
+
"TRACE",
|
|
9939
|
+
"CONNECT"
|
|
9940
|
+
]);
|
|
9941
|
+
var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
|
|
9942
|
+
function parseHttpMethodFromTrigger(trigger) {
|
|
9943
|
+
if (!trigger) return null;
|
|
9944
|
+
const match = LEADING_TOKEN_RE.exec(trigger.trim());
|
|
9945
|
+
const token = match?.[1];
|
|
9946
|
+
if (!token) return null;
|
|
9947
|
+
const method = token.toUpperCase();
|
|
9948
|
+
return HTTP_METHODS.has(method) ? method : null;
|
|
9949
|
+
}
|
|
9950
|
+
var ERROR_STATUS_THRESHOLD3 = 500;
|
|
9951
|
+
function mapEventToSignal(event) {
|
|
9952
|
+
const metadata = event.$metadata;
|
|
9953
|
+
const workers = event.$workers;
|
|
9954
|
+
const method = parseHttpMethodFromTrigger(metadata?.trigger);
|
|
9955
|
+
if (!method) return null;
|
|
9956
|
+
const scriptName = workers?.scriptName ?? metadata?.service;
|
|
9957
|
+
if (!scriptName) return null;
|
|
9958
|
+
const timestampMs = event.timestamp ?? metadata?.startTime;
|
|
9959
|
+
if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
|
|
9960
|
+
const statusCode = metadata?.statusCode;
|
|
9961
|
+
const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
|
|
9962
|
+
return {
|
|
9963
|
+
targetKind: CLOUDFLARE_TARGET_KIND,
|
|
9964
|
+
targetName: scriptName,
|
|
9965
|
+
callCount: 1,
|
|
9966
|
+
errorCount: isError ? 1 : 0,
|
|
9967
|
+
lastObservedIso: new Date(timestampMs).toISOString(),
|
|
9968
|
+
method,
|
|
9969
|
+
...typeof statusCode === "number" ? { statusCode } : {},
|
|
9970
|
+
...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
|
|
9974
|
+
// src/connectors/cloudflare/connector.ts
|
|
9975
|
+
var DEFAULT_MAX_LOOKBACK_MS3 = 60 * 60 * 1e3;
|
|
9976
|
+
function resolveFromMs(since, maxLookbackMs) {
|
|
9977
|
+
const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS3;
|
|
9978
|
+
const now = Date.now();
|
|
9979
|
+
const floor = now - cap;
|
|
9980
|
+
if (!since) return floor;
|
|
9981
|
+
const parsed = Date.parse(since);
|
|
9982
|
+
if (Number.isNaN(parsed)) return floor;
|
|
9983
|
+
return Math.max(parsed, floor);
|
|
9984
|
+
}
|
|
9985
|
+
var CloudflareConnector = class {
|
|
9986
|
+
constructor(config) {
|
|
9987
|
+
this.config = config;
|
|
9988
|
+
}
|
|
9989
|
+
config;
|
|
9990
|
+
provider = "cloudflare";
|
|
9991
|
+
async poll(ctx) {
|
|
9992
|
+
const toMs = Date.now();
|
|
9993
|
+
const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs);
|
|
9994
|
+
const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs });
|
|
9995
|
+
const signals = [];
|
|
9996
|
+
for (const event of events) {
|
|
9997
|
+
const signal = mapEventToSignal(event);
|
|
9998
|
+
if (signal) signals.push(signal);
|
|
9999
|
+
}
|
|
10000
|
+
return signals;
|
|
10001
|
+
}
|
|
10002
|
+
};
|
|
10003
|
+
function createCloudflareResolveTarget(config) {
|
|
10004
|
+
return (signal) => {
|
|
10005
|
+
if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
|
|
10006
|
+
const mapping = config.workers[signal.targetName];
|
|
10007
|
+
if (!mapping) return null;
|
|
10008
|
+
return {
|
|
10009
|
+
targetNodeId: (0, import_types38.fileId)(mapping.service, mapping.entryFile),
|
|
10010
|
+
serviceName: mapping.service,
|
|
10011
|
+
edgeType: import_types38.EdgeType.CALLS
|
|
10012
|
+
};
|
|
10013
|
+
};
|
|
10014
|
+
}
|
|
10015
|
+
|
|
10016
|
+
// src/connectors-config.ts
|
|
10017
|
+
init_cjs_shims();
|
|
10018
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
10019
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
10020
|
+
var import_node_fs25 = require("fs");
|
|
10021
|
+
var CONNECTORS_CONFIG_VERSION = 1;
|
|
10022
|
+
var EnvRefUnsetError = class extends Error {
|
|
10023
|
+
ref;
|
|
10024
|
+
varName;
|
|
10025
|
+
constructor(ref, varName) {
|
|
10026
|
+
super(`${ref} is unset`);
|
|
10027
|
+
this.name = "EnvRefUnsetError";
|
|
10028
|
+
this.ref = ref;
|
|
10029
|
+
this.varName = varName;
|
|
10030
|
+
}
|
|
10031
|
+
};
|
|
10032
|
+
function neatHome2() {
|
|
10033
|
+
const override = process.env.NEAT_HOME;
|
|
10034
|
+
if (override && override.length > 0) return import_node_path44.default.resolve(override);
|
|
10035
|
+
return import_node_path44.default.join(import_node_os4.default.homedir(), ".neat");
|
|
10036
|
+
}
|
|
10037
|
+
function connectorsConfigPath(home = neatHome2()) {
|
|
10038
|
+
return import_node_path44.default.join(home, "connectors.json");
|
|
10039
|
+
}
|
|
10040
|
+
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
10041
|
+
async function warnIfModeLooserThan0600(file) {
|
|
10042
|
+
if (process.platform === "win32") return;
|
|
10043
|
+
try {
|
|
10044
|
+
const stat = await import_node_fs25.promises.stat(file);
|
|
10045
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
10046
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
10047
|
+
console.warn(
|
|
10048
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
|
|
10049
|
+
);
|
|
10050
|
+
}
|
|
10051
|
+
} catch {
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
10054
|
+
async function readConnectorsConfig(home = neatHome2()) {
|
|
10055
|
+
const file = connectorsConfigPath(home);
|
|
10056
|
+
let raw;
|
|
10057
|
+
try {
|
|
10058
|
+
raw = await import_node_fs25.promises.readFile(file, "utf8");
|
|
10059
|
+
} catch (err) {
|
|
10060
|
+
if (err.code === "ENOENT") {
|
|
10061
|
+
return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
|
|
10062
|
+
}
|
|
10063
|
+
throw err;
|
|
10064
|
+
}
|
|
10065
|
+
await warnIfModeLooserThan0600(file);
|
|
10066
|
+
let parsed;
|
|
10067
|
+
try {
|
|
10068
|
+
parsed = JSON.parse(raw);
|
|
10069
|
+
} catch (err) {
|
|
10070
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
10071
|
+
}
|
|
10072
|
+
return validateConfig(parsed, file);
|
|
10073
|
+
}
|
|
10074
|
+
function validateConfig(parsed, file) {
|
|
10075
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10076
|
+
throw new Error(`${file} must be a JSON object with a "connectors" array`);
|
|
10077
|
+
}
|
|
10078
|
+
const obj = parsed;
|
|
10079
|
+
const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
|
|
10080
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
10081
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
10082
|
+
}
|
|
10083
|
+
const rawConnectors = obj.connectors;
|
|
10084
|
+
if (!Array.isArray(rawConnectors)) {
|
|
10085
|
+
throw new Error(`${file}: "connectors" must be an array`);
|
|
10086
|
+
}
|
|
10087
|
+
const connectors = rawConnectors.map((entry2, i) => validateEntry(entry2, i, file));
|
|
10088
|
+
return { version, connectors };
|
|
10089
|
+
}
|
|
10090
|
+
function validateEntry(entry2, index, file) {
|
|
10091
|
+
const where = `${file}: connectors[${index}]`;
|
|
10092
|
+
if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
|
|
10093
|
+
throw new Error(`${where} must be an object`);
|
|
10094
|
+
}
|
|
10095
|
+
const e = entry2;
|
|
10096
|
+
const id = e.id;
|
|
10097
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
10098
|
+
throw new Error(`${where}.id must be a non-empty string`);
|
|
10099
|
+
}
|
|
10100
|
+
const provider = e.provider;
|
|
10101
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
10102
|
+
throw new Error(`${where}.provider must be a non-empty string`);
|
|
10103
|
+
}
|
|
10104
|
+
if (e.project !== void 0 && typeof e.project !== "string") {
|
|
10105
|
+
throw new Error(`${where}.project must be a string when present`);
|
|
10106
|
+
}
|
|
10107
|
+
const credential = validateCredentialRef(e.credential, `${where}.credential`);
|
|
10108
|
+
let options;
|
|
10109
|
+
if (e.options !== void 0) {
|
|
10110
|
+
if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
|
|
10111
|
+
throw new Error(`${where}.options must be an object when present`);
|
|
10112
|
+
}
|
|
10113
|
+
options = e.options;
|
|
10114
|
+
}
|
|
10115
|
+
return {
|
|
10116
|
+
id,
|
|
10117
|
+
provider,
|
|
10118
|
+
...typeof e.project === "string" ? { project: e.project } : {},
|
|
10119
|
+
credential,
|
|
10120
|
+
...options ? { options } : {}
|
|
10121
|
+
};
|
|
10122
|
+
}
|
|
10123
|
+
function validateCredentialRef(raw, where) {
|
|
10124
|
+
if (typeof raw === "string") {
|
|
10125
|
+
if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
|
|
10126
|
+
return raw;
|
|
10127
|
+
}
|
|
10128
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
10129
|
+
const fields = raw;
|
|
10130
|
+
const keys = Object.keys(fields);
|
|
10131
|
+
if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
|
|
10132
|
+
for (const key of keys) {
|
|
10133
|
+
const v = fields[key];
|
|
10134
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
10135
|
+
throw new Error(`${where}.${key} must be a non-empty string`);
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10138
|
+
return fields;
|
|
10139
|
+
}
|
|
10140
|
+
throw new Error(`${where} must be a string or an object of strings`);
|
|
10141
|
+
}
|
|
10142
|
+
function resolveCredential(ref, env = process.env) {
|
|
10143
|
+
if (typeof ref === "string") {
|
|
10144
|
+
return { kind: "single", value: resolveRef(ref, env) };
|
|
10145
|
+
}
|
|
10146
|
+
const fields = {};
|
|
10147
|
+
for (const [key, value] of Object.entries(ref)) {
|
|
10148
|
+
fields[key] = resolveRef(value, env);
|
|
10149
|
+
}
|
|
10150
|
+
return { kind: "fields", fields };
|
|
10151
|
+
}
|
|
10152
|
+
function resolveRef(value, env) {
|
|
10153
|
+
if (value.length > 1 && value.startsWith("$")) {
|
|
10154
|
+
const varName = value.slice(1);
|
|
10155
|
+
const resolved = env[varName];
|
|
10156
|
+
if (resolved === void 0 || resolved.length === 0) {
|
|
10157
|
+
throw new EnvRefUnsetError(value, varName);
|
|
10158
|
+
}
|
|
10159
|
+
return resolved;
|
|
10160
|
+
}
|
|
10161
|
+
return value;
|
|
10162
|
+
}
|
|
10163
|
+
function connectorMatchesProject(entry2, project) {
|
|
10164
|
+
return entry2.project === void 0 || entry2.project === project;
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
// src/connectors/registry.ts
|
|
10168
|
+
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
10169
|
+
async function authProbe(input) {
|
|
10170
|
+
const { provider, accountKey, url, token, init, fetchImpl } = input;
|
|
10171
|
+
try {
|
|
10172
|
+
const res = await junctionFetch(
|
|
10173
|
+
url,
|
|
10174
|
+
{
|
|
10175
|
+
...init ?? {},
|
|
10176
|
+
headers: { ...bearerAuthHeader(token), ...init?.headers ?? {} }
|
|
10177
|
+
},
|
|
10178
|
+
{ provider, accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
10179
|
+
);
|
|
10180
|
+
if (res.ok) return { ok: true };
|
|
10181
|
+
if (res.status === 401 || res.status === 403) {
|
|
10182
|
+
return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` };
|
|
10183
|
+
}
|
|
10184
|
+
return {
|
|
10185
|
+
ok: false,
|
|
10186
|
+
reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} \u2014 could not confirm the credential`
|
|
10187
|
+
};
|
|
10188
|
+
} catch (err) {
|
|
10189
|
+
return {
|
|
10190
|
+
ok: false,
|
|
10191
|
+
reason: `${provider} auth check could not reach the provider: ${err.message}`
|
|
10192
|
+
};
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
var PROVIDER_DISPATCH = {
|
|
10196
|
+
supabase: {
|
|
10197
|
+
provider: "supabase",
|
|
10198
|
+
primaryCredentialKey: "managementToken",
|
|
10199
|
+
requiredCredentialFields: ["managementToken"],
|
|
10200
|
+
requiredOptionFields: ["apiProjectRef", "nodeRef", "serviceName"],
|
|
10201
|
+
build(graph, options) {
|
|
10202
|
+
return createSupabaseConnector(graph, options);
|
|
10203
|
+
},
|
|
10204
|
+
// GET /v1/projects — the Management API's own auth-gated list endpoint, the
|
|
10205
|
+
// cheapest confirmation the management token is live (the same surface
|
|
10206
|
+
// client.ts polls, minus the heavy log query).
|
|
10207
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10208
|
+
const cfg = options;
|
|
10209
|
+
const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
10210
|
+
return authProbe({
|
|
10211
|
+
provider: "supabase",
|
|
10212
|
+
accountKey: cfg.apiProjectRef ?? "validate",
|
|
10213
|
+
url: `${baseUrl}/v1/projects`,
|
|
10214
|
+
token: String(credentials.managementToken ?? ""),
|
|
10215
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10216
|
+
});
|
|
10217
|
+
}
|
|
10218
|
+
},
|
|
10219
|
+
railway: {
|
|
10220
|
+
provider: "railway",
|
|
10221
|
+
primaryCredentialKey: "token",
|
|
10222
|
+
requiredCredentialFields: ["token"],
|
|
10223
|
+
requiredOptionFields: ["environmentId", "serviceId", "serviceNameById"],
|
|
10224
|
+
build(graph, options) {
|
|
10225
|
+
const config = options;
|
|
10226
|
+
return {
|
|
10227
|
+
connector: createRailwayConnector(graph, config),
|
|
10228
|
+
resolveTarget: createRailwayResolveTarget(config)
|
|
10229
|
+
};
|
|
10230
|
+
},
|
|
10231
|
+
// A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call
|
|
10232
|
+
// at the HTTP layer, so a 2xx confirms the token was accepted regardless of
|
|
10233
|
+
// the query's own shape (the connector's real queries are still
|
|
10234
|
+
// needs-endpoint-testing per railway/types.ts — auth is what we check).
|
|
10235
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10236
|
+
const cfg = options;
|
|
10237
|
+
return authProbe({
|
|
10238
|
+
provider: "railway",
|
|
10239
|
+
accountKey: cfg.environmentId ?? "validate",
|
|
10240
|
+
url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
10241
|
+
token: String(credentials.token ?? ""),
|
|
10242
|
+
init: {
|
|
10243
|
+
method: "POST",
|
|
10244
|
+
headers: { "Content-Type": "application/json" },
|
|
10245
|
+
body: JSON.stringify({ query: "{ __typename }" })
|
|
10246
|
+
},
|
|
10247
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10248
|
+
});
|
|
10249
|
+
}
|
|
10250
|
+
},
|
|
10251
|
+
firebase: {
|
|
10252
|
+
provider: "firebase",
|
|
10253
|
+
// Firebase reads both projectId and accessToken from the credential; the
|
|
10254
|
+
// single-string form maps to the secret, and the required-fields check
|
|
10255
|
+
// below catches a projectId that was never supplied.
|
|
10256
|
+
primaryCredentialKey: "accessToken",
|
|
10257
|
+
requiredCredentialFields: ["projectId", "accessToken"],
|
|
10258
|
+
requiredOptionFields: [],
|
|
10259
|
+
build(graph, options) {
|
|
10260
|
+
return createFirebaseConnector(graph, options);
|
|
10261
|
+
},
|
|
10262
|
+
// GET the project's Cloud Logging log-name list (pageSize 1) — within the
|
|
10263
|
+
// same `roles/logging.viewer` grant the connector polls under, and the
|
|
10264
|
+
// lightest call that still fails 401/403 on a bad or wrong-scoped token.
|
|
10265
|
+
validate({ credentials, fetchImpl }) {
|
|
10266
|
+
const projectId = String(credentials.projectId ?? "");
|
|
10267
|
+
return authProbe({
|
|
10268
|
+
provider: "firebase",
|
|
10269
|
+
accountKey: projectId || "validate",
|
|
10270
|
+
url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,
|
|
10271
|
+
token: String(credentials.accessToken ?? ""),
|
|
10272
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10275
|
+
},
|
|
10276
|
+
cloudflare: {
|
|
10277
|
+
provider: "cloudflare",
|
|
10278
|
+
primaryCredentialKey: "apiToken",
|
|
10279
|
+
requiredCredentialFields: ["apiToken"],
|
|
10280
|
+
requiredOptionFields: ["accountId", "workers"],
|
|
10281
|
+
build(_graph, options) {
|
|
10282
|
+
const config = options;
|
|
10283
|
+
return {
|
|
10284
|
+
connector: new CloudflareConnector(config),
|
|
10285
|
+
resolveTarget: createCloudflareResolveTarget(config)
|
|
10286
|
+
};
|
|
10287
|
+
},
|
|
10288
|
+
// GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
|
|
10289
|
+
// token live" endpoint. 200 on a valid token, 401 on an invalid one.
|
|
10290
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10291
|
+
const cfg = options;
|
|
10292
|
+
const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
|
|
10293
|
+
return authProbe({
|
|
10294
|
+
provider: "cloudflare",
|
|
10295
|
+
accountKey: cfg.accountId ?? "validate",
|
|
10296
|
+
url: `${baseUrl}/user/tokens/verify`,
|
|
10297
|
+
token: String(credentials.apiToken ?? ""),
|
|
10298
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10299
|
+
});
|
|
10300
|
+
}
|
|
10301
|
+
}
|
|
10302
|
+
};
|
|
10303
|
+
function resolveEntryCredentials(dispatch, entry2, env) {
|
|
10304
|
+
let credentials;
|
|
10305
|
+
try {
|
|
10306
|
+
const resolved = resolveCredential(entry2.credential, env);
|
|
10307
|
+
credentials = resolved.kind === "single" ? { [dispatch.primaryCredentialKey]: resolved.value } : { ...resolved.fields };
|
|
10308
|
+
} catch (err) {
|
|
10309
|
+
if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
|
|
10310
|
+
return { ok: false, kind: "error", reason: err.message };
|
|
10311
|
+
}
|
|
10312
|
+
const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
|
|
10313
|
+
if (missingCreds.length > 0) {
|
|
10314
|
+
return {
|
|
10315
|
+
ok: false,
|
|
10316
|
+
kind: "missing-field",
|
|
10317
|
+
reason: `credential missing required field(s): ${missingCreds.join(", ")}`
|
|
10318
|
+
};
|
|
10319
|
+
}
|
|
10320
|
+
return { ok: true, credentials };
|
|
10321
|
+
}
|
|
10322
|
+
function buildRegistration(entry2, graph, env = process.env) {
|
|
10323
|
+
const dispatch = PROVIDER_DISPATCH[entry2.provider];
|
|
10324
|
+
if (!dispatch) {
|
|
10325
|
+
return { ok: false, reason: `unknown provider "${entry2.provider}"` };
|
|
10326
|
+
}
|
|
10327
|
+
const creds = resolveEntryCredentials(dispatch, entry2, env);
|
|
10328
|
+
if (!creds.ok) return { ok: false, reason: creds.reason };
|
|
10329
|
+
const credentials = creds.credentials;
|
|
10330
|
+
const options = entry2.options ?? {};
|
|
10331
|
+
const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
|
|
10332
|
+
if (missingOpts.length > 0) {
|
|
10333
|
+
return {
|
|
10334
|
+
ok: false,
|
|
10335
|
+
reason: `options missing required field(s): ${missingOpts.join(", ")}`
|
|
10336
|
+
};
|
|
10337
|
+
}
|
|
10338
|
+
let built;
|
|
10339
|
+
try {
|
|
10340
|
+
built = dispatch.build(graph, options);
|
|
10341
|
+
} catch (err) {
|
|
10342
|
+
return { ok: false, reason: err.message };
|
|
10343
|
+
}
|
|
10344
|
+
const intervalMs = typeof options.intervalMs === "number" ? options.intervalMs : void 0;
|
|
10345
|
+
return {
|
|
10346
|
+
ok: true,
|
|
10347
|
+
registration: {
|
|
10348
|
+
connector: built.connector,
|
|
10349
|
+
credentials,
|
|
10350
|
+
resolveTarget: built.resolveTarget,
|
|
10351
|
+
...intervalMs !== void 0 ? { intervalMs } : {}
|
|
10352
|
+
}
|
|
10353
|
+
};
|
|
10354
|
+
}
|
|
10355
|
+
async function loadConnectorRegistrations(input) {
|
|
10356
|
+
const { project, graph, home, env = process.env, onSkip } = input;
|
|
10357
|
+
let connectors;
|
|
10358
|
+
try {
|
|
10359
|
+
connectors = (await readConnectorsConfig(home)).connectors;
|
|
10360
|
+
} catch (err) {
|
|
10361
|
+
onSkip?.(
|
|
10362
|
+
{ id: "(file)", provider: "(all)", credential: "" },
|
|
10363
|
+
`connectors.json unreadable \u2014 ${err.message}`
|
|
10364
|
+
);
|
|
10365
|
+
return [];
|
|
10366
|
+
}
|
|
10367
|
+
const registrations = [];
|
|
10368
|
+
for (const entry2 of connectors) {
|
|
10369
|
+
if (!connectorMatchesProject(entry2, project)) continue;
|
|
10370
|
+
const result = buildRegistration(entry2, graph, env);
|
|
10371
|
+
if (result.ok) registrations.push(result.registration);
|
|
10372
|
+
else onSkip?.(entry2, result.reason);
|
|
10373
|
+
}
|
|
10374
|
+
return registrations;
|
|
10375
|
+
}
|
|
10376
|
+
|
|
8742
10377
|
// src/daemon.ts
|
|
8743
10378
|
init_auth();
|
|
8744
10379
|
|
|
8745
10380
|
// src/unrouted.ts
|
|
8746
10381
|
init_cjs_shims();
|
|
8747
|
-
var
|
|
8748
|
-
var
|
|
10382
|
+
var import_node_fs26 = require("fs");
|
|
10383
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
8749
10384
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
8750
10385
|
return {
|
|
8751
10386
|
timestamp: now.toISOString(),
|
|
@@ -8754,35 +10389,35 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
8754
10389
|
traceId: traceId ?? null
|
|
8755
10390
|
};
|
|
8756
10391
|
}
|
|
8757
|
-
async function appendUnroutedSpan(
|
|
8758
|
-
const target =
|
|
8759
|
-
await
|
|
8760
|
-
await
|
|
10392
|
+
async function appendUnroutedSpan(neatHome4, record) {
|
|
10393
|
+
const target = import_node_path45.default.join(neatHome4, "errors.ndjson");
|
|
10394
|
+
await import_node_fs26.promises.mkdir(neatHome4, { recursive: true });
|
|
10395
|
+
await import_node_fs26.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
8761
10396
|
}
|
|
8762
|
-
function unroutedErrorsPath(
|
|
8763
|
-
return
|
|
10397
|
+
function unroutedErrorsPath(neatHome4) {
|
|
10398
|
+
return import_node_path45.default.join(neatHome4, "errors.ndjson");
|
|
8764
10399
|
}
|
|
8765
10400
|
|
|
8766
10401
|
// src/daemon.ts
|
|
8767
|
-
var
|
|
10402
|
+
var import_types41 = require("@neat.is/types");
|
|
8768
10403
|
function daemonJsonPath(scanPath) {
|
|
8769
|
-
return
|
|
10404
|
+
return import_node_path46.default.join(scanPath, "neat-out", "daemon.json");
|
|
8770
10405
|
}
|
|
8771
10406
|
function daemonsDiscoveryDir(home) {
|
|
8772
10407
|
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
8773
|
-
return
|
|
10408
|
+
return import_node_path46.default.join(base, "daemons");
|
|
8774
10409
|
}
|
|
8775
10410
|
function daemonDiscoveryPath(project, home) {
|
|
8776
|
-
return
|
|
10411
|
+
return import_node_path46.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
8777
10412
|
}
|
|
8778
10413
|
function sanitizeDiscoveryName(project) {
|
|
8779
10414
|
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
8780
10415
|
}
|
|
8781
10416
|
function neatHomeFromEnv() {
|
|
8782
10417
|
const env = process.env.NEAT_HOME;
|
|
8783
|
-
if (env && env.length > 0) return
|
|
10418
|
+
if (env && env.length > 0) return import_node_path46.default.resolve(env);
|
|
8784
10419
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8785
|
-
return
|
|
10420
|
+
return import_node_path46.default.join(home, ".neat");
|
|
8786
10421
|
}
|
|
8787
10422
|
function resolveNeatVersion() {
|
|
8788
10423
|
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
@@ -8814,7 +10449,7 @@ async function clearDaemonRecord(record, home) {
|
|
|
8814
10449
|
} catch {
|
|
8815
10450
|
}
|
|
8816
10451
|
try {
|
|
8817
|
-
await
|
|
10452
|
+
await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
8818
10453
|
} catch {
|
|
8819
10454
|
}
|
|
8820
10455
|
}
|
|
@@ -8823,12 +10458,12 @@ function reconcileDaemonRecordSync(record, home) {
|
|
|
8823
10458
|
const stopped = { ...record, status: "stopped" };
|
|
8824
10459
|
const target = daemonJsonPath(record.projectPath);
|
|
8825
10460
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
8826
|
-
(0,
|
|
8827
|
-
(0,
|
|
10461
|
+
(0, import_node_fs27.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
|
|
10462
|
+
(0, import_node_fs27.renameSync)(tmp, target);
|
|
8828
10463
|
} catch {
|
|
8829
10464
|
}
|
|
8830
10465
|
try {
|
|
8831
|
-
(0,
|
|
10466
|
+
(0, import_node_fs27.unlinkSync)(daemonDiscoveryPath(record.project, home));
|
|
8832
10467
|
} catch {
|
|
8833
10468
|
}
|
|
8834
10469
|
}
|
|
@@ -8851,11 +10486,11 @@ function teardownSlot(slot) {
|
|
|
8851
10486
|
}
|
|
8852
10487
|
}
|
|
8853
10488
|
function neatHomeFor(opts) {
|
|
8854
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
10489
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path46.default.resolve(opts.neatHome);
|
|
8855
10490
|
const env = process.env.NEAT_HOME;
|
|
8856
|
-
if (env && env.length > 0) return
|
|
10491
|
+
if (env && env.length > 0) return import_node_path46.default.resolve(env);
|
|
8857
10492
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8858
|
-
return
|
|
10493
|
+
return import_node_path46.default.join(home, ".neat");
|
|
8859
10494
|
}
|
|
8860
10495
|
function routeSpanToProject(serviceName, projects) {
|
|
8861
10496
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -8899,13 +10534,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
8899
10534
|
if (!serviceName) return true;
|
|
8900
10535
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
8901
10536
|
return graph.someNode(
|
|
8902
|
-
(_id, attrs) => attrs.type ===
|
|
10537
|
+
(_id, attrs) => attrs.type === import_types41.NodeType.ServiceNode && attrs.name === serviceName
|
|
8903
10538
|
);
|
|
8904
10539
|
}
|
|
8905
|
-
async function bootstrapProject(entry2, connectors = []) {
|
|
8906
|
-
const paths = pathsForProject(entry2.name,
|
|
10540
|
+
async function bootstrapProject(entry2, connectors = [], neatHome4) {
|
|
10541
|
+
const paths = pathsForProject(entry2.name, import_node_path46.default.join(entry2.path, "neat-out"));
|
|
8907
10542
|
try {
|
|
8908
|
-
const stat = await
|
|
10543
|
+
const stat = await import_node_fs27.promises.stat(entry2.path);
|
|
8909
10544
|
if (!stat.isDirectory()) {
|
|
8910
10545
|
throw new Error(`registered path ${entry2.path} is not a directory`);
|
|
8911
10546
|
}
|
|
@@ -8943,7 +10578,16 @@ async function bootstrapProject(entry2, connectors = []) {
|
|
|
8943
10578
|
staleEventsPath: paths.staleEventsPath,
|
|
8944
10579
|
project: entry2.name
|
|
8945
10580
|
});
|
|
8946
|
-
const
|
|
10581
|
+
const fileConnectors = neatHome4 ? await loadConnectorRegistrations({
|
|
10582
|
+
project: entry2.name,
|
|
10583
|
+
graph,
|
|
10584
|
+
home: neatHome4,
|
|
10585
|
+
onSkip: (skipped, reason) => console.warn(
|
|
10586
|
+
`neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry2.name}" \u2014 ${reason}`
|
|
10587
|
+
)
|
|
10588
|
+
}) : [];
|
|
10589
|
+
const allConnectors = [...connectors, ...fileConnectors];
|
|
10590
|
+
const stopFns = allConnectors.map(
|
|
8947
10591
|
(registration) => startConnectorPollLoop(
|
|
8948
10592
|
registration.connector,
|
|
8949
10593
|
{ projectDir: entry2.path, credentials: registration.credentials },
|
|
@@ -9021,7 +10665,7 @@ async function startDaemon(opts = {}) {
|
|
|
9021
10665
|
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
10666
|
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
10667
|
const singleProject = projectArg;
|
|
9024
|
-
const singleProjectPath = singleProject && projectPathArg ?
|
|
10668
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path46.default.resolve(projectPathArg) : null;
|
|
9025
10669
|
if (singleProject && !singleProjectPath) {
|
|
9026
10670
|
throw new Error(
|
|
9027
10671
|
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
@@ -9029,14 +10673,14 @@ async function startDaemon(opts = {}) {
|
|
|
9029
10673
|
}
|
|
9030
10674
|
if (!singleProject) {
|
|
9031
10675
|
try {
|
|
9032
|
-
await
|
|
10676
|
+
await import_node_fs27.promises.access(regPath);
|
|
9033
10677
|
} catch {
|
|
9034
10678
|
throw new Error(
|
|
9035
10679
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
9036
10680
|
);
|
|
9037
10681
|
}
|
|
9038
10682
|
}
|
|
9039
|
-
const pidPath =
|
|
10683
|
+
const pidPath = import_node_path46.default.join(home, "neatd.pid");
|
|
9040
10684
|
await writeAtomically(pidPath, `${process.pid}
|
|
9041
10685
|
`);
|
|
9042
10686
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -9080,7 +10724,7 @@ async function startDaemon(opts = {}) {
|
|
|
9080
10724
|
}
|
|
9081
10725
|
async function tryRecoverSlot(entry2) {
|
|
9082
10726
|
try {
|
|
9083
|
-
const fresh = await bootstrapProject(entry2, opts.connectors ?? []);
|
|
10727
|
+
const fresh = await bootstrapProject(entry2, opts.connectors ?? [], home);
|
|
9084
10728
|
const prior = slots.get(entry2.name);
|
|
9085
10729
|
if (prior) teardownSlot(prior);
|
|
9086
10730
|
slots.set(entry2.name, fresh);
|
|
@@ -9104,7 +10748,7 @@ async function startDaemon(opts = {}) {
|
|
|
9104
10748
|
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
9105
10749
|
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
9106
10750
|
try {
|
|
9107
|
-
const slot = await bootstrapProject(entry2, opts.connectors ?? []);
|
|
10751
|
+
const slot = await bootstrapProject(entry2, opts.connectors ?? [], home);
|
|
9108
10752
|
const prior = slots.get(entry2.name);
|
|
9109
10753
|
if (prior) teardownSlot(prior);
|
|
9110
10754
|
slots.set(entry2.name, slot);
|
|
@@ -9230,7 +10874,7 @@ async function startDaemon(opts = {}) {
|
|
|
9230
10874
|
}
|
|
9231
10875
|
if (restApp) await restApp.close().catch(() => {
|
|
9232
10876
|
});
|
|
9233
|
-
await
|
|
10877
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9234
10878
|
});
|
|
9235
10879
|
throw new Error(
|
|
9236
10880
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
@@ -9356,7 +11000,7 @@ async function startDaemon(opts = {}) {
|
|
|
9356
11000
|
});
|
|
9357
11001
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
9358
11002
|
});
|
|
9359
|
-
await
|
|
11003
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9360
11004
|
});
|
|
9361
11005
|
throw new Error(
|
|
9362
11006
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
@@ -9391,7 +11035,7 @@ async function startDaemon(opts = {}) {
|
|
|
9391
11035
|
});
|
|
9392
11036
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
9393
11037
|
});
|
|
9394
|
-
await
|
|
11038
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9395
11039
|
});
|
|
9396
11040
|
throw new Error(
|
|
9397
11041
|
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
@@ -9438,9 +11082,9 @@ async function startDaemon(opts = {}) {
|
|
|
9438
11082
|
let registryWatcher = null;
|
|
9439
11083
|
let reloadTimer = null;
|
|
9440
11084
|
if (!singleProject) try {
|
|
9441
|
-
const regDir =
|
|
9442
|
-
const regBase =
|
|
9443
|
-
registryWatcher = (0,
|
|
11085
|
+
const regDir = import_node_path46.default.dirname(regPath);
|
|
11086
|
+
const regBase = import_node_path46.default.basename(regPath);
|
|
11087
|
+
registryWatcher = (0, import_node_fs27.watch)(regDir, (_eventType, filename) => {
|
|
9444
11088
|
if (filename !== null && filename !== regBase) return;
|
|
9445
11089
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
9446
11090
|
reloadTimer = setTimeout(() => {
|
|
@@ -9489,7 +11133,7 @@ async function startDaemon(opts = {}) {
|
|
|
9489
11133
|
if (daemonRecord) {
|
|
9490
11134
|
await clearDaemonRecord(daemonRecord, home);
|
|
9491
11135
|
}
|
|
9492
|
-
await
|
|
11136
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9493
11137
|
});
|
|
9494
11138
|
};
|
|
9495
11139
|
return {
|
|
@@ -9512,9 +11156,9 @@ init_auth();
|
|
|
9512
11156
|
// src/web-spawn.ts
|
|
9513
11157
|
init_cjs_shims();
|
|
9514
11158
|
var import_node_child_process2 = require("child_process");
|
|
9515
|
-
var
|
|
11159
|
+
var import_node_fs28 = require("fs");
|
|
9516
11160
|
var import_node_net = __toESM(require("net"), 1);
|
|
9517
|
-
var
|
|
11161
|
+
var import_node_path47 = __toESM(require("path"), 1);
|
|
9518
11162
|
var DEFAULT_WEB_PORT = 6328;
|
|
9519
11163
|
var DEFAULT_REST_PORT = 8080;
|
|
9520
11164
|
function asValidPort(value) {
|
|
@@ -9523,11 +11167,11 @@ function asValidPort(value) {
|
|
|
9523
11167
|
}
|
|
9524
11168
|
function projectRoot() {
|
|
9525
11169
|
const fromEnv = process.env.NEAT_SCAN_PATH;
|
|
9526
|
-
return
|
|
11170
|
+
return import_node_path47.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
|
|
9527
11171
|
}
|
|
9528
11172
|
async function readDaemonPorts(root) {
|
|
9529
11173
|
try {
|
|
9530
|
-
const raw = await
|
|
11174
|
+
const raw = await import_node_fs28.promises.readFile(import_node_path47.default.join(root, "neat-out", "daemon.json"), "utf8");
|
|
9531
11175
|
const parsed = JSON.parse(raw);
|
|
9532
11176
|
const ports = parsed?.ports ?? {};
|
|
9533
11177
|
return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
|
|
@@ -9572,10 +11216,10 @@ function resolveWebPackageDir() {
|
|
|
9572
11216
|
eval("require")
|
|
9573
11217
|
);
|
|
9574
11218
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
9575
|
-
return
|
|
11219
|
+
return import_node_path47.default.dirname(pkgJsonPath);
|
|
9576
11220
|
}
|
|
9577
11221
|
function resolveStandaloneServerEntry(webDir) {
|
|
9578
|
-
return
|
|
11222
|
+
return import_node_path47.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
9579
11223
|
}
|
|
9580
11224
|
async function pickInternalPort() {
|
|
9581
11225
|
return new Promise((resolve, reject) => {
|
|
@@ -9628,7 +11272,7 @@ async function spawnWebUI(restPort, opts = {}) {
|
|
|
9628
11272
|
NEAT_API_URL: apiUrl
|
|
9629
11273
|
};
|
|
9630
11274
|
child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
|
|
9631
|
-
cwd:
|
|
11275
|
+
cwd: import_node_path47.default.dirname(serverEntry),
|
|
9632
11276
|
env,
|
|
9633
11277
|
stdio: ["ignore", "inherit", "inherit"],
|
|
9634
11278
|
detached: false
|
|
@@ -9802,16 +11446,16 @@ function localVersion() {
|
|
|
9802
11446
|
return "0.0.0";
|
|
9803
11447
|
}
|
|
9804
11448
|
}
|
|
9805
|
-
function
|
|
11449
|
+
function neatHome3() {
|
|
9806
11450
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
9807
|
-
return
|
|
11451
|
+
return import_node_path48.default.resolve(process.env.NEAT_HOME);
|
|
9808
11452
|
}
|
|
9809
11453
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
9810
|
-
return
|
|
11454
|
+
return import_node_path48.default.join(home, ".neat");
|
|
9811
11455
|
}
|
|
9812
11456
|
async function readPid() {
|
|
9813
11457
|
try {
|
|
9814
|
-
const raw = await
|
|
11458
|
+
const raw = await import_node_fs29.promises.readFile(import_node_path48.default.join(neatHome3(), "neatd.pid"), "utf8");
|
|
9815
11459
|
const n = Number.parseInt(raw.trim(), 10);
|
|
9816
11460
|
return Number.isFinite(n) ? n : null;
|
|
9817
11461
|
} catch {
|