@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/index.cjs
CHANGED
|
@@ -54,15 +54,16 @@ function mountBearerAuth(app, opts) {
|
|
|
54
54
|
if (!opts.token || opts.token.length === 0) return;
|
|
55
55
|
if (opts.trustProxy) return;
|
|
56
56
|
const expected = Buffer.from(opts.token, "utf8");
|
|
57
|
-
const
|
|
57
|
+
const exactUnauthPaths = /* @__PURE__ */ new Set([
|
|
58
|
+
...DEFAULT_UNAUTH_PATHS,
|
|
59
|
+
...opts.extraUnauthenticatedSuffixes ?? []
|
|
60
|
+
]);
|
|
58
61
|
const publicRead = opts.publicRead === true;
|
|
59
62
|
app.addHook("preHandler", (req, reply, done) => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
63
|
+
const path47 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
64
|
+
if (exactUnauthPaths.has(path47) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path47)) {
|
|
65
|
+
done();
|
|
66
|
+
return;
|
|
66
67
|
}
|
|
67
68
|
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
68
69
|
done();
|
|
@@ -97,7 +98,7 @@ function readAuthEnv(env = process.env) {
|
|
|
97
98
|
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS,
|
|
101
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
|
|
101
102
|
var init_auth = __esm({
|
|
102
103
|
"src/auth.ts"() {
|
|
103
104
|
"use strict";
|
|
@@ -118,12 +119,15 @@ var init_auth = __esm({
|
|
|
118
119
|
}
|
|
119
120
|
};
|
|
120
121
|
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
121
|
-
|
|
122
|
+
DEFAULT_UNAUTH_PATHS = [
|
|
122
123
|
"/health",
|
|
123
124
|
"/healthz",
|
|
124
125
|
"/readyz",
|
|
125
126
|
"/api/config"
|
|
126
127
|
];
|
|
128
|
+
PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
|
|
129
|
+
`^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
|
|
130
|
+
);
|
|
127
131
|
}
|
|
128
132
|
});
|
|
129
133
|
|
|
@@ -357,8 +361,8 @@ function websocketChannelPathOf(attrs) {
|
|
|
357
361
|
const v = attrs[key];
|
|
358
362
|
if (typeof v === "string" && v.length > 0) {
|
|
359
363
|
const q = v.indexOf("?");
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
364
|
+
const path47 = q === -1 ? v : v.slice(0, q);
|
|
365
|
+
if (path47.length > 0) return path47;
|
|
362
366
|
}
|
|
363
367
|
}
|
|
364
368
|
return void 0;
|
|
@@ -1246,19 +1250,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1246
1250
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1247
1251
|
let best = { path: [start], edges: [] };
|
|
1248
1252
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1249
|
-
function step(node,
|
|
1250
|
-
if (
|
|
1251
|
-
best = { path: [...
|
|
1253
|
+
function step(node, path47, edges) {
|
|
1254
|
+
if (path47.length > best.path.length) {
|
|
1255
|
+
best = { path: [...path47], edges: [...edges] };
|
|
1252
1256
|
}
|
|
1253
|
-
if (
|
|
1257
|
+
if (path47.length - 1 >= maxDepth) return;
|
|
1254
1258
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1255
1259
|
for (const [srcId, edge] of incoming) {
|
|
1256
1260
|
if (visited.has(srcId)) continue;
|
|
1257
1261
|
visited.add(srcId);
|
|
1258
|
-
|
|
1262
|
+
path47.push(srcId);
|
|
1259
1263
|
edges.push(edge);
|
|
1260
|
-
step(srcId,
|
|
1261
|
-
|
|
1264
|
+
step(srcId, path47, edges);
|
|
1265
|
+
path47.pop();
|
|
1262
1266
|
edges.pop();
|
|
1263
1267
|
visited.delete(srcId);
|
|
1264
1268
|
}
|
|
@@ -1273,7 +1277,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
|
|
|
1273
1277
|
for (const id of walk3.path) {
|
|
1274
1278
|
const owner = resolveOwningService(graph, id);
|
|
1275
1279
|
if (!owner) continue;
|
|
1276
|
-
const { id:
|
|
1280
|
+
const { id: serviceId5, svc } = owner;
|
|
1277
1281
|
const deps = svc.dependencies ?? {};
|
|
1278
1282
|
for (const pair of candidatePairs) {
|
|
1279
1283
|
const declared = deps[pair.driver];
|
|
@@ -1286,7 +1290,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
|
|
|
1286
1290
|
);
|
|
1287
1291
|
if (!result.compatible) {
|
|
1288
1292
|
return {
|
|
1289
|
-
rootCauseNode:
|
|
1293
|
+
rootCauseNode: serviceId5,
|
|
1290
1294
|
rootCauseReason: result.reason ?? "incompatible driver",
|
|
1291
1295
|
...result.minDriverVersion ? {
|
|
1292
1296
|
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
@@ -1301,7 +1305,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1301
1305
|
for (const id of walk3.path) {
|
|
1302
1306
|
const owner = resolveOwningService(graph, id);
|
|
1303
1307
|
if (!owner) continue;
|
|
1304
|
-
const { id:
|
|
1308
|
+
const { id: serviceId5, svc } = owner;
|
|
1305
1309
|
const deps = svc.dependencies ?? {};
|
|
1306
1310
|
const serviceNodeEngine = svc.nodeEngine;
|
|
1307
1311
|
for (const constraint of nodeEngineConstraints()) {
|
|
@@ -1310,7 +1314,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1310
1314
|
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
1311
1315
|
if (!result.compatible && result.reason) {
|
|
1312
1316
|
return {
|
|
1313
|
-
rootCauseNode:
|
|
1317
|
+
rootCauseNode: serviceId5,
|
|
1314
1318
|
rootCauseReason: result.reason,
|
|
1315
1319
|
...result.requiredNodeVersion ? {
|
|
1316
1320
|
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
@@ -1325,7 +1329,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1325
1329
|
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
1326
1330
|
if (!result.compatible && result.reason) {
|
|
1327
1331
|
return {
|
|
1328
|
-
rootCauseNode:
|
|
1332
|
+
rootCauseNode: serviceId5,
|
|
1329
1333
|
rootCauseReason: result.reason,
|
|
1330
1334
|
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
1331
1335
|
};
|
|
@@ -1416,9 +1420,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
1416
1420
|
function isFailingCallEdge(e) {
|
|
1417
1421
|
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1418
1422
|
}
|
|
1419
|
-
function callSourcesForService(graph,
|
|
1420
|
-
const ids = [
|
|
1421
|
-
for (const edgeId of graph.outboundEdges(
|
|
1423
|
+
function callSourcesForService(graph, serviceId5) {
|
|
1424
|
+
const ids = [serviceId5];
|
|
1425
|
+
for (const edgeId of graph.outboundEdges(serviceId5)) {
|
|
1422
1426
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1423
1427
|
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1424
1428
|
const tgt = graph.getNodeAttributes(e.target);
|
|
@@ -1435,9 +1439,9 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
1435
1439
|
}
|
|
1436
1440
|
return id < curId;
|
|
1437
1441
|
}
|
|
1438
|
-
function dominantFailingCall(graph,
|
|
1442
|
+
function dominantFailingCall(graph, serviceId5, visited) {
|
|
1439
1443
|
let best = null;
|
|
1440
|
-
for (const src of callSourcesForService(graph,
|
|
1444
|
+
for (const src of callSourcesForService(graph, serviceId5)) {
|
|
1441
1445
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1442
1446
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1443
1447
|
if (!isFailingCallEdge(e)) continue;
|
|
@@ -1452,26 +1456,26 @@ function dominantFailingCall(graph, serviceId4, visited) {
|
|
|
1452
1456
|
return best;
|
|
1453
1457
|
}
|
|
1454
1458
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1455
|
-
const
|
|
1459
|
+
const path47 = [originServiceId];
|
|
1456
1460
|
const edges = [];
|
|
1457
1461
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1458
1462
|
let current = originServiceId;
|
|
1459
1463
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1460
1464
|
const hop = dominantFailingCall(graph, current, visited);
|
|
1461
1465
|
if (!hop) break;
|
|
1462
|
-
|
|
1466
|
+
path47.push(hop.nextService);
|
|
1463
1467
|
edges.push(hop.edge);
|
|
1464
1468
|
visited.add(hop.nextService);
|
|
1465
1469
|
current = hop.nextService;
|
|
1466
1470
|
}
|
|
1467
1471
|
if (edges.length === 0) return null;
|
|
1468
|
-
return { path:
|
|
1472
|
+
return { path: path47, edges, culprit: current };
|
|
1469
1473
|
}
|
|
1470
1474
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1471
1475
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1472
1476
|
if (!chain) return null;
|
|
1473
1477
|
const culprit = chain.culprit;
|
|
1474
|
-
const
|
|
1478
|
+
const path47 = [...chain.path];
|
|
1475
1479
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1476
1480
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1477
1481
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -1479,14 +1483,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1479
1483
|
if (loc) {
|
|
1480
1484
|
let rootCauseNode = culprit;
|
|
1481
1485
|
if (loc.fileNode) {
|
|
1482
|
-
|
|
1486
|
+
path47.push(loc.fileNode);
|
|
1483
1487
|
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1484
1488
|
rootCauseNode = loc.fileNode;
|
|
1485
1489
|
}
|
|
1486
1490
|
return import_types.RootCauseResultSchema.parse({
|
|
1487
1491
|
rootCauseNode,
|
|
1488
1492
|
rootCauseReason: loc.rootCauseReason,
|
|
1489
|
-
traversalPath:
|
|
1493
|
+
traversalPath: path47,
|
|
1490
1494
|
edgeProvenances,
|
|
1491
1495
|
confidence,
|
|
1492
1496
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -1498,7 +1502,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1498
1502
|
return import_types.RootCauseResultSchema.parse({
|
|
1499
1503
|
rootCauseNode: culprit,
|
|
1500
1504
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1501
|
-
traversalPath:
|
|
1505
|
+
traversalPath: path47,
|
|
1502
1506
|
edgeProvenances,
|
|
1503
1507
|
confidence,
|
|
1504
1508
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -3054,29 +3058,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
3054
3058
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
3055
3059
|
});
|
|
3056
3060
|
let promoted = 0;
|
|
3057
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
3061
|
+
for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
|
|
3058
3062
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
3059
3063
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
3060
3064
|
if (!gate.allowed) {
|
|
3061
3065
|
continue;
|
|
3062
3066
|
}
|
|
3063
3067
|
}
|
|
3064
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
3068
|
+
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
3065
3069
|
graph.dropNode(frontierId2);
|
|
3066
3070
|
promoted++;
|
|
3067
3071
|
}
|
|
3068
3072
|
return promoted;
|
|
3069
3073
|
}
|
|
3070
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
3074
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId5) {
|
|
3071
3075
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
3072
3076
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
3073
3077
|
for (const edgeId of inbound) {
|
|
3074
3078
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3075
|
-
rebuildEdge(graph, edge, edge.source,
|
|
3079
|
+
rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
|
|
3076
3080
|
}
|
|
3077
3081
|
for (const edgeId of outbound) {
|
|
3078
3082
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3079
|
-
rebuildEdge(graph, edge,
|
|
3083
|
+
rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
|
|
3080
3084
|
}
|
|
3081
3085
|
}
|
|
3082
3086
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -3227,24 +3231,58 @@ function dedupeIncidents(events) {
|
|
|
3227
3231
|
return !hasRealFailure.has(groupKey(ev));
|
|
3228
3232
|
});
|
|
3229
3233
|
}
|
|
3234
|
+
var SnapshotValidationError = class extends Error {
|
|
3235
|
+
constructor(issues) {
|
|
3236
|
+
super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
|
|
3237
|
+
this.issues = issues;
|
|
3238
|
+
this.name = "SnapshotValidationError";
|
|
3239
|
+
}
|
|
3240
|
+
issues;
|
|
3241
|
+
};
|
|
3242
|
+
function describeZodIssues(error) {
|
|
3243
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
3244
|
+
}
|
|
3230
3245
|
function mergeSnapshot(graph, snapshot) {
|
|
3231
3246
|
const exported = snapshot.graph;
|
|
3247
|
+
const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
|
|
3248
|
+
const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
|
|
3249
|
+
const issues = [];
|
|
3250
|
+
const validNodes = [];
|
|
3251
|
+
const validEdges = [];
|
|
3252
|
+
for (const node of incomingNodes) {
|
|
3253
|
+
if (node.attributes === void 0) continue;
|
|
3254
|
+
const parsed = import_types3.GraphNodeSchema.safeParse(node.attributes);
|
|
3255
|
+
if (!parsed.success) {
|
|
3256
|
+
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
validNodes.push({ key: node.key, attributes: parsed.data });
|
|
3260
|
+
}
|
|
3261
|
+
for (const edge of incomingEdges) {
|
|
3262
|
+
if (edge.attributes === void 0) continue;
|
|
3263
|
+
const parsed = import_types3.GraphEdgeSchema.safeParse(edge.attributes);
|
|
3264
|
+
if (!parsed.success) {
|
|
3265
|
+
const label = edge.key ?? `${edge.source}->${edge.target}`;
|
|
3266
|
+
issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
const id = edge.key ?? parsed.data.id;
|
|
3270
|
+
validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
|
|
3271
|
+
}
|
|
3272
|
+
if (issues.length > 0) {
|
|
3273
|
+
throw new SnapshotValidationError(issues);
|
|
3274
|
+
}
|
|
3232
3275
|
let nodesAdded = 0;
|
|
3233
3276
|
let edgesAdded = 0;
|
|
3234
|
-
for (const node of
|
|
3277
|
+
for (const node of validNodes) {
|
|
3235
3278
|
if (graph.hasNode(node.key)) continue;
|
|
3236
|
-
if (!node.attributes) continue;
|
|
3237
3279
|
graph.addNode(node.key, node.attributes);
|
|
3238
3280
|
nodesAdded++;
|
|
3239
3281
|
}
|
|
3240
|
-
for (const edge of
|
|
3241
|
-
|
|
3242
|
-
if (!attrs) continue;
|
|
3243
|
-
const id = edge.key ?? attrs.id;
|
|
3244
|
-
if (!id) continue;
|
|
3245
|
-
if (graph.hasEdge(id)) continue;
|
|
3282
|
+
for (const edge of validEdges) {
|
|
3283
|
+
if (graph.hasEdge(edge.key)) continue;
|
|
3246
3284
|
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
3247
|
-
graph.addEdgeWithKey(
|
|
3285
|
+
graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
|
|
3248
3286
|
edgesAdded++;
|
|
3249
3287
|
}
|
|
3250
3288
|
return { nodesAdded, edgesAdded };
|
|
@@ -3838,9 +3876,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
3838
3876
|
"StatefulSet",
|
|
3839
3877
|
"DaemonSet"
|
|
3840
3878
|
]);
|
|
3841
|
-
function addAliases(graph,
|
|
3842
|
-
if (!graph.hasNode(
|
|
3843
|
-
const node = graph.getNodeAttributes(
|
|
3879
|
+
function addAliases(graph, serviceId5, candidates) {
|
|
3880
|
+
if (!graph.hasNode(serviceId5)) return;
|
|
3881
|
+
const node = graph.getNodeAttributes(serviceId5);
|
|
3844
3882
|
if (node.type !== import_types6.NodeType.ServiceNode) return;
|
|
3845
3883
|
const set = new Set(node.aliases ?? []);
|
|
3846
3884
|
for (const c of candidates) {
|
|
@@ -3850,7 +3888,7 @@ function addAliases(graph, serviceId4, candidates) {
|
|
|
3850
3888
|
}
|
|
3851
3889
|
if (set.size === 0) return;
|
|
3852
3890
|
const updated = { ...node, aliases: [...set].sort() };
|
|
3853
|
-
graph.replaceNodeAttributes(
|
|
3891
|
+
graph.replaceNodeAttributes(serviceId5, updated);
|
|
3854
3892
|
}
|
|
3855
3893
|
function indexServicesByName(services) {
|
|
3856
3894
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3883,12 +3921,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
3883
3921
|
}
|
|
3884
3922
|
if (!compose?.services) return;
|
|
3885
3923
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3886
|
-
const
|
|
3887
|
-
if (!
|
|
3924
|
+
const serviceId5 = serviceIndex.get(composeName);
|
|
3925
|
+
if (!serviceId5) continue;
|
|
3888
3926
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
3889
3927
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
3890
3928
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
3891
|
-
addAliases(graph,
|
|
3929
|
+
addAliases(graph, serviceId5, aliases);
|
|
3892
3930
|
}
|
|
3893
3931
|
}
|
|
3894
3932
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -7352,13 +7390,13 @@ function bucketKey(source, target, type) {
|
|
|
7352
7390
|
return `${type}|${source}|${target}`;
|
|
7353
7391
|
}
|
|
7354
7392
|
function bucketEdges(graph) {
|
|
7355
|
-
const
|
|
7393
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
7356
7394
|
graph.forEachEdge((id, attrs) => {
|
|
7357
7395
|
const e = attrs;
|
|
7358
7396
|
const parsed = (0, import_types27.parseEdgeId)(id);
|
|
7359
7397
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
7360
7398
|
const key = bucketKey(e.source, e.target, e.type);
|
|
7361
|
-
const cur =
|
|
7399
|
+
const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
7362
7400
|
switch (provenance) {
|
|
7363
7401
|
case import_types27.Provenance.EXTRACTED:
|
|
7364
7402
|
cur.extracted = e;
|
|
@@ -7372,9 +7410,9 @@ function bucketEdges(graph) {
|
|
|
7372
7410
|
default:
|
|
7373
7411
|
if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
|
|
7374
7412
|
}
|
|
7375
|
-
|
|
7413
|
+
buckets2.set(key, cur);
|
|
7376
7414
|
});
|
|
7377
|
-
return
|
|
7415
|
+
return buckets2;
|
|
7378
7416
|
}
|
|
7379
7417
|
function nodeIsFrontier(graph, nodeId) {
|
|
7380
7418
|
if (!graph.hasNode(nodeId)) return false;
|
|
@@ -7565,8 +7603,8 @@ function suppressHostMismatchHalves(all) {
|
|
|
7565
7603
|
}
|
|
7566
7604
|
function computeDivergences(graph, opts = {}) {
|
|
7567
7605
|
const all = [];
|
|
7568
|
-
const
|
|
7569
|
-
for (const bucket of
|
|
7606
|
+
const buckets2 = bucketEdges(graph);
|
|
7607
|
+
for (const bucket of buckets2.values()) {
|
|
7570
7608
|
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
7571
7609
|
}
|
|
7572
7610
|
graph.forEachNode((nodeId, attrs) => {
|
|
@@ -7612,6 +7650,51 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7612
7650
|
});
|
|
7613
7651
|
}
|
|
7614
7652
|
|
|
7653
|
+
// src/logs-store.ts
|
|
7654
|
+
init_cjs_shims();
|
|
7655
|
+
var LOGS_STORE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7656
|
+
var LOGS_QUERY_DEFAULT_LIMIT = 100;
|
|
7657
|
+
var LOGS_QUERY_MAX_LIMIT = 1e3;
|
|
7658
|
+
var buffers = /* @__PURE__ */ new Map();
|
|
7659
|
+
var KEY_SEP = "::";
|
|
7660
|
+
function bufferKey(projectName, source) {
|
|
7661
|
+
return `${projectName}${KEY_SEP}${source}`;
|
|
7662
|
+
}
|
|
7663
|
+
function sourcesForProject(projectName) {
|
|
7664
|
+
const prefix = `${projectName}${KEY_SEP}`;
|
|
7665
|
+
const sources = [];
|
|
7666
|
+
for (const key of buffers.keys()) {
|
|
7667
|
+
if (key.startsWith(prefix)) sources.push(key.slice(prefix.length));
|
|
7668
|
+
}
|
|
7669
|
+
return sources;
|
|
7670
|
+
}
|
|
7671
|
+
function queryLogEntries(opts) {
|
|
7672
|
+
const { projectName, source, service, since, limit } = opts;
|
|
7673
|
+
const sources = source && source.length > 0 ? source : sourcesForProject(projectName);
|
|
7674
|
+
let merged = [];
|
|
7675
|
+
for (const src of sources) {
|
|
7676
|
+
const buf = buffers.get(bufferKey(projectName, src));
|
|
7677
|
+
if (buf) merged = merged.concat(buf);
|
|
7678
|
+
}
|
|
7679
|
+
if (service) {
|
|
7680
|
+
merged = merged.filter((e) => e.serviceName === service);
|
|
7681
|
+
}
|
|
7682
|
+
if (since) {
|
|
7683
|
+
const sinceMs = Date.parse(since);
|
|
7684
|
+
if (!Number.isNaN(sinceMs)) {
|
|
7685
|
+
merged = merged.filter((e) => {
|
|
7686
|
+
const t = Date.parse(e.timestamp);
|
|
7687
|
+
return Number.isNaN(t) || t >= sinceMs;
|
|
7688
|
+
});
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
7692
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
|
|
7693
|
+
return merged.slice(0, limit);
|
|
7694
|
+
}
|
|
7695
|
+
return merged;
|
|
7696
|
+
}
|
|
7697
|
+
|
|
7615
7698
|
// src/diff.ts
|
|
7616
7699
|
init_cjs_shims();
|
|
7617
7700
|
var import_node_fs23 = require("fs");
|
|
@@ -8291,6 +8374,23 @@ function registerRoutes(scope, ctx) {
|
|
|
8291
8374
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
8292
8375
|
return { count: sliced.length, total, events: sliced };
|
|
8293
8376
|
});
|
|
8377
|
+
scope.get("/logs", async (req, reply) => {
|
|
8378
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8379
|
+
if (!proj) return;
|
|
8380
|
+
const rawSource = req.query.source;
|
|
8381
|
+
const sources = rawSource === void 0 ? void 0 : Array.isArray(rawSource) ? rawSource : [rawSource];
|
|
8382
|
+
const filtered = queryLogEntries({
|
|
8383
|
+
projectName: proj.name,
|
|
8384
|
+
...sources && sources.length > 0 ? { source: sources } : {},
|
|
8385
|
+
...req.query.service ? { service: req.query.service } : {},
|
|
8386
|
+
...req.query.since ? { since: req.query.since } : {}
|
|
8387
|
+
});
|
|
8388
|
+
const total = filtered.length;
|
|
8389
|
+
const limit = req.query.limit ? Number(req.query.limit) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8390
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, LOGS_QUERY_MAX_LIMIT) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8391
|
+
const sliced = filtered.slice(0, safeLimit);
|
|
8392
|
+
return { count: sliced.length, total, logs: sliced };
|
|
8393
|
+
});
|
|
8294
8394
|
const incidentHistoryHandler = async (req, reply) => {
|
|
8295
8395
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8296
8396
|
if (!proj) return;
|
|
@@ -8386,8 +8486,16 @@ function registerRoutes(scope, ctx) {
|
|
|
8386
8486
|
if (!against) {
|
|
8387
8487
|
return reply.code(400).send({ error: "query parameter `against` is required" });
|
|
8388
8488
|
}
|
|
8489
|
+
const targetProjectName = against === "self" ? proj.name : against;
|
|
8490
|
+
const targetProject = registry.get(targetProjectName);
|
|
8491
|
+
if (!targetProject) {
|
|
8492
|
+
return reply.code(400).send({
|
|
8493
|
+
error: "unknown snapshot id \u2014 `against` must be `self` or the name of a project this server has a snapshot for",
|
|
8494
|
+
against
|
|
8495
|
+
});
|
|
8496
|
+
}
|
|
8389
8497
|
try {
|
|
8390
|
-
const snapshot = await loadSnapshotForDiff(
|
|
8498
|
+
const snapshot = await loadSnapshotForDiff(targetProject.paths.snapshotPath);
|
|
8391
8499
|
return computeGraphDiff(proj.graph, snapshot);
|
|
8392
8500
|
} catch (err) {
|
|
8393
8501
|
return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
|
|
@@ -8417,6 +8525,13 @@ function registerRoutes(scope, ctx) {
|
|
|
8417
8525
|
edgeCount: proj.graph.size
|
|
8418
8526
|
};
|
|
8419
8527
|
} catch (err) {
|
|
8528
|
+
if (err instanceof SnapshotValidationError) {
|
|
8529
|
+
return reply.code(400).send({
|
|
8530
|
+
error: "snapshot merge failed",
|
|
8531
|
+
details: err.message,
|
|
8532
|
+
issues: err.issues
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8420
8535
|
return reply.code(400).send({
|
|
8421
8536
|
error: "snapshot merge failed",
|
|
8422
8537
|
details: err.message
|
|
@@ -8763,8 +8878,8 @@ init_otel_grpc();
|
|
|
8763
8878
|
|
|
8764
8879
|
// src/daemon.ts
|
|
8765
8880
|
init_cjs_shims();
|
|
8766
|
-
var
|
|
8767
|
-
var
|
|
8881
|
+
var import_node_fs27 = require("fs");
|
|
8882
|
+
var import_node_path46 = __toESM(require("path"), 1);
|
|
8768
8883
|
var import_node_module = require("module");
|
|
8769
8884
|
init_otel();
|
|
8770
8885
|
|
|
@@ -8845,13 +8960,1533 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
|
|
|
8845
8960
|
};
|
|
8846
8961
|
}
|
|
8847
8962
|
|
|
8963
|
+
// src/connectors/registry.ts
|
|
8964
|
+
init_cjs_shims();
|
|
8965
|
+
|
|
8966
|
+
// src/connectors/junction.ts
|
|
8967
|
+
init_cjs_shims();
|
|
8968
|
+
var buckets = /* @__PURE__ */ new Map();
|
|
8969
|
+
function bucketMapKey(provider, accountKey) {
|
|
8970
|
+
return `${provider}\0${accountKey}`;
|
|
8971
|
+
}
|
|
8972
|
+
function getBucket(provider, accountKey, config) {
|
|
8973
|
+
const key = bucketMapKey(provider, accountKey);
|
|
8974
|
+
const existing = buckets.get(key);
|
|
8975
|
+
if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {
|
|
8976
|
+
return existing;
|
|
8977
|
+
}
|
|
8978
|
+
const fresh = { ...config, tokens: config.capacity, updatedAt: Date.now() };
|
|
8979
|
+
buckets.set(key, fresh);
|
|
8980
|
+
return fresh;
|
|
8981
|
+
}
|
|
8982
|
+
function refillBucket(bucket, now) {
|
|
8983
|
+
if (now <= bucket.updatedAt) return;
|
|
8984
|
+
const elapsed = now - bucket.updatedAt;
|
|
8985
|
+
const grant = elapsed / bucket.refillMs;
|
|
8986
|
+
if (grant <= 0) return;
|
|
8987
|
+
bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant);
|
|
8988
|
+
bucket.updatedAt = now;
|
|
8989
|
+
}
|
|
8990
|
+
var RateLimitExceededError = class extends Error {
|
|
8991
|
+
constructor(provider, accountKey) {
|
|
8992
|
+
super(
|
|
8993
|
+
`junction: rate limit exceeded for ${provider}:${accountKey} \u2014 waiting for the next token would exceed this call's wall-clock budget`
|
|
8994
|
+
);
|
|
8995
|
+
this.name = "RateLimitExceededError";
|
|
8996
|
+
}
|
|
8997
|
+
};
|
|
8998
|
+
function delay(ms) {
|
|
8999
|
+
if (ms <= 0) return Promise.resolve();
|
|
9000
|
+
return new Promise((resolve) => {
|
|
9001
|
+
const timer = setTimeout(resolve, ms);
|
|
9002
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9003
|
+
});
|
|
9004
|
+
}
|
|
9005
|
+
async function acquireToken(provider, accountKey, config, remainingBudgetMs) {
|
|
9006
|
+
const bucket = getBucket(provider, accountKey, config);
|
|
9007
|
+
refillBucket(bucket, Date.now());
|
|
9008
|
+
if (bucket.tokens >= 1) {
|
|
9009
|
+
bucket.tokens -= 1;
|
|
9010
|
+
return 0;
|
|
9011
|
+
}
|
|
9012
|
+
const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs);
|
|
9013
|
+
if (waitMs > remainingBudgetMs) {
|
|
9014
|
+
throw new RateLimitExceededError(provider, accountKey);
|
|
9015
|
+
}
|
|
9016
|
+
await delay(waitMs);
|
|
9017
|
+
refillBucket(bucket, Date.now());
|
|
9018
|
+
bucket.tokens = Math.max(0, bucket.tokens - 1);
|
|
9019
|
+
return waitMs;
|
|
9020
|
+
}
|
|
9021
|
+
var JUNCTION_DEFAULT_RATE_LIMITS = {
|
|
9022
|
+
// ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of
|
|
9023
|
+
// that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)
|
|
9024
|
+
// stays well clear of the documented limit even under sustained polling.
|
|
9025
|
+
cloudflare: { capacity: 100, refillMs: 3e3 },
|
|
9026
|
+
// Placeholder pending a live project confirming the real cap
|
|
9027
|
+
// (docs/connectors/railway.md: "does not appear to publish one as of this
|
|
9028
|
+
// writing").
|
|
9029
|
+
railway: { capacity: 30, refillMs: 1e4 },
|
|
9030
|
+
// Placeholder pending a live rate-limit check (docs/connectors/
|
|
9031
|
+
// firebase.md: "needs-endpoint-testing against entries.list's live rate
|
|
9032
|
+
// limits").
|
|
9033
|
+
firebase: { capacity: 30, refillMs: 1e4 },
|
|
9034
|
+
// Placeholder pending a live rate-limit check (docs/connectors/supabase.md:
|
|
9035
|
+
// "the documented rate limit for this specific endpoint is unconfirmed").
|
|
9036
|
+
supabase: { capacity: 30, refillMs: 1e4 },
|
|
9037
|
+
// Not a documented API limit at all — a self-imposed ceiling on the raw
|
|
9038
|
+
// pg_stat_statements connection (see module header above).
|
|
9039
|
+
"supabase-postgres": { capacity: 20, refillMs: 3e3 }
|
|
9040
|
+
};
|
|
9041
|
+
var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
|
|
9042
|
+
function defaultRateLimitFor(provider) {
|
|
9043
|
+
return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT;
|
|
9044
|
+
}
|
|
9045
|
+
var JUNCTION_DEFAULT_TIMEOUT_MS = 1e4;
|
|
9046
|
+
var JUNCTION_DEFAULT_MAX_ATTEMPTS = 3;
|
|
9047
|
+
var JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200;
|
|
9048
|
+
var JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4;
|
|
9049
|
+
var JUNCTION_DEFAULT_MAX_ELAPSED_MS = 3e4;
|
|
9050
|
+
var JUNCTION_DEFAULT_DB_TIMEOUT_MS = 1e4;
|
|
9051
|
+
async function backoff(attempt, initialBackoffMs, backoffMultiplier, remainingBudgetMs) {
|
|
9052
|
+
const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1);
|
|
9053
|
+
const capped = Math.max(0, Math.min(raw, remainingBudgetMs));
|
|
9054
|
+
await delay(capped);
|
|
9055
|
+
}
|
|
9056
|
+
function safeUrlLabel(url) {
|
|
9057
|
+
try {
|
|
9058
|
+
const u = typeof url === "string" ? new URL(url) : url;
|
|
9059
|
+
return `${u.origin}${u.pathname}`;
|
|
9060
|
+
} catch {
|
|
9061
|
+
return String(url);
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
function logOutcome(provider, accountKey, outcome, method, label, attempt, startedAt) {
|
|
9065
|
+
const elapsedMs = Date.now() - startedAt;
|
|
9066
|
+
const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} \u2014 ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`;
|
|
9067
|
+
if (outcome === "success" || outcome === "retried-then-succeeded") {
|
|
9068
|
+
console.log(line);
|
|
9069
|
+
} else if (outcome === "rate-limited") {
|
|
9070
|
+
console.warn(line);
|
|
9071
|
+
} else {
|
|
9072
|
+
console.error(line);
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
function bearerAuthHeader(token) {
|
|
9076
|
+
return { Authorization: `Bearer ${token}` };
|
|
9077
|
+
}
|
|
9078
|
+
async function junctionFetch(url, init = {}, policy) {
|
|
9079
|
+
const {
|
|
9080
|
+
provider,
|
|
9081
|
+
accountKey,
|
|
9082
|
+
timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,
|
|
9083
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
9084
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
9085
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
9086
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
9087
|
+
rateLimit = defaultRateLimitFor(provider),
|
|
9088
|
+
fetchImpl = fetch
|
|
9089
|
+
} = policy;
|
|
9090
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
9091
|
+
const label = safeUrlLabel(url);
|
|
9092
|
+
const startedAt = Date.now();
|
|
9093
|
+
let attempt = 0;
|
|
9094
|
+
let sawRetry = false;
|
|
9095
|
+
for (; ; ) {
|
|
9096
|
+
attempt++;
|
|
9097
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
9098
|
+
if (remainingBudget <= 0) {
|
|
9099
|
+
logOutcome(provider, accountKey, "retried-then-failed", method, label, attempt - 1, startedAt);
|
|
9100
|
+
throw new Error(
|
|
9101
|
+
`junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
9102
|
+
);
|
|
9103
|
+
}
|
|
9104
|
+
try {
|
|
9105
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9106
|
+
} catch (err) {
|
|
9107
|
+
if (err instanceof RateLimitExceededError) {
|
|
9108
|
+
logOutcome(provider, accountKey, "rate-limited", method, label, attempt, startedAt);
|
|
9109
|
+
}
|
|
9110
|
+
throw err;
|
|
9111
|
+
}
|
|
9112
|
+
const controller = new AbortController();
|
|
9113
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9114
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9115
|
+
try {
|
|
9116
|
+
const res = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
9117
|
+
clearTimeout(timer);
|
|
9118
|
+
if (res.ok || res.status < 500) {
|
|
9119
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", method, label, attempt, startedAt);
|
|
9120
|
+
return res;
|
|
9121
|
+
}
|
|
9122
|
+
if (attempt >= maxAttempts) {
|
|
9123
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9124
|
+
return res;
|
|
9125
|
+
}
|
|
9126
|
+
sawRetry = true;
|
|
9127
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9128
|
+
} catch (err) {
|
|
9129
|
+
clearTimeout(timer);
|
|
9130
|
+
if (attempt >= maxAttempts) {
|
|
9131
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9132
|
+
throw err;
|
|
9133
|
+
}
|
|
9134
|
+
sawRetry = true;
|
|
9135
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9136
|
+
}
|
|
9137
|
+
}
|
|
9138
|
+
}
|
|
9139
|
+
var DbJunctionTimeoutError = class extends Error {
|
|
9140
|
+
constructor(ms) {
|
|
9141
|
+
super(`junction: db query exceeded its ${ms}ms timeout`);
|
|
9142
|
+
this.name = "DbJunctionTimeoutError";
|
|
9143
|
+
}
|
|
9144
|
+
};
|
|
9145
|
+
function withTimeout(run, timeoutMs) {
|
|
9146
|
+
return new Promise((resolve, reject) => {
|
|
9147
|
+
const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs);
|
|
9148
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9149
|
+
run().then(
|
|
9150
|
+
(value) => {
|
|
9151
|
+
clearTimeout(timer);
|
|
9152
|
+
resolve(value);
|
|
9153
|
+
},
|
|
9154
|
+
(err) => {
|
|
9155
|
+
clearTimeout(timer);
|
|
9156
|
+
reject(err);
|
|
9157
|
+
}
|
|
9158
|
+
);
|
|
9159
|
+
});
|
|
9160
|
+
}
|
|
9161
|
+
var RETRYABLE_NODE_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN", "EPIPE"]);
|
|
9162
|
+
function isRetryableDbError(err) {
|
|
9163
|
+
if (err instanceof DbJunctionTimeoutError) return true;
|
|
9164
|
+
const code = err?.code;
|
|
9165
|
+
if (typeof code !== "string") return false;
|
|
9166
|
+
if (code.startsWith("08") || code === "57P03") return true;
|
|
9167
|
+
return RETRYABLE_NODE_ERROR_CODES.has(code);
|
|
9168
|
+
}
|
|
9169
|
+
async function dbJunction(run, policy) {
|
|
9170
|
+
const {
|
|
9171
|
+
provider,
|
|
9172
|
+
accountKey,
|
|
9173
|
+
timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,
|
|
9174
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
9175
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
9176
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
9177
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
9178
|
+
rateLimit = defaultRateLimitFor(provider)
|
|
9179
|
+
} = policy;
|
|
9180
|
+
const startedAt = Date.now();
|
|
9181
|
+
let attempt = 0;
|
|
9182
|
+
let sawRetry = false;
|
|
9183
|
+
for (; ; ) {
|
|
9184
|
+
attempt++;
|
|
9185
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
9186
|
+
if (remainingBudget <= 0) {
|
|
9187
|
+
logOutcome(provider, accountKey, "retried-then-failed", "QUERY", "db", attempt - 1, startedAt);
|
|
9188
|
+
throw new Error(
|
|
9189
|
+
`junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
9190
|
+
);
|
|
9191
|
+
}
|
|
9192
|
+
try {
|
|
9193
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9194
|
+
} catch (err) {
|
|
9195
|
+
if (err instanceof RateLimitExceededError) {
|
|
9196
|
+
logOutcome(provider, accountKey, "rate-limited", "QUERY", "db", attempt, startedAt);
|
|
9197
|
+
}
|
|
9198
|
+
throw err;
|
|
9199
|
+
}
|
|
9200
|
+
try {
|
|
9201
|
+
const result = await withTimeout(run, timeoutMs);
|
|
9202
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", "QUERY", "db", attempt, startedAt);
|
|
9203
|
+
return result;
|
|
9204
|
+
} catch (err) {
|
|
9205
|
+
if (!isRetryableDbError(err) || attempt >= maxAttempts) {
|
|
9206
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", "QUERY", "db", attempt, startedAt);
|
|
9207
|
+
throw err;
|
|
9208
|
+
}
|
|
9209
|
+
sawRetry = true;
|
|
9210
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9211
|
+
}
|
|
9212
|
+
}
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9215
|
+
// src/connectors/supabase/index.ts
|
|
9216
|
+
init_cjs_shims();
|
|
9217
|
+
|
|
9218
|
+
// src/connectors/supabase/client.ts
|
|
9219
|
+
init_cjs_shims();
|
|
9220
|
+
var DEFAULT_SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com";
|
|
9221
|
+
var DEFAULT_LOG_LIMIT = 1e3;
|
|
9222
|
+
var SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
9223
|
+
function boundedSupabaseLogWindow(since, now, maxLookbackMs) {
|
|
9224
|
+
const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS);
|
|
9225
|
+
const floor = new Date(now.getTime() - window);
|
|
9226
|
+
const endIso = now.toISOString();
|
|
9227
|
+
if (!since) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9228
|
+
const sinceMs = new Date(since).getTime();
|
|
9229
|
+
if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9230
|
+
if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true };
|
|
9231
|
+
return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false };
|
|
9232
|
+
}
|
|
9233
|
+
function buildEdgeLogsQuery(limit) {
|
|
9234
|
+
const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT);
|
|
9235
|
+
return [
|
|
9236
|
+
"select",
|
|
9237
|
+
" format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,",
|
|
9238
|
+
" request.method as method,",
|
|
9239
|
+
" request.path as path,",
|
|
9240
|
+
" response.status_code as status_code",
|
|
9241
|
+
"from edge_logs",
|
|
9242
|
+
"cross join unnest(metadata) as metadata",
|
|
9243
|
+
"cross join unnest(metadata.request) as request",
|
|
9244
|
+
"cross join unnest(metadata.response) as response",
|
|
9245
|
+
"where regexp_contains(request.path, '^/rest/v1/')",
|
|
9246
|
+
"order by timestamp asc",
|
|
9247
|
+
`limit ${safeLimit}`
|
|
9248
|
+
].join("\n");
|
|
9249
|
+
}
|
|
9250
|
+
async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
|
|
9251
|
+
const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
9252
|
+
const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
|
|
9253
|
+
url.searchParams.set("sql", buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT));
|
|
9254
|
+
url.searchParams.set("iso_timestamp_start", startIso);
|
|
9255
|
+
url.searchParams.set("iso_timestamp_end", endIso);
|
|
9256
|
+
const res = await junctionFetch(
|
|
9257
|
+
url,
|
|
9258
|
+
{ method: "GET", headers: bearerAuthHeader(token) },
|
|
9259
|
+
// accountKey: the Supabase project ref (ADR-131's own worked example) —
|
|
9260
|
+
// the Management API's rate limit is enforced per project.
|
|
9261
|
+
{ provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
|
|
9262
|
+
);
|
|
9263
|
+
if (!res.ok) {
|
|
9264
|
+
throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
|
|
9265
|
+
}
|
|
9266
|
+
const body = await res.json();
|
|
9267
|
+
if (body.error) {
|
|
9268
|
+
const message = typeof body.error === "string" ? body.error : body.error.message;
|
|
9269
|
+
throw new Error(`supabase connector: logs.all returned an error (${message})`);
|
|
9270
|
+
}
|
|
9271
|
+
return body.result ?? [];
|
|
9272
|
+
}
|
|
9273
|
+
|
|
9274
|
+
// src/connectors/supabase/map.ts
|
|
9275
|
+
init_cjs_shims();
|
|
9276
|
+
|
|
9277
|
+
// src/connectors/supabase/types.ts
|
|
9278
|
+
init_cjs_shims();
|
|
9279
|
+
function readSupabaseCredentials(raw) {
|
|
9280
|
+
const managementToken = raw["managementToken"];
|
|
9281
|
+
if (typeof managementToken !== "string" || managementToken.length === 0) {
|
|
9282
|
+
throw new Error("supabase connector: credentials.managementToken must be a non-empty string");
|
|
9283
|
+
}
|
|
9284
|
+
const postgresConnectionString = raw["postgresConnectionString"];
|
|
9285
|
+
if (postgresConnectionString !== void 0 && (typeof postgresConnectionString !== "string" || postgresConnectionString.length === 0)) {
|
|
9286
|
+
throw new Error(
|
|
9287
|
+
"supabase connector: credentials.postgresConnectionString must be a non-empty string when present"
|
|
9288
|
+
);
|
|
9289
|
+
}
|
|
9290
|
+
return {
|
|
9291
|
+
managementToken,
|
|
9292
|
+
...postgresConnectionString ? { postgresConnectionString } : {}
|
|
9293
|
+
};
|
|
9294
|
+
}
|
|
9295
|
+
var SUPABASE_TABLE_TARGET_KIND = "supabase-table";
|
|
9296
|
+
var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
|
|
9297
|
+
|
|
9298
|
+
// src/connectors/supabase/map.ts
|
|
9299
|
+
var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
|
|
9300
|
+
var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
|
|
9301
|
+
function targetFromRestPath(path47) {
|
|
9302
|
+
const rpcMatch = REST_RPC_PATH_RE.exec(path47);
|
|
9303
|
+
if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
|
|
9304
|
+
const tableMatch = REST_TABLE_PATH_RE.exec(path47);
|
|
9305
|
+
if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
|
|
9306
|
+
return null;
|
|
9307
|
+
}
|
|
9308
|
+
var ERROR_STATUS_THRESHOLD = 500;
|
|
9309
|
+
function mapEdgeLogRowsToSignals(rows) {
|
|
9310
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9311
|
+
for (const row of rows) {
|
|
9312
|
+
const target = targetFromRestPath(row.path);
|
|
9313
|
+
if (!target) continue;
|
|
9314
|
+
const key = `${target.targetKind}:${target.name}`;
|
|
9315
|
+
const isError = row.status_code >= ERROR_STATUS_THRESHOLD;
|
|
9316
|
+
const existing = buckets2.get(key);
|
|
9317
|
+
if (existing) {
|
|
9318
|
+
existing.callCount += 1;
|
|
9319
|
+
if (isError) existing.errorCount += 1;
|
|
9320
|
+
if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp;
|
|
9321
|
+
} else {
|
|
9322
|
+
buckets2.set(key, {
|
|
9323
|
+
targetKind: target.targetKind,
|
|
9324
|
+
targetName: target.name,
|
|
9325
|
+
callCount: 1,
|
|
9326
|
+
errorCount: isError ? 1 : 0,
|
|
9327
|
+
lastObservedIso: row.timestamp
|
|
9328
|
+
});
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
return [...buckets2.values()].map((b) => ({
|
|
9332
|
+
targetKind: b.targetKind,
|
|
9333
|
+
targetName: b.targetName,
|
|
9334
|
+
callCount: b.callCount,
|
|
9335
|
+
errorCount: b.errorCount,
|
|
9336
|
+
lastObservedIso: b.lastObservedIso
|
|
9337
|
+
}));
|
|
9338
|
+
}
|
|
9339
|
+
var FROM_TABLE_RE = /\bfrom\s+"?(?:[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i;
|
|
9340
|
+
var SYSTEM_SCHEMA_PREFIXES = ["pg_", "information_schema"];
|
|
9341
|
+
function tableNameFromQueryText(query) {
|
|
9342
|
+
const match = FROM_TABLE_RE.exec(query);
|
|
9343
|
+
if (!match) return null;
|
|
9344
|
+
const name = match[1];
|
|
9345
|
+
const lower = name.toLowerCase();
|
|
9346
|
+
if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
|
|
9347
|
+
return name;
|
|
9348
|
+
}
|
|
9349
|
+
function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
|
|
9350
|
+
const signals = [];
|
|
9351
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9352
|
+
for (const row of rows) {
|
|
9353
|
+
seen.add(row.queryid);
|
|
9354
|
+
const calls = Number(row.calls);
|
|
9355
|
+
const prior = previous.get(row.queryid);
|
|
9356
|
+
previous.set(row.queryid, { calls });
|
|
9357
|
+
if (!prior || calls < prior.calls) continue;
|
|
9358
|
+
const delta = calls - prior.calls;
|
|
9359
|
+
if (delta <= 0) continue;
|
|
9360
|
+
const table = tableNameFromQueryText(row.query);
|
|
9361
|
+
if (!table) continue;
|
|
9362
|
+
signals.push({
|
|
9363
|
+
targetKind: SUPABASE_TABLE_TARGET_KIND,
|
|
9364
|
+
targetName: table,
|
|
9365
|
+
callCount: delta,
|
|
9366
|
+
errorCount: 0,
|
|
9367
|
+
lastObservedIso: nowIso2
|
|
9368
|
+
});
|
|
9369
|
+
}
|
|
9370
|
+
for (const queryid of [...previous.keys()]) {
|
|
9371
|
+
if (!seen.has(queryid)) previous.delete(queryid);
|
|
9372
|
+
}
|
|
9373
|
+
return signals;
|
|
9374
|
+
}
|
|
9375
|
+
|
|
9376
|
+
// src/connectors/supabase/postgres-client.ts
|
|
9377
|
+
init_cjs_shims();
|
|
9378
|
+
var import_pg = __toESM(require("pg"), 1);
|
|
9379
|
+
var { Client } = import_pg.default;
|
|
9380
|
+
var DEFAULT_STATEMENT_LIMIT = 500;
|
|
9381
|
+
var STATEMENTS_QUERY = `
|
|
9382
|
+
select queryid, query, calls, total_exec_time, rows
|
|
9383
|
+
from pg_stat_statements
|
|
9384
|
+
where query ~* '^\\s*select\\b'
|
|
9385
|
+
order by calls desc
|
|
9386
|
+
limit $1
|
|
9387
|
+
`;
|
|
9388
|
+
async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT_LIMIT, accountKey = "unknown", clientFactory = (cs) => new Client({ connectionString: cs })) {
|
|
9389
|
+
return dbJunction(
|
|
9390
|
+
async () => {
|
|
9391
|
+
const client = clientFactory(connectionString);
|
|
9392
|
+
await client.connect();
|
|
9393
|
+
try {
|
|
9394
|
+
await client.query("SET default_transaction_read_only = on");
|
|
9395
|
+
const result = await client.query(STATEMENTS_QUERY, [limit]);
|
|
9396
|
+
return result.rows;
|
|
9397
|
+
} finally {
|
|
9398
|
+
await client.end();
|
|
9399
|
+
}
|
|
9400
|
+
},
|
|
9401
|
+
{ provider: "supabase-postgres", accountKey }
|
|
9402
|
+
);
|
|
9403
|
+
}
|
|
9404
|
+
|
|
9405
|
+
// src/connectors/supabase/resolve.ts
|
|
9406
|
+
init_cjs_shims();
|
|
9407
|
+
var import_types31 = require("@neat.is/types");
|
|
9408
|
+
function createSupabaseResolveTarget(graph, config) {
|
|
9409
|
+
return (signal, _ctx) => {
|
|
9410
|
+
if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
|
|
9411
|
+
return null;
|
|
9412
|
+
}
|
|
9413
|
+
const subResourceId = (0, import_types31.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
|
|
9414
|
+
if (graph.hasNode(subResourceId)) {
|
|
9415
|
+
return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types31.EdgeType.CALLS };
|
|
9416
|
+
}
|
|
9417
|
+
const projectLevelId = (0, import_types31.infraId)("supabase", config.nodeRef);
|
|
9418
|
+
if (graph.hasNode(projectLevelId)) {
|
|
9419
|
+
return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types31.EdgeType.CALLS };
|
|
9420
|
+
}
|
|
9421
|
+
return null;
|
|
9422
|
+
};
|
|
9423
|
+
}
|
|
9424
|
+
|
|
9425
|
+
// src/connectors/supabase/index.ts
|
|
9426
|
+
var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9427
|
+
var SupabaseConnector = class {
|
|
9428
|
+
// `deps.fetchPgStatStatements` defaults to the real Postgres-backed
|
|
9429
|
+
// implementation; tests override it to exercise the "both surfaces
|
|
9430
|
+
// combine" and "surface 2 only runs when a connection string is present"
|
|
9431
|
+
// behavior without a live database — the same dependency-injection seam
|
|
9432
|
+
// `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.
|
|
9433
|
+
constructor(config, deps = {}) {
|
|
9434
|
+
this.config = config;
|
|
9435
|
+
this.deps = deps;
|
|
9436
|
+
}
|
|
9437
|
+
config;
|
|
9438
|
+
deps;
|
|
9439
|
+
provider = "supabase";
|
|
9440
|
+
// pg_stat_statements.calls is cumulative, not per-window (map.ts's
|
|
9441
|
+
// diffPgStatStatementsToSignals doc comment) — this Map carries the
|
|
9442
|
+
// previous poll's counts across ticks, the same way
|
|
9443
|
+
// `startConnectorPollLoop` (connectors/index.ts) carries `since` across
|
|
9444
|
+
// ticks for every connector. Lives on the instance, not `ConnectorContext`,
|
|
9445
|
+
// because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's
|
|
9446
|
+
// `{ ...ctx, since }`) while this connector object is the one thing every
|
|
9447
|
+
// tick shares.
|
|
9448
|
+
statementBaselines = /* @__PURE__ */ new Map();
|
|
9449
|
+
async poll(ctx) {
|
|
9450
|
+
const creds = readSupabaseCredentials(ctx.credentials);
|
|
9451
|
+
const now = /* @__PURE__ */ new Date();
|
|
9452
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS;
|
|
9453
|
+
const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs);
|
|
9454
|
+
const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso);
|
|
9455
|
+
const signals = mapEdgeLogRowsToSignals(logRows);
|
|
9456
|
+
if (creds.postgresConnectionString) {
|
|
9457
|
+
const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
|
|
9458
|
+
const statementRows = await fetchStatements(
|
|
9459
|
+
creds.postgresConnectionString,
|
|
9460
|
+
this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
|
|
9461
|
+
this.config.apiProjectRef
|
|
9462
|
+
);
|
|
9463
|
+
signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
|
|
9464
|
+
}
|
|
9465
|
+
return signals;
|
|
9466
|
+
}
|
|
9467
|
+
};
|
|
9468
|
+
function createSupabaseConnector(graph, config, deps = {}) {
|
|
9469
|
+
return {
|
|
9470
|
+
connector: new SupabaseConnector(config, deps),
|
|
9471
|
+
resolveTarget: createSupabaseResolveTarget(graph, config)
|
|
9472
|
+
};
|
|
9473
|
+
}
|
|
9474
|
+
|
|
9475
|
+
// src/connectors/railway/index.ts
|
|
9476
|
+
init_cjs_shims();
|
|
9477
|
+
var import_types35 = require("@neat.is/types");
|
|
9478
|
+
|
|
9479
|
+
// src/connectors/railway/client.ts
|
|
9480
|
+
init_cjs_shims();
|
|
9481
|
+
var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
9482
|
+
var DEFAULT_MAX_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
|
|
9483
|
+
var DEFAULT_LOG_LIMIT2 = 1e3;
|
|
9484
|
+
function readRailwayToken(credentials) {
|
|
9485
|
+
const token = credentials.token;
|
|
9486
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
9487
|
+
throw new Error(
|
|
9488
|
+
"Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)"
|
|
9489
|
+
);
|
|
9490
|
+
}
|
|
9491
|
+
return token;
|
|
9492
|
+
}
|
|
9493
|
+
async function railwayGraphQL(apiUrl, token, query, variables, accountKey) {
|
|
9494
|
+
const res = await junctionFetch(
|
|
9495
|
+
apiUrl,
|
|
9496
|
+
{
|
|
9497
|
+
method: "POST",
|
|
9498
|
+
headers: {
|
|
9499
|
+
"Content-Type": "application/json",
|
|
9500
|
+
// docs.railway.com/integrations/api/graphql-overview confirms
|
|
9501
|
+
// `Authorization: Bearer <token>` for an account/workspace token;
|
|
9502
|
+
// Railway also documents a dedicated `Project-Access-Token: <token>`
|
|
9503
|
+
// header for the project-scoped token ADR-127 targets specifically.
|
|
9504
|
+
// This connector sends the Bearer form — needs-endpoint-testing
|
|
9505
|
+
// whether a live Project-Access-Token requires the dedicated header
|
|
9506
|
+
// instead once this poller runs against a real project.
|
|
9507
|
+
...bearerAuthHeader(token)
|
|
9508
|
+
},
|
|
9509
|
+
body: JSON.stringify({ query, variables })
|
|
9510
|
+
},
|
|
9511
|
+
{ provider: "railway", accountKey }
|
|
9512
|
+
);
|
|
9513
|
+
if (!res.ok) {
|
|
9514
|
+
throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
9515
|
+
}
|
|
9516
|
+
const body = await res.json();
|
|
9517
|
+
if (body.errors && body.errors.length > 0) {
|
|
9518
|
+
throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
9519
|
+
}
|
|
9520
|
+
if (!body.data) throw new Error("Railway GraphQL response carried no data");
|
|
9521
|
+
return body.data;
|
|
9522
|
+
}
|
|
9523
|
+
var HTTP_LOGS_QUERY = `
|
|
9524
|
+
query HttpLogs(
|
|
9525
|
+
$environmentId: String!
|
|
9526
|
+
$serviceId: String!
|
|
9527
|
+
$startDate: String!
|
|
9528
|
+
$endDate: String!
|
|
9529
|
+
$limit: Int
|
|
9530
|
+
) {
|
|
9531
|
+
httpLogs(
|
|
9532
|
+
environmentId: $environmentId
|
|
9533
|
+
serviceId: $serviceId
|
|
9534
|
+
startDate: $startDate
|
|
9535
|
+
endDate: $endDate
|
|
9536
|
+
limit: $limit
|
|
9537
|
+
) {
|
|
9538
|
+
timestamp
|
|
9539
|
+
method
|
|
9540
|
+
path
|
|
9541
|
+
httpStatus
|
|
9542
|
+
totalDuration
|
|
9543
|
+
requestId
|
|
9544
|
+
deploymentId
|
|
9545
|
+
edgeRegion
|
|
9546
|
+
}
|
|
9547
|
+
}
|
|
9548
|
+
`;
|
|
9549
|
+
var NETWORK_FLOW_LOGS_QUERY = `
|
|
9550
|
+
query NetworkFlowLogs(
|
|
9551
|
+
$environmentId: String!
|
|
9552
|
+
$serviceId: String!
|
|
9553
|
+
$startDate: String!
|
|
9554
|
+
$endDate: String!
|
|
9555
|
+
$limit: Int
|
|
9556
|
+
) {
|
|
9557
|
+
networkFlowLogs(
|
|
9558
|
+
environmentId: $environmentId
|
|
9559
|
+
serviceId: $serviceId
|
|
9560
|
+
startDate: $startDate
|
|
9561
|
+
endDate: $endDate
|
|
9562
|
+
limit: $limit
|
|
9563
|
+
) {
|
|
9564
|
+
timestamp
|
|
9565
|
+
peerServiceId
|
|
9566
|
+
peerKind
|
|
9567
|
+
direction
|
|
9568
|
+
byteCount
|
|
9569
|
+
packetCount
|
|
9570
|
+
dropCause
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
`;
|
|
9574
|
+
async function fetchRailwayHttpLogs(config, token, startDate, endDate) {
|
|
9575
|
+
const data = await railwayGraphQL(
|
|
9576
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9577
|
+
token,
|
|
9578
|
+
HTTP_LOGS_QUERY,
|
|
9579
|
+
{
|
|
9580
|
+
environmentId: config.environmentId,
|
|
9581
|
+
serviceId: config.serviceId,
|
|
9582
|
+
startDate,
|
|
9583
|
+
endDate,
|
|
9584
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9585
|
+
},
|
|
9586
|
+
config.environmentId
|
|
9587
|
+
);
|
|
9588
|
+
return data.httpLogs;
|
|
9589
|
+
}
|
|
9590
|
+
async function fetchRailwayNetworkFlowLogs(config, token, startDate, endDate) {
|
|
9591
|
+
const data = await railwayGraphQL(
|
|
9592
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9593
|
+
token,
|
|
9594
|
+
NETWORK_FLOW_LOGS_QUERY,
|
|
9595
|
+
{
|
|
9596
|
+
environmentId: config.environmentId,
|
|
9597
|
+
serviceId: config.serviceId,
|
|
9598
|
+
startDate,
|
|
9599
|
+
endDate,
|
|
9600
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9601
|
+
},
|
|
9602
|
+
config.environmentId
|
|
9603
|
+
);
|
|
9604
|
+
return data.networkFlowLogs;
|
|
9605
|
+
}
|
|
9606
|
+
function boundedRailwayStartDate(since, now, maxLookbackMs) {
|
|
9607
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
9608
|
+
if (!since) return floor.toISOString();
|
|
9609
|
+
const sinceMs = new Date(since).getTime();
|
|
9610
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
9611
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
9612
|
+
}
|
|
9613
|
+
|
|
9614
|
+
// src/connectors/railway/index.ts
|
|
9615
|
+
var ROUTE_TARGET_KIND = "route";
|
|
9616
|
+
var UNMATCHED_ROUTE_TARGET_KIND = "unmatched-route";
|
|
9617
|
+
var PEER_SERVICE_TARGET_KIND = "peer-service";
|
|
9618
|
+
function buildRailwayRouteIndex(graph, serviceName) {
|
|
9619
|
+
const out = [];
|
|
9620
|
+
graph.forEachNode((_id, attrs) => {
|
|
9621
|
+
const node = attrs;
|
|
9622
|
+
if (node.type !== import_types35.NodeType.RouteNode) return;
|
|
9623
|
+
const route = attrs;
|
|
9624
|
+
if (route.service !== serviceName) return;
|
|
9625
|
+
out.push({
|
|
9626
|
+
method: route.method.toUpperCase(),
|
|
9627
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
9628
|
+
routeNodeId: route.id,
|
|
9629
|
+
path: route.path,
|
|
9630
|
+
line: route.line
|
|
9631
|
+
});
|
|
9632
|
+
});
|
|
9633
|
+
return out;
|
|
9634
|
+
}
|
|
9635
|
+
function findRailwayRoute(entries, method, normalizedPath) {
|
|
9636
|
+
return entries.find(
|
|
9637
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
9638
|
+
);
|
|
9639
|
+
}
|
|
9640
|
+
function bucketKey2(method, normalizedPath) {
|
|
9641
|
+
return `${method} ${normalizedPath}`;
|
|
9642
|
+
}
|
|
9643
|
+
function isHttpErrorStatus(status2) {
|
|
9644
|
+
return status2 >= 400;
|
|
9645
|
+
}
|
|
9646
|
+
function upsertBucket(buckets2, key, isError, timestamp, build) {
|
|
9647
|
+
const existing = buckets2.get(key);
|
|
9648
|
+
if (existing) {
|
|
9649
|
+
existing.callCount += 1;
|
|
9650
|
+
if (isError) existing.errorCount += 1;
|
|
9651
|
+
if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
|
|
9652
|
+
return;
|
|
9653
|
+
}
|
|
9654
|
+
buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
|
|
9655
|
+
}
|
|
9656
|
+
function mapRailwayHttpLogsToSignals(entries, routeIndex) {
|
|
9657
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9658
|
+
for (const entry of entries) {
|
|
9659
|
+
const method = entry.method.toUpperCase();
|
|
9660
|
+
const normalizedPath = normalizePathTemplate(entry.path);
|
|
9661
|
+
const match = findRailwayRoute(routeIndex, method, normalizedPath);
|
|
9662
|
+
const isError = isHttpErrorStatus(entry.httpStatus);
|
|
9663
|
+
if (match) {
|
|
9664
|
+
upsertBucket(buckets2, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({
|
|
9665
|
+
targetKind: ROUTE_TARGET_KIND,
|
|
9666
|
+
targetName: match.routeNodeId,
|
|
9667
|
+
// RouteNode.line is optional in the schema (packages/types/src/
|
|
9668
|
+
// nodes.ts) even though routes.ts always sets it today — skip the
|
|
9669
|
+
// callSite rather than fabricate a line when it's ever absent
|
|
9670
|
+
// (file-awareness.md §6).
|
|
9671
|
+
...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
|
|
9672
|
+
}));
|
|
9673
|
+
} else {
|
|
9674
|
+
upsertBucket(
|
|
9675
|
+
buckets2,
|
|
9676
|
+
`unmatched:${bucketKey2(method, normalizedPath)}`,
|
|
9677
|
+
isError,
|
|
9678
|
+
entry.timestamp,
|
|
9679
|
+
() => ({
|
|
9680
|
+
targetKind: UNMATCHED_ROUTE_TARGET_KIND,
|
|
9681
|
+
targetName: bucketKey2(method, normalizedPath)
|
|
9682
|
+
})
|
|
9683
|
+
);
|
|
9684
|
+
}
|
|
9685
|
+
}
|
|
9686
|
+
return [...buckets2.values()].map((b) => ({
|
|
9687
|
+
targetKind: b.targetKind,
|
|
9688
|
+
targetName: b.targetName,
|
|
9689
|
+
callCount: b.callCount,
|
|
9690
|
+
errorCount: b.errorCount,
|
|
9691
|
+
lastObservedIso: b.lastObservedIso,
|
|
9692
|
+
...b.callSite ? { callSite: b.callSite } : {}
|
|
9693
|
+
}));
|
|
9694
|
+
}
|
|
9695
|
+
function mapRailwayNetworkFlowLogsToSignals(entries) {
|
|
9696
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9697
|
+
for (const entry of entries) {
|
|
9698
|
+
if (!entry.peerServiceId) continue;
|
|
9699
|
+
const isError = entry.dropCause !== null && entry.dropCause !== "";
|
|
9700
|
+
upsertBucket(buckets2, entry.peerServiceId, isError, entry.timestamp, () => ({
|
|
9701
|
+
targetKind: PEER_SERVICE_TARGET_KIND,
|
|
9702
|
+
targetName: entry.peerServiceId
|
|
9703
|
+
}));
|
|
9704
|
+
}
|
|
9705
|
+
return [...buckets2.values()].map((b) => ({
|
|
9706
|
+
targetKind: b.targetKind,
|
|
9707
|
+
targetName: b.targetName,
|
|
9708
|
+
callCount: b.callCount,
|
|
9709
|
+
errorCount: b.errorCount,
|
|
9710
|
+
lastObservedIso: b.lastObservedIso
|
|
9711
|
+
}));
|
|
9712
|
+
}
|
|
9713
|
+
function createRailwayResolveTarget(config) {
|
|
9714
|
+
return (signal) => {
|
|
9715
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9716
|
+
if (!serviceName) return null;
|
|
9717
|
+
if (signal.targetKind === ROUTE_TARGET_KIND) {
|
|
9718
|
+
return { targetNodeId: signal.targetName, serviceName, edgeType: import_types35.EdgeType.CALLS };
|
|
9719
|
+
}
|
|
9720
|
+
if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
|
|
9721
|
+
const peerName = config.serviceNameById[signal.targetName];
|
|
9722
|
+
if (!peerName) return null;
|
|
9723
|
+
return { targetNodeId: (0, import_types35.serviceId)(peerName), serviceName, edgeType: import_types35.EdgeType.CONNECTS_TO };
|
|
9724
|
+
}
|
|
9725
|
+
return null;
|
|
9726
|
+
};
|
|
9727
|
+
}
|
|
9728
|
+
function createRailwayConnector(graph, config) {
|
|
9729
|
+
return {
|
|
9730
|
+
provider: "railway",
|
|
9731
|
+
async poll(ctx) {
|
|
9732
|
+
const token = readRailwayToken(ctx.credentials);
|
|
9733
|
+
const now = /* @__PURE__ */ new Date();
|
|
9734
|
+
const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS2;
|
|
9735
|
+
const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs);
|
|
9736
|
+
const endDate = now.toISOString();
|
|
9737
|
+
const [httpLogs, flowLogs] = await Promise.all([
|
|
9738
|
+
fetchRailwayHttpLogs(config, token, startDate, endDate),
|
|
9739
|
+
fetchRailwayNetworkFlowLogs(config, token, startDate, endDate)
|
|
9740
|
+
]);
|
|
9741
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9742
|
+
const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : [];
|
|
9743
|
+
return [
|
|
9744
|
+
...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),
|
|
9745
|
+
...mapRailwayNetworkFlowLogsToSignals(flowLogs)
|
|
9746
|
+
];
|
|
9747
|
+
}
|
|
9748
|
+
};
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9751
|
+
// src/connectors/firebase/index.ts
|
|
9752
|
+
init_cjs_shims();
|
|
9753
|
+
|
|
9754
|
+
// src/connectors/firebase/logging-api.ts
|
|
9755
|
+
init_cjs_shims();
|
|
9756
|
+
function readFirebaseCredentials(raw) {
|
|
9757
|
+
const projectId = raw["projectId"];
|
|
9758
|
+
const accessToken = raw["accessToken"];
|
|
9759
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
9760
|
+
throw new Error("firebase connector: credentials.projectId must be a non-empty string");
|
|
9761
|
+
}
|
|
9762
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
9763
|
+
throw new Error("firebase connector: credentials.accessToken must be a non-empty string");
|
|
9764
|
+
}
|
|
9765
|
+
return { projectId, accessToken };
|
|
9766
|
+
}
|
|
9767
|
+
var RESOURCE_TYPES = [
|
|
9768
|
+
"cloud_function",
|
|
9769
|
+
"cloud_run_revision",
|
|
9770
|
+
"firebase_domain"
|
|
9771
|
+
];
|
|
9772
|
+
function isFirebaseResourceType(value) {
|
|
9773
|
+
return RESOURCE_TYPES.includes(value);
|
|
9774
|
+
}
|
|
9775
|
+
function buildEntriesFilter(sinceIso) {
|
|
9776
|
+
return [
|
|
9777
|
+
'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
|
|
9778
|
+
"httpRequest:*",
|
|
9779
|
+
`timestamp >= "${sinceIso}"`
|
|
9780
|
+
].join(" AND ");
|
|
9781
|
+
}
|
|
9782
|
+
var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9783
|
+
var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
|
|
9784
|
+
var PAGE_SIZE = 1e3;
|
|
9785
|
+
var MAX_PAGES = 20;
|
|
9786
|
+
async function fetchHttpRequestLogEntries(creds, sinceIso) {
|
|
9787
|
+
const filter = buildEntriesFilter(sinceIso);
|
|
9788
|
+
const out = [];
|
|
9789
|
+
let pageToken;
|
|
9790
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
9791
|
+
const body = {
|
|
9792
|
+
resourceNames: [`projects/${creds.projectId}`],
|
|
9793
|
+
filter,
|
|
9794
|
+
orderBy: "timestamp asc",
|
|
9795
|
+
pageSize: PAGE_SIZE,
|
|
9796
|
+
...pageToken ? { pageToken } : {}
|
|
9797
|
+
};
|
|
9798
|
+
const res = await junctionFetch(
|
|
9799
|
+
ENTRIES_LIST_URL,
|
|
9800
|
+
{
|
|
9801
|
+
method: "POST",
|
|
9802
|
+
headers: {
|
|
9803
|
+
...bearerAuthHeader(creds.accessToken),
|
|
9804
|
+
"Content-Type": "application/json"
|
|
9805
|
+
},
|
|
9806
|
+
body: JSON.stringify(body)
|
|
9807
|
+
},
|
|
9808
|
+
// accountKey: the GCP project id (ADR-131's own worked example for
|
|
9809
|
+
// Firebase) — one customer's Cloud Logging quota is scoped per GCP
|
|
9810
|
+
// project, not per Firebase site/function.
|
|
9811
|
+
{ provider: "firebase", accountKey: creds.projectId }
|
|
9812
|
+
);
|
|
9813
|
+
if (!res.ok) {
|
|
9814
|
+
throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
|
|
9815
|
+
}
|
|
9816
|
+
const json = await res.json();
|
|
9817
|
+
out.push(...json.entries ?? []);
|
|
9818
|
+
if (!json.nextPageToken) break;
|
|
9819
|
+
pageToken = json.nextPageToken;
|
|
9820
|
+
}
|
|
9821
|
+
return out;
|
|
9822
|
+
}
|
|
9823
|
+
|
|
9824
|
+
// src/connectors/firebase/map.ts
|
|
9825
|
+
init_cjs_shims();
|
|
9826
|
+
var FIELD_SEP = "\0";
|
|
9827
|
+
function packFirebaseTargetName(identity) {
|
|
9828
|
+
return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP);
|
|
9829
|
+
}
|
|
9830
|
+
function parseFirebaseTargetName(targetName) {
|
|
9831
|
+
const firstSep = targetName.indexOf(FIELD_SEP);
|
|
9832
|
+
if (firstSep === -1) return null;
|
|
9833
|
+
const resourceName = targetName.slice(0, firstSep);
|
|
9834
|
+
const rest = targetName.slice(firstSep + 1);
|
|
9835
|
+
const secondSep = rest.indexOf(FIELD_SEP);
|
|
9836
|
+
if (secondSep === -1) return null;
|
|
9837
|
+
const method = rest.slice(0, secondSep);
|
|
9838
|
+
const path47 = rest.slice(secondSep + 1);
|
|
9839
|
+
if (!resourceName || !method || !path47) return null;
|
|
9840
|
+
return { resourceName, method, path: path47 };
|
|
9841
|
+
}
|
|
9842
|
+
function resourceNameFor(type, labels) {
|
|
9843
|
+
if (!labels) return null;
|
|
9844
|
+
switch (type) {
|
|
9845
|
+
case "cloud_function":
|
|
9846
|
+
return labels["function_name"] ?? null;
|
|
9847
|
+
case "cloud_run_revision":
|
|
9848
|
+
return labels["service_name"] ?? null;
|
|
9849
|
+
case "firebase_domain":
|
|
9850
|
+
return labels["site_name"] ?? null;
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
function pathFromRequestUrl(requestUrl) {
|
|
9854
|
+
if (!requestUrl) return null;
|
|
9855
|
+
if (requestUrl.startsWith("/")) {
|
|
9856
|
+
const withoutQuery = requestUrl.split("?")[0];
|
|
9857
|
+
return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
|
|
9858
|
+
}
|
|
9859
|
+
try {
|
|
9860
|
+
const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
|
|
9861
|
+
const parsed = new URL(candidate);
|
|
9862
|
+
return parsed.pathname || "/";
|
|
9863
|
+
} catch {
|
|
9864
|
+
return null;
|
|
9865
|
+
}
|
|
9866
|
+
}
|
|
9867
|
+
var ERROR_STATUS_THRESHOLD2 = 500;
|
|
9868
|
+
function mapLogEntryToSignal(entry) {
|
|
9869
|
+
const resourceType = entry.resource?.type;
|
|
9870
|
+
if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
|
|
9871
|
+
const resourceName = resourceNameFor(resourceType, entry.resource?.labels);
|
|
9872
|
+
if (!resourceName) return null;
|
|
9873
|
+
const req = entry.httpRequest;
|
|
9874
|
+
if (!req) return null;
|
|
9875
|
+
if (!req.requestMethod) return null;
|
|
9876
|
+
const method = req.requestMethod.toUpperCase();
|
|
9877
|
+
const path47 = pathFromRequestUrl(req.requestUrl);
|
|
9878
|
+
if (path47 === null) return null;
|
|
9879
|
+
const timestamp = entry.timestamp;
|
|
9880
|
+
if (!timestamp) return null;
|
|
9881
|
+
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
|
|
9882
|
+
return {
|
|
9883
|
+
targetKind: resourceType,
|
|
9884
|
+
targetName: packFirebaseTargetName({ resourceName, method, path: path47 }),
|
|
9885
|
+
callCount: 1,
|
|
9886
|
+
errorCount: isError ? 1 : 0,
|
|
9887
|
+
lastObservedIso: timestamp
|
|
9888
|
+
};
|
|
9889
|
+
}
|
|
9890
|
+
function mapLogEntriesToSignals(entries) {
|
|
9891
|
+
const out = [];
|
|
9892
|
+
for (const entry of entries) {
|
|
9893
|
+
const signal = mapLogEntryToSignal(entry);
|
|
9894
|
+
if (signal) out.push(signal);
|
|
9895
|
+
}
|
|
9896
|
+
return out;
|
|
9897
|
+
}
|
|
9898
|
+
|
|
9899
|
+
// src/connectors/firebase/resolve.ts
|
|
9900
|
+
init_cjs_shims();
|
|
9901
|
+
var import_types36 = require("@neat.is/types");
|
|
9902
|
+
function neatServiceNameFor(resourceType, resourceName, serviceMap) {
|
|
9903
|
+
switch (resourceType) {
|
|
9904
|
+
case "cloud_function":
|
|
9905
|
+
return serviceMap.functions?.[resourceName] ?? null;
|
|
9906
|
+
case "cloud_run_revision":
|
|
9907
|
+
return serviceMap.cloudRun?.[resourceName] ?? null;
|
|
9908
|
+
case "firebase_domain":
|
|
9909
|
+
return serviceMap.hosting?.[resourceName] ?? null;
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
function routeEntriesFor(graph, serviceName) {
|
|
9913
|
+
const entries = [];
|
|
9914
|
+
graph.forEachNode((_id, attrs) => {
|
|
9915
|
+
const node = attrs;
|
|
9916
|
+
if (node.type !== import_types36.NodeType.RouteNode) return;
|
|
9917
|
+
const route = attrs;
|
|
9918
|
+
if (route.service !== serviceName) return;
|
|
9919
|
+
entries.push({
|
|
9920
|
+
method: route.method.toUpperCase(),
|
|
9921
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
9922
|
+
routeNodeId: route.id
|
|
9923
|
+
});
|
|
9924
|
+
});
|
|
9925
|
+
return entries;
|
|
9926
|
+
}
|
|
9927
|
+
function findRoute2(entries, method, normalizedPath) {
|
|
9928
|
+
return entries.find(
|
|
9929
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
9930
|
+
);
|
|
9931
|
+
}
|
|
9932
|
+
function createFirebaseResolveTarget(graph, serviceMap) {
|
|
9933
|
+
return (signal, _ctx) => {
|
|
9934
|
+
const resourceType = signal.targetKind;
|
|
9935
|
+
if (resourceType !== "cloud_function" && resourceType !== "cloud_run_revision" && resourceType !== "firebase_domain") {
|
|
9936
|
+
return null;
|
|
9937
|
+
}
|
|
9938
|
+
const identity = parseFirebaseTargetName(signal.targetName);
|
|
9939
|
+
if (!identity) return null;
|
|
9940
|
+
const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
|
|
9941
|
+
if (!serviceName) return null;
|
|
9942
|
+
const normalizedPath = normalizePathTemplate(identity.path);
|
|
9943
|
+
const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
|
|
9944
|
+
if (!match) return null;
|
|
9945
|
+
return {
|
|
9946
|
+
targetNodeId: match.routeNodeId,
|
|
9947
|
+
serviceName,
|
|
9948
|
+
edgeType: import_types36.EdgeType.CALLS
|
|
9949
|
+
};
|
|
9950
|
+
};
|
|
9951
|
+
}
|
|
9952
|
+
|
|
9953
|
+
// src/connectors/firebase/index.ts
|
|
9954
|
+
var FirebaseConnector = class {
|
|
9955
|
+
provider = "firebase";
|
|
9956
|
+
async poll(ctx) {
|
|
9957
|
+
const creds = readFirebaseCredentials(ctx.credentials);
|
|
9958
|
+
const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
|
|
9959
|
+
const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
|
|
9960
|
+
return mapLogEntriesToSignals(entries);
|
|
9961
|
+
}
|
|
9962
|
+
};
|
|
9963
|
+
function createFirebaseConnector(graph, serviceMap) {
|
|
9964
|
+
return {
|
|
9965
|
+
connector: new FirebaseConnector(),
|
|
9966
|
+
resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
|
|
9967
|
+
};
|
|
9968
|
+
}
|
|
9969
|
+
|
|
9970
|
+
// src/connectors/cloudflare/index.ts
|
|
9971
|
+
init_cjs_shims();
|
|
9972
|
+
|
|
9973
|
+
// src/connectors/cloudflare/connector.ts
|
|
9974
|
+
init_cjs_shims();
|
|
9975
|
+
var import_types38 = require("@neat.is/types");
|
|
9976
|
+
|
|
9977
|
+
// src/connectors/cloudflare/client.ts
|
|
9978
|
+
init_cjs_shims();
|
|
9979
|
+
var import_node_crypto3 = require("crypto");
|
|
9980
|
+
var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
9981
|
+
var DEFAULT_EVENT_LIMIT = 1e3;
|
|
9982
|
+
async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
|
|
9983
|
+
const token = ctx.credentials.apiToken;
|
|
9984
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
9985
|
+
throw new Error("cloudflare connector: ctx.credentials.apiToken must be a non-empty string");
|
|
9986
|
+
}
|
|
9987
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
9988
|
+
const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`;
|
|
9989
|
+
const body = {
|
|
9990
|
+
// Cloudflare's schema requires an identifier per query even for an
|
|
9991
|
+
// ad-hoc, unsaved one — a fresh id per tick, never reused.
|
|
9992
|
+
queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
|
|
9993
|
+
timeframe: { from: window.fromMs, to: window.toMs },
|
|
9994
|
+
view: "events",
|
|
9995
|
+
limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
|
|
9996
|
+
// Execute without persisting — this is a read, not a saved query
|
|
9997
|
+
// (connectors.md §2's "never writes on the read path" applies to
|
|
9998
|
+
// Cloudflare's own query-history state too).
|
|
9999
|
+
dry: true
|
|
10000
|
+
};
|
|
10001
|
+
const res = await junctionFetch(
|
|
10002
|
+
url,
|
|
10003
|
+
{
|
|
10004
|
+
method: "POST",
|
|
10005
|
+
headers: {
|
|
10006
|
+
"Content-Type": "application/json",
|
|
10007
|
+
...bearerAuthHeader(token)
|
|
10008
|
+
},
|
|
10009
|
+
body: JSON.stringify(body)
|
|
10010
|
+
},
|
|
10011
|
+
// accountKey: the Cloudflare account id (ADR-131's own worked example) —
|
|
10012
|
+
// the Telemetry Query API's ~300/5min limit is enforced per account.
|
|
10013
|
+
{ provider: "cloudflare", accountKey: config.accountId, fetchImpl }
|
|
10014
|
+
);
|
|
10015
|
+
if (!res.ok) {
|
|
10016
|
+
throw new Error(
|
|
10017
|
+
`cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`
|
|
10018
|
+
);
|
|
10019
|
+
}
|
|
10020
|
+
const payload = await res.json();
|
|
10021
|
+
if (!payload.success) {
|
|
10022
|
+
const message = payload.errors?.map((e) => e.message).join("; ") || "unknown error";
|
|
10023
|
+
throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
|
|
10024
|
+
}
|
|
10025
|
+
return payload.result?.events?.events ?? [];
|
|
10026
|
+
}
|
|
10027
|
+
|
|
10028
|
+
// src/connectors/cloudflare/map.ts
|
|
10029
|
+
init_cjs_shims();
|
|
10030
|
+
|
|
10031
|
+
// src/connectors/cloudflare/types.ts
|
|
10032
|
+
init_cjs_shims();
|
|
10033
|
+
var CLOUDFLARE_TARGET_KIND = "cloudflare-worker-invocation";
|
|
10034
|
+
|
|
10035
|
+
// src/connectors/cloudflare/map.ts
|
|
10036
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
10037
|
+
"GET",
|
|
10038
|
+
"POST",
|
|
10039
|
+
"PUT",
|
|
10040
|
+
"PATCH",
|
|
10041
|
+
"DELETE",
|
|
10042
|
+
"HEAD",
|
|
10043
|
+
"OPTIONS",
|
|
10044
|
+
"TRACE",
|
|
10045
|
+
"CONNECT"
|
|
10046
|
+
]);
|
|
10047
|
+
var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
|
|
10048
|
+
function parseHttpMethodFromTrigger(trigger) {
|
|
10049
|
+
if (!trigger) return null;
|
|
10050
|
+
const match = LEADING_TOKEN_RE.exec(trigger.trim());
|
|
10051
|
+
const token = match?.[1];
|
|
10052
|
+
if (!token) return null;
|
|
10053
|
+
const method = token.toUpperCase();
|
|
10054
|
+
return HTTP_METHODS.has(method) ? method : null;
|
|
10055
|
+
}
|
|
10056
|
+
var ERROR_STATUS_THRESHOLD3 = 500;
|
|
10057
|
+
function mapEventToSignal(event) {
|
|
10058
|
+
const metadata = event.$metadata;
|
|
10059
|
+
const workers = event.$workers;
|
|
10060
|
+
const method = parseHttpMethodFromTrigger(metadata?.trigger);
|
|
10061
|
+
if (!method) return null;
|
|
10062
|
+
const scriptName = workers?.scriptName ?? metadata?.service;
|
|
10063
|
+
if (!scriptName) return null;
|
|
10064
|
+
const timestampMs = event.timestamp ?? metadata?.startTime;
|
|
10065
|
+
if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
|
|
10066
|
+
const statusCode = metadata?.statusCode;
|
|
10067
|
+
const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
|
|
10068
|
+
return {
|
|
10069
|
+
targetKind: CLOUDFLARE_TARGET_KIND,
|
|
10070
|
+
targetName: scriptName,
|
|
10071
|
+
callCount: 1,
|
|
10072
|
+
errorCount: isError ? 1 : 0,
|
|
10073
|
+
lastObservedIso: new Date(timestampMs).toISOString(),
|
|
10074
|
+
method,
|
|
10075
|
+
...typeof statusCode === "number" ? { statusCode } : {},
|
|
10076
|
+
...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
|
|
10077
|
+
};
|
|
10078
|
+
}
|
|
10079
|
+
|
|
10080
|
+
// src/connectors/cloudflare/connector.ts
|
|
10081
|
+
var DEFAULT_MAX_LOOKBACK_MS3 = 60 * 60 * 1e3;
|
|
10082
|
+
function resolveFromMs(since, maxLookbackMs) {
|
|
10083
|
+
const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS3;
|
|
10084
|
+
const now = Date.now();
|
|
10085
|
+
const floor = now - cap;
|
|
10086
|
+
if (!since) return floor;
|
|
10087
|
+
const parsed = Date.parse(since);
|
|
10088
|
+
if (Number.isNaN(parsed)) return floor;
|
|
10089
|
+
return Math.max(parsed, floor);
|
|
10090
|
+
}
|
|
10091
|
+
var CloudflareConnector = class {
|
|
10092
|
+
constructor(config) {
|
|
10093
|
+
this.config = config;
|
|
10094
|
+
}
|
|
10095
|
+
config;
|
|
10096
|
+
provider = "cloudflare";
|
|
10097
|
+
async poll(ctx) {
|
|
10098
|
+
const toMs = Date.now();
|
|
10099
|
+
const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs);
|
|
10100
|
+
const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs });
|
|
10101
|
+
const signals = [];
|
|
10102
|
+
for (const event of events) {
|
|
10103
|
+
const signal = mapEventToSignal(event);
|
|
10104
|
+
if (signal) signals.push(signal);
|
|
10105
|
+
}
|
|
10106
|
+
return signals;
|
|
10107
|
+
}
|
|
10108
|
+
};
|
|
10109
|
+
function createCloudflareResolveTarget(config) {
|
|
10110
|
+
return (signal) => {
|
|
10111
|
+
if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
|
|
10112
|
+
const mapping = config.workers[signal.targetName];
|
|
10113
|
+
if (!mapping) return null;
|
|
10114
|
+
return {
|
|
10115
|
+
targetNodeId: (0, import_types38.fileId)(mapping.service, mapping.entryFile),
|
|
10116
|
+
serviceName: mapping.service,
|
|
10117
|
+
edgeType: import_types38.EdgeType.CALLS
|
|
10118
|
+
};
|
|
10119
|
+
};
|
|
10120
|
+
}
|
|
10121
|
+
|
|
10122
|
+
// src/connectors-config.ts
|
|
10123
|
+
init_cjs_shims();
|
|
10124
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
10125
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
10126
|
+
var import_node_fs25 = require("fs");
|
|
10127
|
+
var CONNECTORS_CONFIG_VERSION = 1;
|
|
10128
|
+
var EnvRefUnsetError = class extends Error {
|
|
10129
|
+
ref;
|
|
10130
|
+
varName;
|
|
10131
|
+
constructor(ref, varName) {
|
|
10132
|
+
super(`${ref} is unset`);
|
|
10133
|
+
this.name = "EnvRefUnsetError";
|
|
10134
|
+
this.ref = ref;
|
|
10135
|
+
this.varName = varName;
|
|
10136
|
+
}
|
|
10137
|
+
};
|
|
10138
|
+
function neatHome2() {
|
|
10139
|
+
const override = process.env.NEAT_HOME;
|
|
10140
|
+
if (override && override.length > 0) return import_node_path44.default.resolve(override);
|
|
10141
|
+
return import_node_path44.default.join(import_node_os4.default.homedir(), ".neat");
|
|
10142
|
+
}
|
|
10143
|
+
function connectorsConfigPath(home = neatHome2()) {
|
|
10144
|
+
return import_node_path44.default.join(home, "connectors.json");
|
|
10145
|
+
}
|
|
10146
|
+
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
10147
|
+
async function warnIfModeLooserThan0600(file) {
|
|
10148
|
+
if (process.platform === "win32") return;
|
|
10149
|
+
try {
|
|
10150
|
+
const stat = await import_node_fs25.promises.stat(file);
|
|
10151
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
10152
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
10153
|
+
console.warn(
|
|
10154
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
|
|
10155
|
+
);
|
|
10156
|
+
}
|
|
10157
|
+
} catch {
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
async function readConnectorsConfig(home = neatHome2()) {
|
|
10161
|
+
const file = connectorsConfigPath(home);
|
|
10162
|
+
let raw;
|
|
10163
|
+
try {
|
|
10164
|
+
raw = await import_node_fs25.promises.readFile(file, "utf8");
|
|
10165
|
+
} catch (err) {
|
|
10166
|
+
if (err.code === "ENOENT") {
|
|
10167
|
+
return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
|
|
10168
|
+
}
|
|
10169
|
+
throw err;
|
|
10170
|
+
}
|
|
10171
|
+
await warnIfModeLooserThan0600(file);
|
|
10172
|
+
let parsed;
|
|
10173
|
+
try {
|
|
10174
|
+
parsed = JSON.parse(raw);
|
|
10175
|
+
} catch (err) {
|
|
10176
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
10177
|
+
}
|
|
10178
|
+
return validateConfig(parsed, file);
|
|
10179
|
+
}
|
|
10180
|
+
function validateConfig(parsed, file) {
|
|
10181
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10182
|
+
throw new Error(`${file} must be a JSON object with a "connectors" array`);
|
|
10183
|
+
}
|
|
10184
|
+
const obj = parsed;
|
|
10185
|
+
const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
|
|
10186
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
10187
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
10188
|
+
}
|
|
10189
|
+
const rawConnectors = obj.connectors;
|
|
10190
|
+
if (!Array.isArray(rawConnectors)) {
|
|
10191
|
+
throw new Error(`${file}: "connectors" must be an array`);
|
|
10192
|
+
}
|
|
10193
|
+
const connectors = rawConnectors.map((entry, i) => validateEntry(entry, i, file));
|
|
10194
|
+
return { version, connectors };
|
|
10195
|
+
}
|
|
10196
|
+
function validateEntry(entry, index, file) {
|
|
10197
|
+
const where = `${file}: connectors[${index}]`;
|
|
10198
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
10199
|
+
throw new Error(`${where} must be an object`);
|
|
10200
|
+
}
|
|
10201
|
+
const e = entry;
|
|
10202
|
+
const id = e.id;
|
|
10203
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
10204
|
+
throw new Error(`${where}.id must be a non-empty string`);
|
|
10205
|
+
}
|
|
10206
|
+
const provider = e.provider;
|
|
10207
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
10208
|
+
throw new Error(`${where}.provider must be a non-empty string`);
|
|
10209
|
+
}
|
|
10210
|
+
if (e.project !== void 0 && typeof e.project !== "string") {
|
|
10211
|
+
throw new Error(`${where}.project must be a string when present`);
|
|
10212
|
+
}
|
|
10213
|
+
const credential = validateCredentialRef(e.credential, `${where}.credential`);
|
|
10214
|
+
let options;
|
|
10215
|
+
if (e.options !== void 0) {
|
|
10216
|
+
if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
|
|
10217
|
+
throw new Error(`${where}.options must be an object when present`);
|
|
10218
|
+
}
|
|
10219
|
+
options = e.options;
|
|
10220
|
+
}
|
|
10221
|
+
return {
|
|
10222
|
+
id,
|
|
10223
|
+
provider,
|
|
10224
|
+
...typeof e.project === "string" ? { project: e.project } : {},
|
|
10225
|
+
credential,
|
|
10226
|
+
...options ? { options } : {}
|
|
10227
|
+
};
|
|
10228
|
+
}
|
|
10229
|
+
function validateCredentialRef(raw, where) {
|
|
10230
|
+
if (typeof raw === "string") {
|
|
10231
|
+
if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
|
|
10232
|
+
return raw;
|
|
10233
|
+
}
|
|
10234
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
10235
|
+
const fields = raw;
|
|
10236
|
+
const keys = Object.keys(fields);
|
|
10237
|
+
if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
|
|
10238
|
+
for (const key of keys) {
|
|
10239
|
+
const v = fields[key];
|
|
10240
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
10241
|
+
throw new Error(`${where}.${key} must be a non-empty string`);
|
|
10242
|
+
}
|
|
10243
|
+
}
|
|
10244
|
+
return fields;
|
|
10245
|
+
}
|
|
10246
|
+
throw new Error(`${where} must be a string or an object of strings`);
|
|
10247
|
+
}
|
|
10248
|
+
function resolveCredential(ref, env = process.env) {
|
|
10249
|
+
if (typeof ref === "string") {
|
|
10250
|
+
return { kind: "single", value: resolveRef(ref, env) };
|
|
10251
|
+
}
|
|
10252
|
+
const fields = {};
|
|
10253
|
+
for (const [key, value] of Object.entries(ref)) {
|
|
10254
|
+
fields[key] = resolveRef(value, env);
|
|
10255
|
+
}
|
|
10256
|
+
return { kind: "fields", fields };
|
|
10257
|
+
}
|
|
10258
|
+
function resolveRef(value, env) {
|
|
10259
|
+
if (value.length > 1 && value.startsWith("$")) {
|
|
10260
|
+
const varName = value.slice(1);
|
|
10261
|
+
const resolved = env[varName];
|
|
10262
|
+
if (resolved === void 0 || resolved.length === 0) {
|
|
10263
|
+
throw new EnvRefUnsetError(value, varName);
|
|
10264
|
+
}
|
|
10265
|
+
return resolved;
|
|
10266
|
+
}
|
|
10267
|
+
return value;
|
|
10268
|
+
}
|
|
10269
|
+
function connectorMatchesProject(entry, project) {
|
|
10270
|
+
return entry.project === void 0 || entry.project === project;
|
|
10271
|
+
}
|
|
10272
|
+
|
|
10273
|
+
// src/connectors/registry.ts
|
|
10274
|
+
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
10275
|
+
async function authProbe(input) {
|
|
10276
|
+
const { provider, accountKey, url, token, init, fetchImpl } = input;
|
|
10277
|
+
try {
|
|
10278
|
+
const res = await junctionFetch(
|
|
10279
|
+
url,
|
|
10280
|
+
{
|
|
10281
|
+
...init ?? {},
|
|
10282
|
+
headers: { ...bearerAuthHeader(token), ...init?.headers ?? {} }
|
|
10283
|
+
},
|
|
10284
|
+
{ provider, accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
10285
|
+
);
|
|
10286
|
+
if (res.ok) return { ok: true };
|
|
10287
|
+
if (res.status === 401 || res.status === 403) {
|
|
10288
|
+
return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` };
|
|
10289
|
+
}
|
|
10290
|
+
return {
|
|
10291
|
+
ok: false,
|
|
10292
|
+
reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} \u2014 could not confirm the credential`
|
|
10293
|
+
};
|
|
10294
|
+
} catch (err) {
|
|
10295
|
+
return {
|
|
10296
|
+
ok: false,
|
|
10297
|
+
reason: `${provider} auth check could not reach the provider: ${err.message}`
|
|
10298
|
+
};
|
|
10299
|
+
}
|
|
10300
|
+
}
|
|
10301
|
+
var PROVIDER_DISPATCH = {
|
|
10302
|
+
supabase: {
|
|
10303
|
+
provider: "supabase",
|
|
10304
|
+
primaryCredentialKey: "managementToken",
|
|
10305
|
+
requiredCredentialFields: ["managementToken"],
|
|
10306
|
+
requiredOptionFields: ["apiProjectRef", "nodeRef", "serviceName"],
|
|
10307
|
+
build(graph, options) {
|
|
10308
|
+
return createSupabaseConnector(graph, options);
|
|
10309
|
+
},
|
|
10310
|
+
// GET /v1/projects — the Management API's own auth-gated list endpoint, the
|
|
10311
|
+
// cheapest confirmation the management token is live (the same surface
|
|
10312
|
+
// client.ts polls, minus the heavy log query).
|
|
10313
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10314
|
+
const cfg = options;
|
|
10315
|
+
const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
10316
|
+
return authProbe({
|
|
10317
|
+
provider: "supabase",
|
|
10318
|
+
accountKey: cfg.apiProjectRef ?? "validate",
|
|
10319
|
+
url: `${baseUrl}/v1/projects`,
|
|
10320
|
+
token: String(credentials.managementToken ?? ""),
|
|
10321
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10322
|
+
});
|
|
10323
|
+
}
|
|
10324
|
+
},
|
|
10325
|
+
railway: {
|
|
10326
|
+
provider: "railway",
|
|
10327
|
+
primaryCredentialKey: "token",
|
|
10328
|
+
requiredCredentialFields: ["token"],
|
|
10329
|
+
requiredOptionFields: ["environmentId", "serviceId", "serviceNameById"],
|
|
10330
|
+
build(graph, options) {
|
|
10331
|
+
const config = options;
|
|
10332
|
+
return {
|
|
10333
|
+
connector: createRailwayConnector(graph, config),
|
|
10334
|
+
resolveTarget: createRailwayResolveTarget(config)
|
|
10335
|
+
};
|
|
10336
|
+
},
|
|
10337
|
+
// A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call
|
|
10338
|
+
// at the HTTP layer, so a 2xx confirms the token was accepted regardless of
|
|
10339
|
+
// the query's own shape (the connector's real queries are still
|
|
10340
|
+
// needs-endpoint-testing per railway/types.ts — auth is what we check).
|
|
10341
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10342
|
+
const cfg = options;
|
|
10343
|
+
return authProbe({
|
|
10344
|
+
provider: "railway",
|
|
10345
|
+
accountKey: cfg.environmentId ?? "validate",
|
|
10346
|
+
url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
10347
|
+
token: String(credentials.token ?? ""),
|
|
10348
|
+
init: {
|
|
10349
|
+
method: "POST",
|
|
10350
|
+
headers: { "Content-Type": "application/json" },
|
|
10351
|
+
body: JSON.stringify({ query: "{ __typename }" })
|
|
10352
|
+
},
|
|
10353
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10354
|
+
});
|
|
10355
|
+
}
|
|
10356
|
+
},
|
|
10357
|
+
firebase: {
|
|
10358
|
+
provider: "firebase",
|
|
10359
|
+
// Firebase reads both projectId and accessToken from the credential; the
|
|
10360
|
+
// single-string form maps to the secret, and the required-fields check
|
|
10361
|
+
// below catches a projectId that was never supplied.
|
|
10362
|
+
primaryCredentialKey: "accessToken",
|
|
10363
|
+
requiredCredentialFields: ["projectId", "accessToken"],
|
|
10364
|
+
requiredOptionFields: [],
|
|
10365
|
+
build(graph, options) {
|
|
10366
|
+
return createFirebaseConnector(graph, options);
|
|
10367
|
+
},
|
|
10368
|
+
// GET the project's Cloud Logging log-name list (pageSize 1) — within the
|
|
10369
|
+
// same `roles/logging.viewer` grant the connector polls under, and the
|
|
10370
|
+
// lightest call that still fails 401/403 on a bad or wrong-scoped token.
|
|
10371
|
+
validate({ credentials, fetchImpl }) {
|
|
10372
|
+
const projectId = String(credentials.projectId ?? "");
|
|
10373
|
+
return authProbe({
|
|
10374
|
+
provider: "firebase",
|
|
10375
|
+
accountKey: projectId || "validate",
|
|
10376
|
+
url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,
|
|
10377
|
+
token: String(credentials.accessToken ?? ""),
|
|
10378
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10379
|
+
});
|
|
10380
|
+
}
|
|
10381
|
+
},
|
|
10382
|
+
cloudflare: {
|
|
10383
|
+
provider: "cloudflare",
|
|
10384
|
+
primaryCredentialKey: "apiToken",
|
|
10385
|
+
requiredCredentialFields: ["apiToken"],
|
|
10386
|
+
requiredOptionFields: ["accountId", "workers"],
|
|
10387
|
+
build(_graph, options) {
|
|
10388
|
+
const config = options;
|
|
10389
|
+
return {
|
|
10390
|
+
connector: new CloudflareConnector(config),
|
|
10391
|
+
resolveTarget: createCloudflareResolveTarget(config)
|
|
10392
|
+
};
|
|
10393
|
+
},
|
|
10394
|
+
// GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
|
|
10395
|
+
// token live" endpoint. 200 on a valid token, 401 on an invalid one.
|
|
10396
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10397
|
+
const cfg = options;
|
|
10398
|
+
const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
|
|
10399
|
+
return authProbe({
|
|
10400
|
+
provider: "cloudflare",
|
|
10401
|
+
accountKey: cfg.accountId ?? "validate",
|
|
10402
|
+
url: `${baseUrl}/user/tokens/verify`,
|
|
10403
|
+
token: String(credentials.apiToken ?? ""),
|
|
10404
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10405
|
+
});
|
|
10406
|
+
}
|
|
10407
|
+
}
|
|
10408
|
+
};
|
|
10409
|
+
function resolveEntryCredentials(dispatch, entry, env) {
|
|
10410
|
+
let credentials;
|
|
10411
|
+
try {
|
|
10412
|
+
const resolved = resolveCredential(entry.credential, env);
|
|
10413
|
+
credentials = resolved.kind === "single" ? { [dispatch.primaryCredentialKey]: resolved.value } : { ...resolved.fields };
|
|
10414
|
+
} catch (err) {
|
|
10415
|
+
if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
|
|
10416
|
+
return { ok: false, kind: "error", reason: err.message };
|
|
10417
|
+
}
|
|
10418
|
+
const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
|
|
10419
|
+
if (missingCreds.length > 0) {
|
|
10420
|
+
return {
|
|
10421
|
+
ok: false,
|
|
10422
|
+
kind: "missing-field",
|
|
10423
|
+
reason: `credential missing required field(s): ${missingCreds.join(", ")}`
|
|
10424
|
+
};
|
|
10425
|
+
}
|
|
10426
|
+
return { ok: true, credentials };
|
|
10427
|
+
}
|
|
10428
|
+
function buildRegistration(entry, graph, env = process.env) {
|
|
10429
|
+
const dispatch = PROVIDER_DISPATCH[entry.provider];
|
|
10430
|
+
if (!dispatch) {
|
|
10431
|
+
return { ok: false, reason: `unknown provider "${entry.provider}"` };
|
|
10432
|
+
}
|
|
10433
|
+
const creds = resolveEntryCredentials(dispatch, entry, env);
|
|
10434
|
+
if (!creds.ok) return { ok: false, reason: creds.reason };
|
|
10435
|
+
const credentials = creds.credentials;
|
|
10436
|
+
const options = entry.options ?? {};
|
|
10437
|
+
const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
|
|
10438
|
+
if (missingOpts.length > 0) {
|
|
10439
|
+
return {
|
|
10440
|
+
ok: false,
|
|
10441
|
+
reason: `options missing required field(s): ${missingOpts.join(", ")}`
|
|
10442
|
+
};
|
|
10443
|
+
}
|
|
10444
|
+
let built;
|
|
10445
|
+
try {
|
|
10446
|
+
built = dispatch.build(graph, options);
|
|
10447
|
+
} catch (err) {
|
|
10448
|
+
return { ok: false, reason: err.message };
|
|
10449
|
+
}
|
|
10450
|
+
const intervalMs = typeof options.intervalMs === "number" ? options.intervalMs : void 0;
|
|
10451
|
+
return {
|
|
10452
|
+
ok: true,
|
|
10453
|
+
registration: {
|
|
10454
|
+
connector: built.connector,
|
|
10455
|
+
credentials,
|
|
10456
|
+
resolveTarget: built.resolveTarget,
|
|
10457
|
+
...intervalMs !== void 0 ? { intervalMs } : {}
|
|
10458
|
+
}
|
|
10459
|
+
};
|
|
10460
|
+
}
|
|
10461
|
+
async function loadConnectorRegistrations(input) {
|
|
10462
|
+
const { project, graph, home, env = process.env, onSkip } = input;
|
|
10463
|
+
let connectors;
|
|
10464
|
+
try {
|
|
10465
|
+
connectors = (await readConnectorsConfig(home)).connectors;
|
|
10466
|
+
} catch (err) {
|
|
10467
|
+
onSkip?.(
|
|
10468
|
+
{ id: "(file)", provider: "(all)", credential: "" },
|
|
10469
|
+
`connectors.json unreadable \u2014 ${err.message}`
|
|
10470
|
+
);
|
|
10471
|
+
return [];
|
|
10472
|
+
}
|
|
10473
|
+
const registrations = [];
|
|
10474
|
+
for (const entry of connectors) {
|
|
10475
|
+
if (!connectorMatchesProject(entry, project)) continue;
|
|
10476
|
+
const result = buildRegistration(entry, graph, env);
|
|
10477
|
+
if (result.ok) registrations.push(result.registration);
|
|
10478
|
+
else onSkip?.(entry, result.reason);
|
|
10479
|
+
}
|
|
10480
|
+
return registrations;
|
|
10481
|
+
}
|
|
10482
|
+
|
|
8848
10483
|
// src/daemon.ts
|
|
8849
10484
|
init_auth();
|
|
8850
10485
|
|
|
8851
10486
|
// src/unrouted.ts
|
|
8852
10487
|
init_cjs_shims();
|
|
8853
|
-
var
|
|
8854
|
-
var
|
|
10488
|
+
var import_node_fs26 = require("fs");
|
|
10489
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
8855
10490
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
8856
10491
|
return {
|
|
8857
10492
|
timestamp: now.toISOString(),
|
|
@@ -8860,35 +10495,35 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
8860
10495
|
traceId: traceId ?? null
|
|
8861
10496
|
};
|
|
8862
10497
|
}
|
|
8863
|
-
async function appendUnroutedSpan(
|
|
8864
|
-
const target =
|
|
8865
|
-
await
|
|
8866
|
-
await
|
|
10498
|
+
async function appendUnroutedSpan(neatHome3, record) {
|
|
10499
|
+
const target = import_node_path45.default.join(neatHome3, "errors.ndjson");
|
|
10500
|
+
await import_node_fs26.promises.mkdir(neatHome3, { recursive: true });
|
|
10501
|
+
await import_node_fs26.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
8867
10502
|
}
|
|
8868
|
-
function unroutedErrorsPath(
|
|
8869
|
-
return
|
|
10503
|
+
function unroutedErrorsPath(neatHome3) {
|
|
10504
|
+
return import_node_path45.default.join(neatHome3, "errors.ndjson");
|
|
8870
10505
|
}
|
|
8871
10506
|
|
|
8872
10507
|
// src/daemon.ts
|
|
8873
|
-
var
|
|
10508
|
+
var import_types41 = require("@neat.is/types");
|
|
8874
10509
|
function daemonJsonPath(scanPath) {
|
|
8875
|
-
return
|
|
10510
|
+
return import_node_path46.default.join(scanPath, "neat-out", "daemon.json");
|
|
8876
10511
|
}
|
|
8877
10512
|
function daemonsDiscoveryDir(home) {
|
|
8878
10513
|
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
8879
|
-
return
|
|
10514
|
+
return import_node_path46.default.join(base, "daemons");
|
|
8880
10515
|
}
|
|
8881
10516
|
function daemonDiscoveryPath(project, home) {
|
|
8882
|
-
return
|
|
10517
|
+
return import_node_path46.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
8883
10518
|
}
|
|
8884
10519
|
function sanitizeDiscoveryName(project) {
|
|
8885
10520
|
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
8886
10521
|
}
|
|
8887
10522
|
function neatHomeFromEnv() {
|
|
8888
10523
|
const env = process.env.NEAT_HOME;
|
|
8889
|
-
if (env && env.length > 0) return
|
|
10524
|
+
if (env && env.length > 0) return import_node_path46.default.resolve(env);
|
|
8890
10525
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8891
|
-
return
|
|
10526
|
+
return import_node_path46.default.join(home, ".neat");
|
|
8892
10527
|
}
|
|
8893
10528
|
function resolveNeatVersion() {
|
|
8894
10529
|
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
@@ -8920,7 +10555,7 @@ async function clearDaemonRecord(record, home) {
|
|
|
8920
10555
|
} catch {
|
|
8921
10556
|
}
|
|
8922
10557
|
try {
|
|
8923
|
-
await
|
|
10558
|
+
await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
8924
10559
|
} catch {
|
|
8925
10560
|
}
|
|
8926
10561
|
}
|
|
@@ -8943,11 +10578,11 @@ function teardownSlot(slot) {
|
|
|
8943
10578
|
}
|
|
8944
10579
|
}
|
|
8945
10580
|
function neatHomeFor(opts) {
|
|
8946
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
10581
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path46.default.resolve(opts.neatHome);
|
|
8947
10582
|
const env = process.env.NEAT_HOME;
|
|
8948
|
-
if (env && env.length > 0) return
|
|
10583
|
+
if (env && env.length > 0) return import_node_path46.default.resolve(env);
|
|
8949
10584
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8950
|
-
return
|
|
10585
|
+
return import_node_path46.default.join(home, ".neat");
|
|
8951
10586
|
}
|
|
8952
10587
|
function routeSpanToProject(serviceName, projects) {
|
|
8953
10588
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -8991,13 +10626,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
8991
10626
|
if (!serviceName) return true;
|
|
8992
10627
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
8993
10628
|
return graph.someNode(
|
|
8994
|
-
(_id, attrs) => attrs.type ===
|
|
10629
|
+
(_id, attrs) => attrs.type === import_types41.NodeType.ServiceNode && attrs.name === serviceName
|
|
8995
10630
|
);
|
|
8996
10631
|
}
|
|
8997
|
-
async function bootstrapProject(entry, connectors = []) {
|
|
8998
|
-
const paths = pathsForProject(entry.name,
|
|
10632
|
+
async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
10633
|
+
const paths = pathsForProject(entry.name, import_node_path46.default.join(entry.path, "neat-out"));
|
|
8999
10634
|
try {
|
|
9000
|
-
const stat = await
|
|
10635
|
+
const stat = await import_node_fs27.promises.stat(entry.path);
|
|
9001
10636
|
if (!stat.isDirectory()) {
|
|
9002
10637
|
throw new Error(`registered path ${entry.path} is not a directory`);
|
|
9003
10638
|
}
|
|
@@ -9035,7 +10670,16 @@ async function bootstrapProject(entry, connectors = []) {
|
|
|
9035
10670
|
staleEventsPath: paths.staleEventsPath,
|
|
9036
10671
|
project: entry.name
|
|
9037
10672
|
});
|
|
9038
|
-
const
|
|
10673
|
+
const fileConnectors = neatHome3 ? await loadConnectorRegistrations({
|
|
10674
|
+
project: entry.name,
|
|
10675
|
+
graph,
|
|
10676
|
+
home: neatHome3,
|
|
10677
|
+
onSkip: (skipped, reason) => console.warn(
|
|
10678
|
+
`neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
|
|
10679
|
+
)
|
|
10680
|
+
}) : [];
|
|
10681
|
+
const allConnectors = [...connectors, ...fileConnectors];
|
|
10682
|
+
const stopFns = allConnectors.map(
|
|
9039
10683
|
(registration) => startConnectorPollLoop(
|
|
9040
10684
|
registration.connector,
|
|
9041
10685
|
{ projectDir: entry.path, credentials: registration.credentials },
|
|
@@ -9113,7 +10757,7 @@ async function startDaemon(opts = {}) {
|
|
|
9113
10757
|
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
9114
10758
|
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
9115
10759
|
const singleProject = projectArg;
|
|
9116
|
-
const singleProjectPath = singleProject && projectPathArg ?
|
|
10760
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path46.default.resolve(projectPathArg) : null;
|
|
9117
10761
|
if (singleProject && !singleProjectPath) {
|
|
9118
10762
|
throw new Error(
|
|
9119
10763
|
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
@@ -9121,14 +10765,14 @@ async function startDaemon(opts = {}) {
|
|
|
9121
10765
|
}
|
|
9122
10766
|
if (!singleProject) {
|
|
9123
10767
|
try {
|
|
9124
|
-
await
|
|
10768
|
+
await import_node_fs27.promises.access(regPath);
|
|
9125
10769
|
} catch {
|
|
9126
10770
|
throw new Error(
|
|
9127
10771
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
9128
10772
|
);
|
|
9129
10773
|
}
|
|
9130
10774
|
}
|
|
9131
|
-
const pidPath =
|
|
10775
|
+
const pidPath = import_node_path46.default.join(home, "neatd.pid");
|
|
9132
10776
|
await writeAtomically(pidPath, `${process.pid}
|
|
9133
10777
|
`);
|
|
9134
10778
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -9172,7 +10816,7 @@ async function startDaemon(opts = {}) {
|
|
|
9172
10816
|
}
|
|
9173
10817
|
async function tryRecoverSlot(entry) {
|
|
9174
10818
|
try {
|
|
9175
|
-
const fresh = await bootstrapProject(entry, opts.connectors ?? []);
|
|
10819
|
+
const fresh = await bootstrapProject(entry, opts.connectors ?? [], home);
|
|
9176
10820
|
const prior = slots.get(entry.name);
|
|
9177
10821
|
if (prior) teardownSlot(prior);
|
|
9178
10822
|
slots.set(entry.name, fresh);
|
|
@@ -9196,7 +10840,7 @@ async function startDaemon(opts = {}) {
|
|
|
9196
10840
|
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
9197
10841
|
bootstrapStartedAt.set(entry.name, Date.now());
|
|
9198
10842
|
try {
|
|
9199
|
-
const slot = await bootstrapProject(entry, opts.connectors ?? []);
|
|
10843
|
+
const slot = await bootstrapProject(entry, opts.connectors ?? [], home);
|
|
9200
10844
|
const prior = slots.get(entry.name);
|
|
9201
10845
|
if (prior) teardownSlot(prior);
|
|
9202
10846
|
slots.set(entry.name, slot);
|
|
@@ -9322,7 +10966,7 @@ async function startDaemon(opts = {}) {
|
|
|
9322
10966
|
}
|
|
9323
10967
|
if (restApp) await restApp.close().catch(() => {
|
|
9324
10968
|
});
|
|
9325
|
-
await
|
|
10969
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9326
10970
|
});
|
|
9327
10971
|
throw new Error(
|
|
9328
10972
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
@@ -9448,7 +11092,7 @@ async function startDaemon(opts = {}) {
|
|
|
9448
11092
|
});
|
|
9449
11093
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
9450
11094
|
});
|
|
9451
|
-
await
|
|
11095
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9452
11096
|
});
|
|
9453
11097
|
throw new Error(
|
|
9454
11098
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
@@ -9483,7 +11127,7 @@ async function startDaemon(opts = {}) {
|
|
|
9483
11127
|
});
|
|
9484
11128
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
9485
11129
|
});
|
|
9486
|
-
await
|
|
11130
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9487
11131
|
});
|
|
9488
11132
|
throw new Error(
|
|
9489
11133
|
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
@@ -9530,9 +11174,9 @@ async function startDaemon(opts = {}) {
|
|
|
9530
11174
|
let registryWatcher = null;
|
|
9531
11175
|
let reloadTimer = null;
|
|
9532
11176
|
if (!singleProject) try {
|
|
9533
|
-
const regDir =
|
|
9534
|
-
const regBase =
|
|
9535
|
-
registryWatcher = (0,
|
|
11177
|
+
const regDir = import_node_path46.default.dirname(regPath);
|
|
11178
|
+
const regBase = import_node_path46.default.basename(regPath);
|
|
11179
|
+
registryWatcher = (0, import_node_fs27.watch)(regDir, (_eventType, filename) => {
|
|
9536
11180
|
if (filename !== null && filename !== regBase) return;
|
|
9537
11181
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
9538
11182
|
reloadTimer = setTimeout(() => {
|
|
@@ -9581,7 +11225,7 @@ async function startDaemon(opts = {}) {
|
|
|
9581
11225
|
if (daemonRecord) {
|
|
9582
11226
|
await clearDaemonRecord(daemonRecord, home);
|
|
9583
11227
|
}
|
|
9584
|
-
await
|
|
11228
|
+
await import_node_fs27.promises.unlink(pidPath).catch(() => {
|
|
9585
11229
|
});
|
|
9586
11230
|
};
|
|
9587
11231
|
return {
|