@neat.is/core 0.4.16 → 0.4.18
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-6CO7C4IU.js → chunk-BGPWBRLU.js} +4 -2
- package/dist/{chunk-6CO7C4IU.js.map → chunk-BGPWBRLU.js.map} +1 -1
- package/dist/{chunk-VMLWUK7W.js → chunk-GXWBMJDJ.js} +210 -11
- package/dist/chunk-GXWBMJDJ.js.map +1 -0
- package/dist/{chunk-GHPHVXYM.js → chunk-MTXF77TN.js} +2 -2
- package/dist/{chunk-XS4CGNRO.js → chunk-T3X6XJPU.js} +403 -72
- package/dist/chunk-T3X6XJPU.js.map +1 -0
- package/dist/cli.cjs +964 -364
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +12 -2
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +331 -86
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +559 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -13
- package/dist/index.d.ts +51 -13
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +726 -193
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +161 -37
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-QAISVAY5.js → otel-grpc-I67ONCRM.js} +3 -3
- package/dist/server.cjs +271 -97
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-VMLWUK7W.js.map +0 -1
- package/dist/chunk-XS4CGNRO.js.map +0 -1
- /package/dist/{chunk-GHPHVXYM.js.map → chunk-MTXF77TN.js.map} +0 -0
- /package/dist/{otel-grpc-QAISVAY5.js.map → otel-grpc-I67ONCRM.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
|
|
|
57
57
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
58
|
const publicRead = opts.publicRead === true;
|
|
59
59
|
app.addHook("preHandler", (req, reply, done) => {
|
|
60
|
-
const
|
|
60
|
+
const path43 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
61
61
|
for (const suffix of suffixes) {
|
|
62
|
-
if (
|
|
62
|
+
if (path43 === suffix || path43.endsWith(suffix)) {
|
|
63
63
|
done();
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -187,8 +187,8 @@ function reshapeGrpcRequest(req) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
function resolveProtoRoot() {
|
|
190
|
-
const here =
|
|
191
|
-
return
|
|
190
|
+
const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
191
|
+
return import_node_path39.default.resolve(here, "..", "proto");
|
|
192
192
|
}
|
|
193
193
|
function loadTraceService() {
|
|
194
194
|
const protoRoot = resolveProtoRoot();
|
|
@@ -256,13 +256,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
256
256
|
})
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
-
var import_node_url,
|
|
259
|
+
var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
|
|
260
260
|
var init_otel_grpc = __esm({
|
|
261
261
|
"src/otel-grpc.ts"() {
|
|
262
262
|
"use strict";
|
|
263
263
|
init_cjs_shims();
|
|
264
264
|
import_node_url = require("url");
|
|
265
|
-
|
|
265
|
+
import_node_path39 = __toESM(require("path"), 1);
|
|
266
266
|
import_node_crypto2 = require("crypto");
|
|
267
267
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
268
268
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
@@ -372,10 +372,10 @@ function parseOtlpRequest(body) {
|
|
|
372
372
|
return out;
|
|
373
373
|
}
|
|
374
374
|
function loadProtoRoot() {
|
|
375
|
-
const here =
|
|
376
|
-
const protoRoot =
|
|
375
|
+
const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
376
|
+
const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
|
|
377
377
|
const root = new import_protobufjs.default.Root();
|
|
378
|
-
root.resolvePath = (_origin, target) =>
|
|
378
|
+
root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
|
|
379
379
|
root.loadSync(
|
|
380
380
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
381
381
|
{ keepCase: true }
|
|
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
|
|
|
473
473
|
for (const s of spans) projectQueue.push({ project, span: s });
|
|
474
474
|
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
475
475
|
};
|
|
476
|
+
const offersProjectRouting = opts.onProjectSpan !== void 0;
|
|
476
477
|
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
477
478
|
function warnLegacyEndpoint(serviceName) {
|
|
479
|
+
if (!offersProjectRouting) return;
|
|
478
480
|
if (legacyEndpointWarned.has(serviceName)) return;
|
|
479
481
|
legacyEndpointWarned.add(serviceName);
|
|
480
482
|
console.warn(
|
|
@@ -579,12 +581,12 @@ function logSpanHandler(span) {
|
|
|
579
581
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
580
582
|
);
|
|
581
583
|
}
|
|
582
|
-
var
|
|
584
|
+
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
583
585
|
var init_otel = __esm({
|
|
584
586
|
"src/otel.ts"() {
|
|
585
587
|
"use strict";
|
|
586
588
|
init_cjs_shims();
|
|
587
|
-
|
|
589
|
+
import_node_path40 = __toESM(require("path"), 1);
|
|
588
590
|
import_node_url2 = require("url");
|
|
589
591
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
590
592
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -1172,19 +1174,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1172
1174
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1173
1175
|
let best = { path: [start], edges: [] };
|
|
1174
1176
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1175
|
-
function step(node,
|
|
1176
|
-
if (
|
|
1177
|
-
best = { path: [...
|
|
1177
|
+
function step(node, path43, edges) {
|
|
1178
|
+
if (path43.length > best.path.length) {
|
|
1179
|
+
best = { path: [...path43], edges: [...edges] };
|
|
1178
1180
|
}
|
|
1179
|
-
if (
|
|
1181
|
+
if (path43.length - 1 >= maxDepth) return;
|
|
1180
1182
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1181
1183
|
for (const [srcId, edge] of incoming) {
|
|
1182
1184
|
if (visited.has(srcId)) continue;
|
|
1183
1185
|
visited.add(srcId);
|
|
1184
|
-
|
|
1186
|
+
path43.push(srcId);
|
|
1185
1187
|
edges.push(edge);
|
|
1186
|
-
step(srcId,
|
|
1187
|
-
|
|
1188
|
+
step(srcId, path43, edges);
|
|
1189
|
+
path43.pop();
|
|
1188
1190
|
edges.pop();
|
|
1189
1191
|
visited.delete(srcId);
|
|
1190
1192
|
}
|
|
@@ -1724,6 +1726,41 @@ function thresholdForEdgeType(edgeType, overrides) {
|
|
|
1724
1726
|
const map = overrides ?? loadStaleThresholdsFromEnv();
|
|
1725
1727
|
return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
|
|
1726
1728
|
}
|
|
1729
|
+
var DEFAULT_INCIDENT_THRESHOLDS = {
|
|
1730
|
+
threshold: 5,
|
|
1731
|
+
windowMs: 6e4
|
|
1732
|
+
};
|
|
1733
|
+
function loadIncidentThresholdsFromEnv() {
|
|
1734
|
+
const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
|
|
1735
|
+
if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1736
|
+
try {
|
|
1737
|
+
const overrides = JSON.parse(raw);
|
|
1738
|
+
const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
|
|
1739
|
+
if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
|
|
1740
|
+
merged.threshold = Math.floor(overrides.threshold);
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
|
|
1743
|
+
merged.windowMs = overrides.windowMs;
|
|
1744
|
+
}
|
|
1745
|
+
return merged;
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
console.warn(
|
|
1748
|
+
`[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
|
|
1749
|
+
);
|
|
1750
|
+
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function httpResponseStatus(span) {
|
|
1754
|
+
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1755
|
+
const v = span.attributes[key];
|
|
1756
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1757
|
+
if (typeof v === "string") {
|
|
1758
|
+
const n = Number(v);
|
|
1759
|
+
if (Number.isFinite(n)) return n;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return void 0;
|
|
1763
|
+
}
|
|
1727
1764
|
function nowIso(ctx) {
|
|
1728
1765
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1729
1766
|
}
|
|
@@ -2137,6 +2174,71 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
2137
2174
|
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2138
2175
|
};
|
|
2139
2176
|
}
|
|
2177
|
+
async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
|
|
2178
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
2179
|
+
const first = firstTimestamp ?? timestamp;
|
|
2180
|
+
const peer = pickAddress(span);
|
|
2181
|
+
const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
|
|
2182
|
+
const ev = {
|
|
2183
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
2184
|
+
timestamp,
|
|
2185
|
+
service: span.service,
|
|
2186
|
+
traceId: span.traceId,
|
|
2187
|
+
spanId: span.spanId,
|
|
2188
|
+
errorType: "http-failure",
|
|
2189
|
+
errorMessage: message,
|
|
2190
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2191
|
+
affectedNode,
|
|
2192
|
+
httpStatusCode: statusCode,
|
|
2193
|
+
incidentCount: count,
|
|
2194
|
+
firstTimestamp: first,
|
|
2195
|
+
lastTimestamp: timestamp
|
|
2196
|
+
};
|
|
2197
|
+
await appendErrorEvent(ctx, ev);
|
|
2198
|
+
}
|
|
2199
|
+
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2200
|
+
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2201
|
+
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
2202
|
+
const peer = pickAddress(span) ?? span.spanId;
|
|
2203
|
+
const key = `${span.service}->${peer}`;
|
|
2204
|
+
const existing = ctx.burstState.get(key);
|
|
2205
|
+
let state;
|
|
2206
|
+
if (existing && nowMs - existing.lastMs <= windowMs) {
|
|
2207
|
+
existing.count += 1;
|
|
2208
|
+
existing.lastTs = ts;
|
|
2209
|
+
existing.lastMs = nowMs;
|
|
2210
|
+
existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
|
|
2211
|
+
state = existing;
|
|
2212
|
+
} else {
|
|
2213
|
+
state = {
|
|
2214
|
+
count: 1,
|
|
2215
|
+
firstTs: ts,
|
|
2216
|
+
lastTs: ts,
|
|
2217
|
+
lastMs: nowMs,
|
|
2218
|
+
codes: /* @__PURE__ */ new Map([[status2, 1]])
|
|
2219
|
+
};
|
|
2220
|
+
ctx.burstState.set(key, state);
|
|
2221
|
+
}
|
|
2222
|
+
if (state.count < threshold) return;
|
|
2223
|
+
let dominant = status2;
|
|
2224
|
+
let max = 0;
|
|
2225
|
+
for (const [code, n] of state.codes) {
|
|
2226
|
+
if (n > max) {
|
|
2227
|
+
max = n;
|
|
2228
|
+
dominant = code;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
await recordFailingResponseIncident(
|
|
2232
|
+
ctx,
|
|
2233
|
+
span,
|
|
2234
|
+
affectedNode,
|
|
2235
|
+
state.lastTs,
|
|
2236
|
+
dominant,
|
|
2237
|
+
state.count,
|
|
2238
|
+
state.firstTs
|
|
2239
|
+
);
|
|
2240
|
+
ctx.burstState.delete(key);
|
|
2241
|
+
}
|
|
2140
2242
|
async function handleSpan(ctx, span) {
|
|
2141
2243
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
2142
2244
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
@@ -2235,6 +2337,14 @@ async function handleSpan(ctx, span) {
|
|
|
2235
2337
|
await appendErrorEvent(ctx, ev);
|
|
2236
2338
|
}
|
|
2237
2339
|
}
|
|
2340
|
+
if (span.statusCode !== 2) {
|
|
2341
|
+
const status2 = httpResponseStatus(span);
|
|
2342
|
+
if (status2 !== void 0 && status2 >= 500) {
|
|
2343
|
+
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2344
|
+
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2345
|
+
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2238
2348
|
void affectedNode;
|
|
2239
2349
|
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
2240
2350
|
}
|
|
@@ -4254,7 +4364,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
4254
4364
|
|
|
4255
4365
|
// src/extract/calls/index.ts
|
|
4256
4366
|
init_cjs_shims();
|
|
4257
|
-
var
|
|
4367
|
+
var import_types17 = require("@neat.is/types");
|
|
4258
4368
|
|
|
4259
4369
|
// src/extract/calls/http.ts
|
|
4260
4370
|
init_cjs_shims();
|
|
@@ -4607,15 +4717,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
4607
4717
|
return out;
|
|
4608
4718
|
}
|
|
4609
4719
|
|
|
4720
|
+
// src/extract/calls/supabase.ts
|
|
4721
|
+
init_cjs_shims();
|
|
4722
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
4723
|
+
var import_types16 = require("@neat.is/types");
|
|
4724
|
+
var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
|
|
4725
|
+
var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
|
|
4726
|
+
var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
4727
|
+
function hostFromLiteral(literal) {
|
|
4728
|
+
if (!literal) return null;
|
|
4729
|
+
const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
|
|
4730
|
+
if (!m) return null;
|
|
4731
|
+
const host = m[1];
|
|
4732
|
+
if (!/\.supabase\.(co|in)$/i.test(host)) return null;
|
|
4733
|
+
return host;
|
|
4734
|
+
}
|
|
4735
|
+
function readImports2(content) {
|
|
4736
|
+
return {
|
|
4737
|
+
hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
|
|
4738
|
+
hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
function constructorMatchesImport(name, ctx) {
|
|
4742
|
+
if (name === "createClient") return ctx.hasSupabaseJs;
|
|
4743
|
+
return ctx.hasSupabaseSsr;
|
|
4744
|
+
}
|
|
4745
|
+
function supabaseEndpointsFromFile(file, serviceDir) {
|
|
4746
|
+
const ctx = readImports2(file.content);
|
|
4747
|
+
if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
|
|
4748
|
+
const out = [];
|
|
4749
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4750
|
+
SUPABASE_CLIENT_RE.lastIndex = 0;
|
|
4751
|
+
let m;
|
|
4752
|
+
while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
|
|
4753
|
+
const ctor = m[1];
|
|
4754
|
+
if (!constructorMatchesImport(ctor, ctx)) continue;
|
|
4755
|
+
const host = hostFromLiteral(m[2]);
|
|
4756
|
+
const name = host ?? "env";
|
|
4757
|
+
if (seen.has(name)) continue;
|
|
4758
|
+
seen.add(name);
|
|
4759
|
+
const line = lineOf(file.content, m[0]);
|
|
4760
|
+
out.push({
|
|
4761
|
+
infraId: (0, import_types16.infraId)("supabase", name),
|
|
4762
|
+
name,
|
|
4763
|
+
kind: "supabase",
|
|
4764
|
+
edgeType: "CALLS",
|
|
4765
|
+
// `createClient(...)` from @supabase/supabase-js (or createServerClient /
|
|
4766
|
+
// createBrowserClient from @supabase/ssr) with the import in scope — a
|
|
4767
|
+
// framework-aware recognizer matched the SDK shape. Verified-call-site
|
|
4768
|
+
// tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
|
|
4769
|
+
confidenceKind: "verified-call-site",
|
|
4770
|
+
evidence: {
|
|
4771
|
+
file: import_node_path27.default.relative(serviceDir, file.path),
|
|
4772
|
+
line,
|
|
4773
|
+
snippet: snippet(file.content, line)
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
return out;
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4610
4780
|
// src/extract/calls/index.ts
|
|
4611
4781
|
function edgeTypeFromEndpoint(ep) {
|
|
4612
4782
|
switch (ep.edgeType) {
|
|
4613
4783
|
case "PUBLISHES_TO":
|
|
4614
|
-
return
|
|
4784
|
+
return import_types17.EdgeType.PUBLISHES_TO;
|
|
4615
4785
|
case "CONSUMES_FROM":
|
|
4616
|
-
return
|
|
4786
|
+
return import_types17.EdgeType.CONSUMES_FROM;
|
|
4617
4787
|
default:
|
|
4618
|
-
return
|
|
4788
|
+
return import_types17.EdgeType.CALLS;
|
|
4619
4789
|
}
|
|
4620
4790
|
}
|
|
4621
4791
|
function isAwsKind(kind) {
|
|
@@ -4635,6 +4805,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4635
4805
|
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
4636
4806
|
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
4637
4807
|
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
4808
|
+
endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
|
|
4638
4809
|
}
|
|
4639
4810
|
if (endpoints.length === 0) continue;
|
|
4640
4811
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -4642,7 +4813,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4642
4813
|
if (!graph.hasNode(ep.infraId)) {
|
|
4643
4814
|
const node = {
|
|
4644
4815
|
id: ep.infraId,
|
|
4645
|
-
type:
|
|
4816
|
+
type: import_types17.NodeType.InfraNode,
|
|
4646
4817
|
name: ep.name,
|
|
4647
4818
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
4648
4819
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -4654,7 +4825,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4654
4825
|
nodesAdded++;
|
|
4655
4826
|
}
|
|
4656
4827
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
4657
|
-
const confidence = (0,
|
|
4828
|
+
const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
|
|
4658
4829
|
const relFile = toPosix2(ep.evidence.file);
|
|
4659
4830
|
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
4660
4831
|
graph,
|
|
@@ -4664,7 +4835,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4664
4835
|
);
|
|
4665
4836
|
nodesAdded += n;
|
|
4666
4837
|
edgesAdded += e;
|
|
4667
|
-
if (!(0,
|
|
4838
|
+
if (!(0, import_types17.passesExtractedFloor)(confidence)) {
|
|
4668
4839
|
noteExtractedDropped({
|
|
4669
4840
|
source: fileNodeId,
|
|
4670
4841
|
target: ep.infraId,
|
|
@@ -4684,7 +4855,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4684
4855
|
source: fileNodeId,
|
|
4685
4856
|
target: ep.infraId,
|
|
4686
4857
|
type: edgeType,
|
|
4687
|
-
provenance:
|
|
4858
|
+
provenance: import_types17.Provenance.EXTRACTED,
|
|
4688
4859
|
confidence,
|
|
4689
4860
|
evidence: ep.evidence
|
|
4690
4861
|
};
|
|
@@ -4709,16 +4880,16 @@ init_cjs_shims();
|
|
|
4709
4880
|
|
|
4710
4881
|
// src/extract/infra/docker-compose.ts
|
|
4711
4882
|
init_cjs_shims();
|
|
4712
|
-
var
|
|
4713
|
-
var
|
|
4883
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
4884
|
+
var import_types19 = require("@neat.is/types");
|
|
4714
4885
|
|
|
4715
4886
|
// src/extract/infra/shared.ts
|
|
4716
4887
|
init_cjs_shims();
|
|
4717
|
-
var
|
|
4888
|
+
var import_types18 = require("@neat.is/types");
|
|
4718
4889
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
4719
4890
|
return {
|
|
4720
|
-
id: (0,
|
|
4721
|
-
type:
|
|
4891
|
+
id: (0, import_types18.infraId)(kind, name),
|
|
4892
|
+
type: import_types18.NodeType.InfraNode,
|
|
4722
4893
|
name,
|
|
4723
4894
|
provider,
|
|
4724
4895
|
kind,
|
|
@@ -4747,7 +4918,7 @@ function dependsOnList(value) {
|
|
|
4747
4918
|
}
|
|
4748
4919
|
function serviceNameToServiceNode(name, services) {
|
|
4749
4920
|
for (const s of services) {
|
|
4750
|
-
if (s.node.name === name ||
|
|
4921
|
+
if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
|
|
4751
4922
|
}
|
|
4752
4923
|
return null;
|
|
4753
4924
|
}
|
|
@@ -4756,7 +4927,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4756
4927
|
let edgesAdded = 0;
|
|
4757
4928
|
let composePath = null;
|
|
4758
4929
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
4759
|
-
const abs =
|
|
4930
|
+
const abs = import_node_path28.default.join(scanPath, name);
|
|
4760
4931
|
if (await exists(abs)) {
|
|
4761
4932
|
composePath = abs;
|
|
4762
4933
|
break;
|
|
@@ -4769,13 +4940,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4769
4940
|
} catch (err) {
|
|
4770
4941
|
recordExtractionError(
|
|
4771
4942
|
"infra docker-compose",
|
|
4772
|
-
|
|
4943
|
+
import_node_path28.default.relative(scanPath, composePath),
|
|
4773
4944
|
err
|
|
4774
4945
|
);
|
|
4775
4946
|
return { nodesAdded, edgesAdded };
|
|
4776
4947
|
}
|
|
4777
4948
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
4778
|
-
const evidenceFile =
|
|
4949
|
+
const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
|
|
4779
4950
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
4780
4951
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
4781
4952
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -4797,15 +4968,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4797
4968
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
4798
4969
|
const targetId = composeNameToNodeId.get(dep);
|
|
4799
4970
|
if (!targetId) continue;
|
|
4800
|
-
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId,
|
|
4971
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
|
|
4801
4972
|
if (graph.hasEdge(edgeId)) continue;
|
|
4802
4973
|
const edge = {
|
|
4803
4974
|
id: edgeId,
|
|
4804
4975
|
source: sourceId,
|
|
4805
4976
|
target: targetId,
|
|
4806
|
-
type:
|
|
4807
|
-
provenance:
|
|
4808
|
-
confidence: (0,
|
|
4977
|
+
type: import_types19.EdgeType.DEPENDS_ON,
|
|
4978
|
+
provenance: import_types19.Provenance.EXTRACTED,
|
|
4979
|
+
confidence: (0, import_types19.confidenceForExtracted)("structural"),
|
|
4809
4980
|
evidence: { file: evidenceFile }
|
|
4810
4981
|
};
|
|
4811
4982
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4817,9 +4988,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4817
4988
|
|
|
4818
4989
|
// src/extract/infra/dockerfile.ts
|
|
4819
4990
|
init_cjs_shims();
|
|
4820
|
-
var
|
|
4991
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
4821
4992
|
var import_node_fs15 = require("fs");
|
|
4822
|
-
var
|
|
4993
|
+
var import_types20 = require("@neat.is/types");
|
|
4823
4994
|
function runtimeImage(content) {
|
|
4824
4995
|
const lines = content.split("\n");
|
|
4825
4996
|
let last = null;
|
|
@@ -4838,7 +5009,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4838
5009
|
let nodesAdded = 0;
|
|
4839
5010
|
let edgesAdded = 0;
|
|
4840
5011
|
for (const service of services) {
|
|
4841
|
-
const dockerfilePath =
|
|
5012
|
+
const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
|
|
4842
5013
|
if (!await exists(dockerfilePath)) continue;
|
|
4843
5014
|
let content;
|
|
4844
5015
|
try {
|
|
@@ -4846,7 +5017,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4846
5017
|
} catch (err) {
|
|
4847
5018
|
recordExtractionError(
|
|
4848
5019
|
"infra dockerfile",
|
|
4849
|
-
|
|
5020
|
+
import_node_path29.default.relative(scanPath, dockerfilePath),
|
|
4850
5021
|
err
|
|
4851
5022
|
);
|
|
4852
5023
|
continue;
|
|
@@ -4858,7 +5029,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4858
5029
|
graph.addNode(node.id, node);
|
|
4859
5030
|
nodesAdded++;
|
|
4860
5031
|
}
|
|
4861
|
-
const relDockerfile = toPosix2(
|
|
5032
|
+
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
4862
5033
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4863
5034
|
graph,
|
|
4864
5035
|
service.pkg.name,
|
|
@@ -4867,17 +5038,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4867
5038
|
);
|
|
4868
5039
|
nodesAdded += fn;
|
|
4869
5040
|
edgesAdded += fe;
|
|
4870
|
-
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id,
|
|
5041
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
|
|
4871
5042
|
if (!graph.hasEdge(edgeId)) {
|
|
4872
5043
|
const edge = {
|
|
4873
5044
|
id: edgeId,
|
|
4874
5045
|
source: fileNodeId,
|
|
4875
5046
|
target: node.id,
|
|
4876
|
-
type:
|
|
4877
|
-
provenance:
|
|
4878
|
-
confidence: (0,
|
|
5047
|
+
type: import_types20.EdgeType.RUNS_ON,
|
|
5048
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5049
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
4879
5050
|
evidence: {
|
|
4880
|
-
file: toPosix2(
|
|
5051
|
+
file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
|
|
4881
5052
|
}
|
|
4882
5053
|
};
|
|
4883
5054
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4890,7 +5061,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4890
5061
|
// src/extract/infra/terraform.ts
|
|
4891
5062
|
init_cjs_shims();
|
|
4892
5063
|
var import_node_fs16 = require("fs");
|
|
4893
|
-
var
|
|
5064
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4894
5065
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
4895
5066
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4896
5067
|
if (depth > max) return [];
|
|
@@ -4899,11 +5070,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4899
5070
|
for (const entry of entries) {
|
|
4900
5071
|
if (entry.isDirectory()) {
|
|
4901
5072
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
4902
|
-
const child =
|
|
5073
|
+
const child = import_node_path30.default.join(start, entry.name);
|
|
4903
5074
|
if (await isPythonVenvDir(child)) continue;
|
|
4904
5075
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
4905
5076
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
4906
|
-
out.push(
|
|
5077
|
+
out.push(import_node_path30.default.join(start, entry.name));
|
|
4907
5078
|
}
|
|
4908
5079
|
}
|
|
4909
5080
|
return out;
|
|
@@ -4931,7 +5102,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
4931
5102
|
// src/extract/infra/k8s.ts
|
|
4932
5103
|
init_cjs_shims();
|
|
4933
5104
|
var import_node_fs17 = require("fs");
|
|
4934
|
-
var
|
|
5105
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4935
5106
|
var import_yaml3 = require("yaml");
|
|
4936
5107
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
4937
5108
|
Service: "k8s-service",
|
|
@@ -4949,11 +5120,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
4949
5120
|
for (const entry of entries) {
|
|
4950
5121
|
if (entry.isDirectory()) {
|
|
4951
5122
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4952
|
-
const child =
|
|
5123
|
+
const child = import_node_path31.default.join(start, entry.name);
|
|
4953
5124
|
if (await isPythonVenvDir(child)) continue;
|
|
4954
5125
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
4955
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
4956
|
-
out.push(
|
|
5126
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry.name))) {
|
|
5127
|
+
out.push(import_node_path31.default.join(start, entry.name));
|
|
4957
5128
|
}
|
|
4958
5129
|
}
|
|
4959
5130
|
return out;
|
|
@@ -4997,17 +5168,17 @@ async function addInfra(graph, scanPath, services) {
|
|
|
4997
5168
|
}
|
|
4998
5169
|
|
|
4999
5170
|
// src/extract/index.ts
|
|
5000
|
-
var
|
|
5171
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
5001
5172
|
|
|
5002
5173
|
// src/extract/retire.ts
|
|
5003
5174
|
init_cjs_shims();
|
|
5004
5175
|
var import_node_fs18 = require("fs");
|
|
5005
|
-
var
|
|
5006
|
-
var
|
|
5176
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
5177
|
+
var import_types21 = require("@neat.is/types");
|
|
5007
5178
|
function dropOrphanedFileNodes(graph) {
|
|
5008
5179
|
const orphans = [];
|
|
5009
5180
|
graph.forEachNode((id, attrs) => {
|
|
5010
|
-
if (attrs.type !==
|
|
5181
|
+
if (attrs.type !== import_types21.NodeType.FileNode) return;
|
|
5011
5182
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5012
5183
|
orphans.push(id);
|
|
5013
5184
|
}
|
|
@@ -5020,14 +5191,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5020
5191
|
const bases = [scanPath, ...serviceDirs];
|
|
5021
5192
|
graph.forEachEdge((id, attrs) => {
|
|
5022
5193
|
const edge = attrs;
|
|
5023
|
-
if (edge.provenance !==
|
|
5194
|
+
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
5024
5195
|
const evidenceFile = edge.evidence?.file;
|
|
5025
5196
|
if (!evidenceFile) return;
|
|
5026
|
-
if (
|
|
5197
|
+
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
5027
5198
|
if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
|
|
5028
5199
|
return;
|
|
5029
5200
|
}
|
|
5030
|
-
const found = bases.some((base) => (0, import_node_fs18.existsSync)(
|
|
5201
|
+
const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
|
|
5031
5202
|
if (!found) toDrop.push(id);
|
|
5032
5203
|
});
|
|
5033
5204
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -5067,7 +5238,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5067
5238
|
}
|
|
5068
5239
|
const droppedEntries = drainDroppedExtracted();
|
|
5069
5240
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
5070
|
-
const rejectedPath =
|
|
5241
|
+
const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
5071
5242
|
try {
|
|
5072
5243
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
5073
5244
|
} catch (err) {
|
|
@@ -5102,8 +5273,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5102
5273
|
// src/persist.ts
|
|
5103
5274
|
init_cjs_shims();
|
|
5104
5275
|
var import_node_fs19 = require("fs");
|
|
5105
|
-
var
|
|
5106
|
-
var
|
|
5276
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
5277
|
+
var import_types22 = require("@neat.is/types");
|
|
5107
5278
|
var SCHEMA_VERSION = 4;
|
|
5108
5279
|
function migrateV1ToV2(payload) {
|
|
5109
5280
|
const nodes = payload.graph.nodes;
|
|
@@ -5125,12 +5296,12 @@ function migrateV2ToV3(payload) {
|
|
|
5125
5296
|
for (const edge of edges) {
|
|
5126
5297
|
const attrs = edge.attributes;
|
|
5127
5298
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5128
|
-
attrs.provenance =
|
|
5299
|
+
attrs.provenance = import_types22.Provenance.OBSERVED;
|
|
5129
5300
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5130
5301
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5131
5302
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5132
5303
|
if (type && source && target) {
|
|
5133
|
-
const newId = (0,
|
|
5304
|
+
const newId = (0, import_types22.observedEdgeId)(source, target, type);
|
|
5134
5305
|
attrs.id = newId;
|
|
5135
5306
|
if (edge.key) edge.key = newId;
|
|
5136
5307
|
}
|
|
@@ -5139,7 +5310,7 @@ function migrateV2ToV3(payload) {
|
|
|
5139
5310
|
return { ...payload, schemaVersion: 3 };
|
|
5140
5311
|
}
|
|
5141
5312
|
async function ensureDir(filePath) {
|
|
5142
|
-
await import_node_fs19.promises.mkdir(
|
|
5313
|
+
await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
|
|
5143
5314
|
}
|
|
5144
5315
|
async function saveGraphToDisk(graph, outPath) {
|
|
5145
5316
|
await ensureDir(outPath);
|
|
@@ -5216,19 +5387,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5216
5387
|
init_cjs_shims();
|
|
5217
5388
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5218
5389
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5219
|
-
var
|
|
5390
|
+
var import_types25 = require("@neat.is/types");
|
|
5220
5391
|
|
|
5221
5392
|
// src/extend/index.ts
|
|
5222
5393
|
init_cjs_shims();
|
|
5223
5394
|
var import_node_fs21 = require("fs");
|
|
5224
|
-
var
|
|
5395
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5225
5396
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
5226
5397
|
var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
|
|
5227
5398
|
|
|
5228
5399
|
// src/installers/package-manager.ts
|
|
5229
5400
|
init_cjs_shims();
|
|
5230
5401
|
var import_node_fs20 = require("fs");
|
|
5231
|
-
var
|
|
5402
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
5232
5403
|
var import_node_child_process = require("child_process");
|
|
5233
5404
|
var LOCKFILE_PRIORITY = [
|
|
5234
5405
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -5250,22 +5421,22 @@ async function exists2(p) {
|
|
|
5250
5421
|
}
|
|
5251
5422
|
}
|
|
5252
5423
|
async function detectPackageManager(serviceDir) {
|
|
5253
|
-
let dir =
|
|
5424
|
+
let dir = import_node_path35.default.resolve(serviceDir);
|
|
5254
5425
|
const stops = /* @__PURE__ */ new Set();
|
|
5255
5426
|
for (let i = 0; i < 64; i++) {
|
|
5256
5427
|
if (stops.has(dir)) break;
|
|
5257
5428
|
stops.add(dir);
|
|
5258
5429
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
5259
|
-
const lockPath =
|
|
5430
|
+
const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
|
|
5260
5431
|
if (await exists2(lockPath)) {
|
|
5261
5432
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
5262
5433
|
}
|
|
5263
5434
|
}
|
|
5264
|
-
const parent =
|
|
5435
|
+
const parent = import_node_path35.default.dirname(dir);
|
|
5265
5436
|
if (parent === dir) break;
|
|
5266
5437
|
dir = parent;
|
|
5267
5438
|
}
|
|
5268
|
-
return { pm: "npm", cwd:
|
|
5439
|
+
return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
5269
5440
|
}
|
|
5270
5441
|
async function runPackageManagerInstall(cmd) {
|
|
5271
5442
|
return new Promise((resolve) => {
|
|
@@ -5314,7 +5485,7 @@ async function fileExists2(p) {
|
|
|
5314
5485
|
}
|
|
5315
5486
|
}
|
|
5316
5487
|
async function readPackageJson(scanPath) {
|
|
5317
|
-
const pkgPath =
|
|
5488
|
+
const pkgPath = import_node_path36.default.join(scanPath, "package.json");
|
|
5318
5489
|
const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
|
|
5319
5490
|
return JSON.parse(raw);
|
|
5320
5491
|
}
|
|
@@ -5325,11 +5496,11 @@ async function findHookFiles(scanPath) {
|
|
|
5325
5496
|
).sort();
|
|
5326
5497
|
}
|
|
5327
5498
|
function extendLogPath() {
|
|
5328
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
5499
|
+
return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
|
|
5329
5500
|
}
|
|
5330
5501
|
async function appendExtendLog(entry) {
|
|
5331
5502
|
const logPath = extendLogPath();
|
|
5332
|
-
await import_node_fs21.promises.mkdir(
|
|
5503
|
+
await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
|
|
5333
5504
|
await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
5334
5505
|
}
|
|
5335
5506
|
function splicedContent(fileContent, snippet2) {
|
|
@@ -5388,7 +5559,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
5388
5559
|
}
|
|
5389
5560
|
async function describeProjectInstrumentation(ctx) {
|
|
5390
5561
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5391
|
-
const envNeat = await fileExists2(
|
|
5562
|
+
const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
|
|
5392
5563
|
const registryInstrPackages = new Set(
|
|
5393
5564
|
(0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
5394
5565
|
);
|
|
@@ -5410,16 +5581,16 @@ async function applyExtension(ctx, args, options) {
|
|
|
5410
5581
|
);
|
|
5411
5582
|
}
|
|
5412
5583
|
for (const file of hookFiles) {
|
|
5413
|
-
const content = await import_node_fs21.promises.readFile(
|
|
5584
|
+
const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
|
|
5414
5585
|
if (content.includes(args.registration_snippet)) {
|
|
5415
5586
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
5416
5587
|
}
|
|
5417
5588
|
}
|
|
5418
5589
|
const primaryFile = hookFiles[0];
|
|
5419
|
-
const primaryPath =
|
|
5590
|
+
const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
|
|
5420
5591
|
const filesTouched = [];
|
|
5421
5592
|
const depsAdded = [];
|
|
5422
|
-
const pkgPath =
|
|
5593
|
+
const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
|
|
5423
5594
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
5424
5595
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5425
5596
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
@@ -5465,7 +5636,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
5465
5636
|
};
|
|
5466
5637
|
}
|
|
5467
5638
|
for (const file of hookFiles) {
|
|
5468
|
-
const content = await import_node_fs21.promises.readFile(
|
|
5639
|
+
const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
|
|
5469
5640
|
if (content.includes(args.registration_snippet)) {
|
|
5470
5641
|
return {
|
|
5471
5642
|
library: args.library,
|
|
@@ -5487,7 +5658,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
5487
5658
|
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
5488
5659
|
filesTouched.push("package.json");
|
|
5489
5660
|
}
|
|
5490
|
-
const hookContent = await import_node_fs21.promises.readFile(
|
|
5661
|
+
const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
|
|
5491
5662
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5492
5663
|
if (patched) {
|
|
5493
5664
|
filesTouched.push(primaryFile);
|
|
@@ -5508,7 +5679,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5508
5679
|
if (!match) {
|
|
5509
5680
|
return { undone: false, message: "no apply found for library" };
|
|
5510
5681
|
}
|
|
5511
|
-
const pkgPath =
|
|
5682
|
+
const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
|
|
5512
5683
|
if (await fileExists2(pkgPath)) {
|
|
5513
5684
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
5514
5685
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
@@ -5519,7 +5690,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5519
5690
|
}
|
|
5520
5691
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5521
5692
|
for (const file of hookFiles) {
|
|
5522
|
-
const filePath =
|
|
5693
|
+
const filePath = import_node_path36.default.join(ctx.scanPath, file);
|
|
5523
5694
|
const content = await import_node_fs21.promises.readFile(filePath, "utf8");
|
|
5524
5695
|
if (content.includes(match.registration_snippet)) {
|
|
5525
5696
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
@@ -5535,7 +5706,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5535
5706
|
|
|
5536
5707
|
// src/divergences.ts
|
|
5537
5708
|
init_cjs_shims();
|
|
5538
|
-
var
|
|
5709
|
+
var import_types23 = require("@neat.is/types");
|
|
5539
5710
|
function bucketKey(source, target, type) {
|
|
5540
5711
|
return `${type}|${source}|${target}`;
|
|
5541
5712
|
}
|
|
@@ -5543,22 +5714,22 @@ function bucketEdges(graph) {
|
|
|
5543
5714
|
const buckets = /* @__PURE__ */ new Map();
|
|
5544
5715
|
graph.forEachEdge((id, attrs) => {
|
|
5545
5716
|
const e = attrs;
|
|
5546
|
-
const parsed = (0,
|
|
5717
|
+
const parsed = (0, import_types23.parseEdgeId)(id);
|
|
5547
5718
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5548
5719
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5549
5720
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5550
5721
|
switch (provenance) {
|
|
5551
|
-
case
|
|
5722
|
+
case import_types23.Provenance.EXTRACTED:
|
|
5552
5723
|
cur.extracted = e;
|
|
5553
5724
|
break;
|
|
5554
|
-
case
|
|
5725
|
+
case import_types23.Provenance.OBSERVED:
|
|
5555
5726
|
cur.observed = e;
|
|
5556
5727
|
break;
|
|
5557
|
-
case
|
|
5728
|
+
case import_types23.Provenance.INFERRED:
|
|
5558
5729
|
cur.inferred = e;
|
|
5559
5730
|
break;
|
|
5560
5731
|
default:
|
|
5561
|
-
if (e.provenance ===
|
|
5732
|
+
if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
|
|
5562
5733
|
}
|
|
5563
5734
|
buckets.set(key, cur);
|
|
5564
5735
|
});
|
|
@@ -5567,7 +5738,7 @@ function bucketEdges(graph) {
|
|
|
5567
5738
|
function nodeIsFrontier(graph, nodeId) {
|
|
5568
5739
|
if (!graph.hasNode(nodeId)) return false;
|
|
5569
5740
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5570
|
-
return attrs.type ===
|
|
5741
|
+
return attrs.type === import_types23.NodeType.FrontierNode;
|
|
5571
5742
|
}
|
|
5572
5743
|
function clampConfidence(n) {
|
|
5573
5744
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5588,7 +5759,7 @@ function gradedConfidence(edge) {
|
|
|
5588
5759
|
}
|
|
5589
5760
|
function detectMissingDivergences(graph, bucket) {
|
|
5590
5761
|
const out = [];
|
|
5591
|
-
if (bucket.type ===
|
|
5762
|
+
if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
|
|
5592
5763
|
if (bucket.extracted && !bucket.observed) {
|
|
5593
5764
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5594
5765
|
out.push({
|
|
@@ -5629,7 +5800,7 @@ function declaredHostFor(svc) {
|
|
|
5629
5800
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5630
5801
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5631
5802
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5632
|
-
if (e.type ===
|
|
5803
|
+
if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
|
|
5633
5804
|
return true;
|
|
5634
5805
|
}
|
|
5635
5806
|
}
|
|
@@ -5642,10 +5813,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5642
5813
|
const out = [];
|
|
5643
5814
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5644
5815
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5645
|
-
if (edge.type !==
|
|
5646
|
-
if (edge.provenance !==
|
|
5816
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
5817
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5647
5818
|
const target = graph.getNodeAttributes(edge.target);
|
|
5648
|
-
if (target.type !==
|
|
5819
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5649
5820
|
const observedHost = target.host?.trim();
|
|
5650
5821
|
if (!observedHost) continue;
|
|
5651
5822
|
if (observedHost === declaredHost) continue;
|
|
@@ -5667,10 +5838,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5667
5838
|
const deps = svc.dependencies ?? {};
|
|
5668
5839
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5669
5840
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5670
|
-
if (edge.type !==
|
|
5671
|
-
if (edge.provenance !==
|
|
5841
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
5842
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5672
5843
|
const target = graph.getNodeAttributes(edge.target);
|
|
5673
|
-
if (target.type !==
|
|
5844
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5674
5845
|
for (const pair of compatPairs()) {
|
|
5675
5846
|
if (pair.engine !== target.engine) continue;
|
|
5676
5847
|
const declared = deps[pair.driver];
|
|
@@ -5731,7 +5902,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5731
5902
|
}
|
|
5732
5903
|
graph.forEachNode((nodeId, attrs) => {
|
|
5733
5904
|
const n = attrs;
|
|
5734
|
-
if (n.type !==
|
|
5905
|
+
if (n.type !== import_types23.NodeType.ServiceNode) return;
|
|
5735
5906
|
const svc = n;
|
|
5736
5907
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5737
5908
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -5764,7 +5935,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5764
5935
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5765
5936
|
return a.target.localeCompare(b.target);
|
|
5766
5937
|
});
|
|
5767
|
-
return
|
|
5938
|
+
return import_types23.DivergenceResultSchema.parse({
|
|
5768
5939
|
divergences: filtered,
|
|
5769
5940
|
totalAffected: filtered.length,
|
|
5770
5941
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5850,23 +6021,23 @@ function canonicalJson(value) {
|
|
|
5850
6021
|
|
|
5851
6022
|
// src/projects.ts
|
|
5852
6023
|
init_cjs_shims();
|
|
5853
|
-
var
|
|
6024
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5854
6025
|
function pathsForProject(project, baseDir) {
|
|
5855
6026
|
if (project === DEFAULT_PROJECT) {
|
|
5856
6027
|
return {
|
|
5857
|
-
snapshotPath:
|
|
5858
|
-
errorsPath:
|
|
5859
|
-
staleEventsPath:
|
|
5860
|
-
embeddingsCachePath:
|
|
5861
|
-
policyViolationsPath:
|
|
6028
|
+
snapshotPath: import_node_path37.default.join(baseDir, "graph.json"),
|
|
6029
|
+
errorsPath: import_node_path37.default.join(baseDir, "errors.ndjson"),
|
|
6030
|
+
staleEventsPath: import_node_path37.default.join(baseDir, "stale-events.ndjson"),
|
|
6031
|
+
embeddingsCachePath: import_node_path37.default.join(baseDir, "embeddings.json"),
|
|
6032
|
+
policyViolationsPath: import_node_path37.default.join(baseDir, "policy-violations.ndjson")
|
|
5862
6033
|
};
|
|
5863
6034
|
}
|
|
5864
6035
|
return {
|
|
5865
|
-
snapshotPath:
|
|
5866
|
-
errorsPath:
|
|
5867
|
-
staleEventsPath:
|
|
5868
|
-
embeddingsCachePath:
|
|
5869
|
-
policyViolationsPath:
|
|
6036
|
+
snapshotPath: import_node_path37.default.join(baseDir, `${project}.json`),
|
|
6037
|
+
errorsPath: import_node_path37.default.join(baseDir, `errors.${project}.ndjson`),
|
|
6038
|
+
staleEventsPath: import_node_path37.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
6039
|
+
embeddingsCachePath: import_node_path37.default.join(baseDir, `embeddings.${project}.json`),
|
|
6040
|
+
policyViolationsPath: import_node_path37.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
5870
6041
|
};
|
|
5871
6042
|
}
|
|
5872
6043
|
var Projects = class {
|
|
@@ -5904,23 +6075,23 @@ var Projects = class {
|
|
|
5904
6075
|
init_cjs_shims();
|
|
5905
6076
|
var import_node_fs23 = require("fs");
|
|
5906
6077
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5907
|
-
var
|
|
5908
|
-
var
|
|
6078
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
6079
|
+
var import_types24 = require("@neat.is/types");
|
|
5909
6080
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5910
6081
|
var LOCK_RETRY_MS = 50;
|
|
5911
6082
|
function neatHome() {
|
|
5912
6083
|
const override = process.env.NEAT_HOME;
|
|
5913
|
-
if (override && override.length > 0) return
|
|
5914
|
-
return
|
|
6084
|
+
if (override && override.length > 0) return import_node_path38.default.resolve(override);
|
|
6085
|
+
return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
|
|
5915
6086
|
}
|
|
5916
6087
|
function registryPath() {
|
|
5917
|
-
return
|
|
6088
|
+
return import_node_path38.default.join(neatHome(), "projects.json");
|
|
5918
6089
|
}
|
|
5919
6090
|
function registryLockPath() {
|
|
5920
|
-
return
|
|
6091
|
+
return import_node_path38.default.join(neatHome(), "projects.json.lock");
|
|
5921
6092
|
}
|
|
5922
6093
|
function daemonPidPath() {
|
|
5923
|
-
return
|
|
6094
|
+
return import_node_path38.default.join(neatHome(), "neatd.pid");
|
|
5924
6095
|
}
|
|
5925
6096
|
function isPidAliveDefault(pid) {
|
|
5926
6097
|
try {
|
|
@@ -5980,7 +6151,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
5980
6151
|
}
|
|
5981
6152
|
}
|
|
5982
6153
|
async function normalizeProjectPath(input) {
|
|
5983
|
-
const resolved =
|
|
6154
|
+
const resolved = import_node_path38.default.resolve(input);
|
|
5984
6155
|
try {
|
|
5985
6156
|
return await import_node_fs23.promises.realpath(resolved);
|
|
5986
6157
|
} catch {
|
|
@@ -5988,7 +6159,7 @@ async function normalizeProjectPath(input) {
|
|
|
5988
6159
|
}
|
|
5989
6160
|
}
|
|
5990
6161
|
async function writeAtomically(target, contents) {
|
|
5991
|
-
await import_node_fs23.promises.mkdir(
|
|
6162
|
+
await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
|
|
5992
6163
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
5993
6164
|
const fd = await import_node_fs23.promises.open(tmp, "w");
|
|
5994
6165
|
try {
|
|
@@ -6001,7 +6172,7 @@ async function writeAtomically(target, contents) {
|
|
|
6001
6172
|
}
|
|
6002
6173
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
6003
6174
|
const deadline = Date.now() + timeoutMs;
|
|
6004
|
-
await import_node_fs23.promises.mkdir(
|
|
6175
|
+
await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
|
|
6005
6176
|
let probedHolder = false;
|
|
6006
6177
|
while (true) {
|
|
6007
6178
|
try {
|
|
@@ -6054,10 +6225,10 @@ async function readRegistry() {
|
|
|
6054
6225
|
throw err;
|
|
6055
6226
|
}
|
|
6056
6227
|
const parsed = JSON.parse(raw);
|
|
6057
|
-
return
|
|
6228
|
+
return import_types24.RegistryFileSchema.parse(parsed);
|
|
6058
6229
|
}
|
|
6059
6230
|
async function writeRegistry(reg) {
|
|
6060
|
-
const validated =
|
|
6231
|
+
const validated = import_types24.RegistryFileSchema.parse(reg);
|
|
6061
6232
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6062
6233
|
}
|
|
6063
6234
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -6137,6 +6308,58 @@ async function removeProject(name) {
|
|
|
6137
6308
|
return removed;
|
|
6138
6309
|
});
|
|
6139
6310
|
}
|
|
6311
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
6312
|
+
var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
|
|
6313
|
+
function pruneTtlMs() {
|
|
6314
|
+
const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
|
|
6315
|
+
if (!raw) return DEFAULT_PRUNE_TTL_MS;
|
|
6316
|
+
const n = Number.parseInt(raw, 10);
|
|
6317
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
6318
|
+
console.warn(
|
|
6319
|
+
`[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
|
|
6320
|
+
);
|
|
6321
|
+
return DEFAULT_PRUNE_TTL_MS;
|
|
6322
|
+
}
|
|
6323
|
+
async function statPathStatus(p) {
|
|
6324
|
+
try {
|
|
6325
|
+
const stat = await import_node_fs23.promises.stat(p);
|
|
6326
|
+
return stat.isDirectory() ? "present" : "unknown";
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
async function pruneRegistry(opts = {}) {
|
|
6332
|
+
const ttlMs = opts.ttlMs ?? pruneTtlMs();
|
|
6333
|
+
const statPath = opts.statPath ?? statPathStatus;
|
|
6334
|
+
const now = opts.now ?? Date.now;
|
|
6335
|
+
return withLock(async () => {
|
|
6336
|
+
const reg = await readRegistry();
|
|
6337
|
+
const removed = [];
|
|
6338
|
+
const kept = [];
|
|
6339
|
+
for (const entry of reg.projects) {
|
|
6340
|
+
const status2 = await statPath(entry.path);
|
|
6341
|
+
if (status2 !== "gone") {
|
|
6342
|
+
kept.push(entry);
|
|
6343
|
+
continue;
|
|
6344
|
+
}
|
|
6345
|
+
if (ttlMs <= 0) {
|
|
6346
|
+
removed.push(entry);
|
|
6347
|
+
continue;
|
|
6348
|
+
}
|
|
6349
|
+
const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
|
|
6350
|
+
const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
|
|
6351
|
+
if (age > ttlMs) {
|
|
6352
|
+
removed.push(entry);
|
|
6353
|
+
} else {
|
|
6354
|
+
kept.push(entry);
|
|
6355
|
+
}
|
|
6356
|
+
}
|
|
6357
|
+
if (removed.length === 0) return [];
|
|
6358
|
+
reg.projects = kept;
|
|
6359
|
+
await writeRegistry(reg);
|
|
6360
|
+
return removed;
|
|
6361
|
+
});
|
|
6362
|
+
}
|
|
6140
6363
|
|
|
6141
6364
|
// src/streaming.ts
|
|
6142
6365
|
init_cjs_shims();
|
|
@@ -6150,6 +6373,7 @@ function handleSse(req, reply, opts) {
|
|
|
6150
6373
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
6151
6374
|
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
6152
6375
|
reply.raw.flushHeaders?.();
|
|
6376
|
+
reply.raw.write(":open\n\n");
|
|
6153
6377
|
let pending = 0;
|
|
6154
6378
|
let dropped = false;
|
|
6155
6379
|
const closeConnection = () => {
|
|
@@ -6329,11 +6553,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6329
6553
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6330
6554
|
const parsed = [];
|
|
6331
6555
|
for (const c of candidates) {
|
|
6332
|
-
const r =
|
|
6556
|
+
const r = import_types25.DivergenceTypeSchema.safeParse(c);
|
|
6333
6557
|
if (!r.success) {
|
|
6334
6558
|
return reply.code(400).send({
|
|
6335
6559
|
error: `unknown divergence type "${c}"`,
|
|
6336
|
-
allowed:
|
|
6560
|
+
allowed: import_types25.DivergenceTypeSchema.options
|
|
6337
6561
|
});
|
|
6338
6562
|
}
|
|
6339
6563
|
parsed.push(r.data);
|
|
@@ -6546,7 +6770,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6546
6770
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6547
6771
|
let violations = await log.readAll();
|
|
6548
6772
|
if (req.query.severity) {
|
|
6549
|
-
const sev =
|
|
6773
|
+
const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
|
|
6550
6774
|
if (!sev.success) {
|
|
6551
6775
|
return reply.code(400).send({
|
|
6552
6776
|
error: "invalid severity",
|
|
@@ -6563,7 +6787,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6563
6787
|
scope.post("/policies/check", async (req, reply) => {
|
|
6564
6788
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6565
6789
|
if (!proj) return;
|
|
6566
|
-
const parsed =
|
|
6790
|
+
const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6567
6791
|
if (!parsed.success) {
|
|
6568
6792
|
return reply.code(400).send({
|
|
6569
6793
|
error: "invalid /policies/check body",
|
|
@@ -6806,14 +7030,15 @@ init_otel_grpc();
|
|
|
6806
7030
|
// src/daemon.ts
|
|
6807
7031
|
init_cjs_shims();
|
|
6808
7032
|
var import_node_fs25 = require("fs");
|
|
6809
|
-
var
|
|
7033
|
+
var import_node_path42 = __toESM(require("path"), 1);
|
|
7034
|
+
var import_node_module = require("module");
|
|
6810
7035
|
init_otel();
|
|
6811
7036
|
init_auth();
|
|
6812
7037
|
|
|
6813
7038
|
// src/unrouted.ts
|
|
6814
7039
|
init_cjs_shims();
|
|
6815
7040
|
var import_node_fs24 = require("fs");
|
|
6816
|
-
var
|
|
7041
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
6817
7042
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
6818
7043
|
return {
|
|
6819
7044
|
timestamp: now.toISOString(),
|
|
@@ -6823,15 +7048,68 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
6823
7048
|
};
|
|
6824
7049
|
}
|
|
6825
7050
|
async function appendUnroutedSpan(neatHome2, record) {
|
|
6826
|
-
const target =
|
|
7051
|
+
const target = import_node_path41.default.join(neatHome2, "errors.ndjson");
|
|
6827
7052
|
await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
|
|
6828
7053
|
await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
6829
7054
|
}
|
|
6830
7055
|
function unroutedErrorsPath(neatHome2) {
|
|
6831
|
-
return
|
|
7056
|
+
return import_node_path41.default.join(neatHome2, "errors.ndjson");
|
|
6832
7057
|
}
|
|
6833
7058
|
|
|
6834
7059
|
// src/daemon.ts
|
|
7060
|
+
function daemonJsonPath(scanPath) {
|
|
7061
|
+
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
7062
|
+
}
|
|
7063
|
+
function daemonsDiscoveryDir(home) {
|
|
7064
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
7065
|
+
return import_node_path42.default.join(base, "daemons");
|
|
7066
|
+
}
|
|
7067
|
+
function daemonDiscoveryPath(project, home) {
|
|
7068
|
+
return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
7069
|
+
}
|
|
7070
|
+
function sanitizeDiscoveryName(project) {
|
|
7071
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
7072
|
+
}
|
|
7073
|
+
function neatHomeFromEnv() {
|
|
7074
|
+
const env = process.env.NEAT_HOME;
|
|
7075
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
7076
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7077
|
+
return import_node_path42.default.join(home, ".neat");
|
|
7078
|
+
}
|
|
7079
|
+
function resolveNeatVersion() {
|
|
7080
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
7081
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
7082
|
+
}
|
|
7083
|
+
try {
|
|
7084
|
+
const req = (0, import_node_module.createRequire)(importMetaUrl);
|
|
7085
|
+
const pkg = req("../package.json");
|
|
7086
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
7087
|
+
} catch {
|
|
7088
|
+
return "0.0.0";
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
async function writeDaemonRecord(record, home) {
|
|
7092
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
7093
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
7094
|
+
try {
|
|
7095
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
7096
|
+
} catch (err) {
|
|
7097
|
+
console.warn(
|
|
7098
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
7099
|
+
);
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
async function clearDaemonRecord(record, home) {
|
|
7103
|
+
try {
|
|
7104
|
+
const stopped = { ...record, status: "stopped" };
|
|
7105
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
7106
|
+
} catch {
|
|
7107
|
+
}
|
|
7108
|
+
try {
|
|
7109
|
+
await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
7110
|
+
} catch {
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
6835
7113
|
function teardownSlot(slot) {
|
|
6836
7114
|
try {
|
|
6837
7115
|
slot.stopPersist();
|
|
@@ -6843,11 +7121,11 @@ function teardownSlot(slot) {
|
|
|
6843
7121
|
}
|
|
6844
7122
|
}
|
|
6845
7123
|
function neatHomeFor(opts) {
|
|
6846
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
7124
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
|
|
6847
7125
|
const env = process.env.NEAT_HOME;
|
|
6848
|
-
if (env && env.length > 0) return
|
|
7126
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
6849
7127
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6850
|
-
return
|
|
7128
|
+
return import_node_path42.default.join(home, ".neat");
|
|
6851
7129
|
}
|
|
6852
7130
|
function routeSpanToProject(serviceName, projects) {
|
|
6853
7131
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -6882,7 +7160,7 @@ function isTokenContained(needle, haystack) {
|
|
|
6882
7160
|
return tokens.includes(needle);
|
|
6883
7161
|
}
|
|
6884
7162
|
async function bootstrapProject(entry) {
|
|
6885
|
-
const paths = pathsForProject(entry.name,
|
|
7163
|
+
const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
|
|
6886
7164
|
try {
|
|
6887
7165
|
const stat = await import_node_fs25.promises.stat(entry.path);
|
|
6888
7166
|
if (!stat.isDirectory()) {
|
|
@@ -6948,6 +7226,23 @@ function resolveOtlpPort(opts) {
|
|
|
6948
7226
|
}
|
|
6949
7227
|
return 4318;
|
|
6950
7228
|
}
|
|
7229
|
+
function resolveWebPort() {
|
|
7230
|
+
const env = process.env.NEAT_WEB_PORT;
|
|
7231
|
+
if (env && env.length > 0) {
|
|
7232
|
+
const n = Number.parseInt(env, 10);
|
|
7233
|
+
if (Number.isFinite(n)) return n;
|
|
7234
|
+
}
|
|
7235
|
+
return 6328;
|
|
7236
|
+
}
|
|
7237
|
+
function portFromListenAddress(address, fallback) {
|
|
7238
|
+
try {
|
|
7239
|
+
const port = new URL(address).port;
|
|
7240
|
+
const n = Number.parseInt(port, 10);
|
|
7241
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
7242
|
+
} catch {
|
|
7243
|
+
}
|
|
7244
|
+
return fallback;
|
|
7245
|
+
}
|
|
6951
7246
|
function resolveHost(opts, authTokenSet) {
|
|
6952
7247
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
6953
7248
|
const env = process.env.HOST;
|
|
@@ -6958,14 +7253,25 @@ function resolveHost(opts, authTokenSet) {
|
|
|
6958
7253
|
async function startDaemon(opts = {}) {
|
|
6959
7254
|
const home = neatHomeFor(opts);
|
|
6960
7255
|
const regPath = registryPath();
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
7256
|
+
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;
|
|
7257
|
+
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;
|
|
7258
|
+
const singleProject = projectArg;
|
|
7259
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
|
|
7260
|
+
if (singleProject && !singleProjectPath) {
|
|
6964
7261
|
throw new Error(
|
|
6965
|
-
`neatd:
|
|
7262
|
+
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
6966
7263
|
);
|
|
6967
7264
|
}
|
|
6968
|
-
|
|
7265
|
+
if (!singleProject) {
|
|
7266
|
+
try {
|
|
7267
|
+
await import_node_fs25.promises.access(regPath);
|
|
7268
|
+
} catch {
|
|
7269
|
+
throw new Error(
|
|
7270
|
+
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
7271
|
+
);
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
const pidPath = import_node_path42.default.join(home, "neatd.pid");
|
|
6969
7275
|
await writeAtomically(pidPath, `${process.pid}
|
|
6970
7276
|
`);
|
|
6971
7277
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -7053,8 +7359,37 @@ async function startDaemon(opts = {}) {
|
|
|
7053
7359
|
});
|
|
7054
7360
|
}
|
|
7055
7361
|
}
|
|
7362
|
+
async function enumerateProjects() {
|
|
7363
|
+
if (singleProject && singleProjectPath) {
|
|
7364
|
+
return [
|
|
7365
|
+
{
|
|
7366
|
+
name: singleProject,
|
|
7367
|
+
path: singleProjectPath,
|
|
7368
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7369
|
+
languages: [],
|
|
7370
|
+
status: "active"
|
|
7371
|
+
}
|
|
7372
|
+
];
|
|
7373
|
+
}
|
|
7374
|
+
return listProjects();
|
|
7375
|
+
}
|
|
7056
7376
|
async function loadAll() {
|
|
7057
|
-
|
|
7377
|
+
if (!singleProject) {
|
|
7378
|
+
try {
|
|
7379
|
+
const pruned = await pruneRegistry();
|
|
7380
|
+
for (const entry of pruned) {
|
|
7381
|
+
console.log(
|
|
7382
|
+
`neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
|
|
7383
|
+
);
|
|
7384
|
+
slots.delete(entry.name);
|
|
7385
|
+
bootstrapStatus.delete(entry.name);
|
|
7386
|
+
bootstrapStartedAt.delete(entry.name);
|
|
7387
|
+
}
|
|
7388
|
+
} catch (err) {
|
|
7389
|
+
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
const projects = await enumerateProjects();
|
|
7058
7393
|
const seen = /* @__PURE__ */ new Set();
|
|
7059
7394
|
const pending = [];
|
|
7060
7395
|
for (const entry of projects) {
|
|
@@ -7079,7 +7414,7 @@ async function startDaemon(opts = {}) {
|
|
|
7079
7414
|
}
|
|
7080
7415
|
await Promise.allSettled(pending);
|
|
7081
7416
|
}
|
|
7082
|
-
const initialEntries = await
|
|
7417
|
+
const initialEntries = await enumerateProjects().catch(() => []);
|
|
7083
7418
|
for (const entry of initialEntries) {
|
|
7084
7419
|
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
7085
7420
|
bootstrapStartedAt.set(entry.name, Date.now());
|
|
@@ -7089,6 +7424,7 @@ async function startDaemon(opts = {}) {
|
|
|
7089
7424
|
let otlpApp = null;
|
|
7090
7425
|
let restAddress = "";
|
|
7091
7426
|
let otlpAddress = "";
|
|
7427
|
+
let daemonRecord = null;
|
|
7092
7428
|
if (bind) {
|
|
7093
7429
|
const auth = readAuthEnv();
|
|
7094
7430
|
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
@@ -7113,6 +7449,20 @@ async function startDaemon(opts = {}) {
|
|
|
7113
7449
|
}
|
|
7114
7450
|
}
|
|
7115
7451
|
});
|
|
7452
|
+
if (singleProject) {
|
|
7453
|
+
restApp.addHook("onSend", async (req, _reply, payload) => {
|
|
7454
|
+
if (req.url.split("?")[0] !== "/health") return payload;
|
|
7455
|
+
if (typeof payload !== "string") return payload;
|
|
7456
|
+
try {
|
|
7457
|
+
const body = JSON.parse(payload);
|
|
7458
|
+
if (typeof body !== "object" || body === null) return payload;
|
|
7459
|
+
body.project = singleProject;
|
|
7460
|
+
return JSON.stringify(body);
|
|
7461
|
+
} catch {
|
|
7462
|
+
return payload;
|
|
7463
|
+
}
|
|
7464
|
+
});
|
|
7465
|
+
}
|
|
7116
7466
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7117
7467
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
7118
7468
|
} catch (err) {
|
|
@@ -7128,6 +7478,25 @@ async function startDaemon(opts = {}) {
|
|
|
7128
7478
|
);
|
|
7129
7479
|
}
|
|
7130
7480
|
async function resolveTargetSlot(serviceName, traceId) {
|
|
7481
|
+
if (singleProject) {
|
|
7482
|
+
let slot2 = slots.get(singleProject);
|
|
7483
|
+
if (!slot2) {
|
|
7484
|
+
slot2 = await tryRecoverSlot({
|
|
7485
|
+
name: singleProject,
|
|
7486
|
+
path: singleProjectPath,
|
|
7487
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7488
|
+
languages: [],
|
|
7489
|
+
status: "active"
|
|
7490
|
+
});
|
|
7491
|
+
} else if (slot2.status === "broken") {
|
|
7492
|
+
slot2 = await tryRecoverSlot(slot2.entry);
|
|
7493
|
+
}
|
|
7494
|
+
if (!slot2 || slot2.status !== "active") {
|
|
7495
|
+
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7496
|
+
return null;
|
|
7497
|
+
}
|
|
7498
|
+
return slot2;
|
|
7499
|
+
}
|
|
7131
7500
|
const liveEntries = await listProjects().catch(() => []);
|
|
7132
7501
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
7133
7502
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
@@ -7230,6 +7599,42 @@ async function startDaemon(opts = {}) {
|
|
|
7230
7599
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
7231
7600
|
);
|
|
7232
7601
|
}
|
|
7602
|
+
if (singleProject && singleProjectPath) {
|
|
7603
|
+
const ports = {
|
|
7604
|
+
rest: portFromListenAddress(restAddress, restPort),
|
|
7605
|
+
otlp: portFromListenAddress(otlpAddress, otlpPort),
|
|
7606
|
+
// The daemon doesn't bind the web port itself (neatd spawns the web
|
|
7607
|
+
// child); it records the allocated value passed through so the
|
|
7608
|
+
// dashboard and `neat ps` agree on where to look.
|
|
7609
|
+
web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
|
|
7610
|
+
};
|
|
7611
|
+
daemonRecord = {
|
|
7612
|
+
project: singleProject,
|
|
7613
|
+
projectPath: singleProjectPath,
|
|
7614
|
+
pid: process.pid,
|
|
7615
|
+
status: "running",
|
|
7616
|
+
ports,
|
|
7617
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7618
|
+
neatVersion: resolveNeatVersion()
|
|
7619
|
+
};
|
|
7620
|
+
try {
|
|
7621
|
+
await writeDaemonRecord(daemonRecord, home);
|
|
7622
|
+
console.log(
|
|
7623
|
+
`neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
|
|
7624
|
+
);
|
|
7625
|
+
} catch (err) {
|
|
7626
|
+
for (const slot of slots.values()) teardownSlot(slot);
|
|
7627
|
+
if (restApp) await restApp.close().catch(() => {
|
|
7628
|
+
});
|
|
7629
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
7630
|
+
});
|
|
7631
|
+
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7632
|
+
});
|
|
7633
|
+
throw new Error(
|
|
7634
|
+
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
7635
|
+
);
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7233
7638
|
}
|
|
7234
7639
|
const initialBootstrap = loadAll().catch((err) => {
|
|
7235
7640
|
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
@@ -7269,9 +7674,9 @@ async function startDaemon(opts = {}) {
|
|
|
7269
7674
|
const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
|
|
7270
7675
|
let registryWatcher = null;
|
|
7271
7676
|
let reloadTimer = null;
|
|
7272
|
-
try {
|
|
7273
|
-
const regDir =
|
|
7274
|
-
const regBase =
|
|
7677
|
+
if (!singleProject) try {
|
|
7678
|
+
const regDir = import_node_path42.default.dirname(regPath);
|
|
7679
|
+
const regBase = import_node_path42.default.basename(regPath);
|
|
7275
7680
|
registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
|
|
7276
7681
|
if (filename !== null && filename !== regBase) return;
|
|
7277
7682
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
@@ -7312,6 +7717,9 @@ async function startDaemon(opts = {}) {
|
|
|
7312
7717
|
for (const slot of slots.values()) {
|
|
7313
7718
|
teardownSlot(slot);
|
|
7314
7719
|
}
|
|
7720
|
+
if (daemonRecord) {
|
|
7721
|
+
await clearDaemonRecord(daemonRecord, home);
|
|
7722
|
+
}
|
|
7315
7723
|
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7316
7724
|
});
|
|
7317
7725
|
};
|
|
@@ -7323,7 +7731,8 @@ async function startDaemon(opts = {}) {
|
|
|
7323
7731
|
restAddress,
|
|
7324
7732
|
otlpAddress,
|
|
7325
7733
|
bootstrap: tracker,
|
|
7326
|
-
initialBootstrap
|
|
7734
|
+
initialBootstrap,
|
|
7735
|
+
daemonRecord
|
|
7327
7736
|
};
|
|
7328
7737
|
}
|
|
7329
7738
|
// Annotate the CommonJS export names for ESM import in node:
|