@neat.is/core 0.3.8 → 0.4.0
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-7TYESDAI.js → chunk-4V23KYOP.js} +33 -4
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-IXIFJKMM.js} +2 -2
- package/dist/{chunk-LQ3JFBTX.js → chunk-N6RPINEJ.js} +5 -4
- package/dist/{chunk-LQ3JFBTX.js.map → chunk-N6RPINEJ.js.map} +1 -1
- package/dist/{chunk-CZ3T6TE2.js → chunk-UPW4CMOH.js} +120 -28
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +916 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +765 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +153 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +153 -31
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +282 -160
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-IXIFJKMM.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
package/dist/server.cjs
CHANGED
|
@@ -54,6 +54,7 @@ function mountBearerAuth(app, opts) {
|
|
|
54
54
|
if (opts.trustProxy) return;
|
|
55
55
|
const expected = Buffer.from(opts.token, "utf8");
|
|
56
56
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
57
|
+
const publicRead = opts.publicRead === true;
|
|
57
58
|
app.addHook("preHandler", (req, reply, done) => {
|
|
58
59
|
const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
59
60
|
for (const suffix of suffixes) {
|
|
@@ -62,6 +63,10 @@ function mountBearerAuth(app, opts) {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
67
|
+
done();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
65
70
|
const header = req.headers.authorization;
|
|
66
71
|
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
67
72
|
void reply.code(401).send({ error: "unauthorized" });
|
|
@@ -75,16 +80,21 @@ function mountBearerAuth(app, opts) {
|
|
|
75
80
|
done();
|
|
76
81
|
});
|
|
77
82
|
}
|
|
83
|
+
function parseBoolEnv(v) {
|
|
84
|
+
if (!v) return false;
|
|
85
|
+
return v === "true" || v === "1";
|
|
86
|
+
}
|
|
78
87
|
function readAuthEnv(env = process.env) {
|
|
79
88
|
const t = env.NEAT_AUTH_TOKEN;
|
|
80
89
|
const ot = env.NEAT_OTEL_TOKEN;
|
|
81
90
|
return {
|
|
82
91
|
authToken: t && t.length > 0 ? t : void 0,
|
|
83
92
|
otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
|
|
84
|
-
trustProxy: env.NEAT_AUTH_PROXY === "true"
|
|
93
|
+
trustProxy: env.NEAT_AUTH_PROXY === "true",
|
|
94
|
+
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
85
95
|
};
|
|
86
96
|
}
|
|
87
|
-
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, DEFAULT_UNAUTH_SUFFIXES;
|
|
97
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
|
|
88
98
|
var init_auth = __esm({
|
|
89
99
|
"src/auth.ts"() {
|
|
90
100
|
"use strict";
|
|
@@ -104,7 +114,13 @@ var init_auth = __esm({
|
|
|
104
114
|
this.name = "BindAuthorityError";
|
|
105
115
|
}
|
|
106
116
|
};
|
|
107
|
-
|
|
117
|
+
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
118
|
+
DEFAULT_UNAUTH_SUFFIXES = [
|
|
119
|
+
"/health",
|
|
120
|
+
"/healthz",
|
|
121
|
+
"/readyz",
|
|
122
|
+
"/api/config"
|
|
123
|
+
];
|
|
108
124
|
}
|
|
109
125
|
});
|
|
110
126
|
|
|
@@ -307,6 +323,15 @@ function isoFromUnixNano(nanos) {
|
|
|
307
323
|
return void 0;
|
|
308
324
|
}
|
|
309
325
|
}
|
|
326
|
+
function pickEnv(spanAttrs, resourceAttrs) {
|
|
327
|
+
for (const attrs of [spanAttrs, resourceAttrs]) {
|
|
328
|
+
for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
|
|
329
|
+
const v = attrs[key];
|
|
330
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return ENV_FALLBACK;
|
|
334
|
+
}
|
|
310
335
|
function parseOtlpRequest(body) {
|
|
311
336
|
const out = [];
|
|
312
337
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -326,6 +351,7 @@ function parseOtlpRequest(body) {
|
|
|
326
351
|
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
327
352
|
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
328
353
|
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
354
|
+
env: pickEnv(attrs, resourceAttrs),
|
|
329
355
|
attributes: attrs,
|
|
330
356
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
331
357
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
@@ -464,7 +490,7 @@ async function buildOtelReceiver(opts) {
|
|
|
464
490
|
};
|
|
465
491
|
return decorated;
|
|
466
492
|
}
|
|
467
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
493
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
468
494
|
var init_otel = __esm({
|
|
469
495
|
"src/otel.ts"() {
|
|
470
496
|
"use strict";
|
|
@@ -474,6 +500,9 @@ var init_otel = __esm({
|
|
|
474
500
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
475
501
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
476
502
|
init_auth();
|
|
503
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
504
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
505
|
+
ENV_FALLBACK = "unknown";
|
|
477
506
|
exportTraceServiceRequestType = null;
|
|
478
507
|
exportTraceServiceResponseType = null;
|
|
479
508
|
cachedProtobufResponseBody = null;
|
|
@@ -506,7 +535,7 @@ function getGraph(project = DEFAULT_PROJECT) {
|
|
|
506
535
|
init_cjs_shims();
|
|
507
536
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
508
537
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
509
|
-
var
|
|
538
|
+
var import_types22 = require("@neat.is/types");
|
|
510
539
|
|
|
511
540
|
// src/divergences.ts
|
|
512
541
|
init_cjs_shims();
|
|
@@ -1763,52 +1792,64 @@ function cacheSpanService(span, now) {
|
|
|
1763
1792
|
if (!span.traceId || !span.spanId) return;
|
|
1764
1793
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1765
1794
|
parentSpanCache.delete(key);
|
|
1766
|
-
parentSpanCache.set(key, {
|
|
1795
|
+
parentSpanCache.set(key, {
|
|
1796
|
+
service: span.service,
|
|
1797
|
+
env: span.env ?? "unknown",
|
|
1798
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1799
|
+
});
|
|
1767
1800
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1768
1801
|
const oldest = parentSpanCache.keys().next().value;
|
|
1769
1802
|
if (!oldest) break;
|
|
1770
1803
|
parentSpanCache.delete(oldest);
|
|
1771
1804
|
}
|
|
1772
1805
|
}
|
|
1773
|
-
function
|
|
1806
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1774
1807
|
const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1775
1808
|
if (!entry) return null;
|
|
1776
1809
|
if (entry.expiresAt <= now) {
|
|
1777
1810
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1778
1811
|
return null;
|
|
1779
1812
|
}
|
|
1780
|
-
return entry.service;
|
|
1813
|
+
return { service: entry.service, env: entry.env };
|
|
1781
1814
|
}
|
|
1782
|
-
function resolveServiceId(graph, host) {
|
|
1783
|
-
const
|
|
1784
|
-
if (graph.hasNode(
|
|
1785
|
-
|
|
1815
|
+
function resolveServiceId(graph, host, env) {
|
|
1816
|
+
const envTagged = (0, import_types4.serviceId)(host, env);
|
|
1817
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1818
|
+
const envLess = (0, import_types4.serviceId)(host);
|
|
1819
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1820
|
+
let sameEnv = null;
|
|
1821
|
+
let envLessMatch = null;
|
|
1822
|
+
let anyMatch = null;
|
|
1786
1823
|
graph.forEachNode((id, attrs) => {
|
|
1787
|
-
if (
|
|
1824
|
+
if (sameEnv) return;
|
|
1788
1825
|
const a = attrs;
|
|
1789
1826
|
if (a.type !== import_types4.NodeType.ServiceNode) return;
|
|
1790
|
-
|
|
1791
|
-
|
|
1827
|
+
const matchesByName = a.name === host;
|
|
1828
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1829
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1830
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1831
|
+
if (nodeEnv === env) {
|
|
1832
|
+
sameEnv = id;
|
|
1792
1833
|
return;
|
|
1793
1834
|
}
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1835
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1836
|
+
else if (!anyMatch) anyMatch = id;
|
|
1797
1837
|
});
|
|
1798
|
-
return
|
|
1838
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1799
1839
|
}
|
|
1800
1840
|
function frontierIdFor(host) {
|
|
1801
1841
|
return (0, import_types4.frontierId)(host);
|
|
1802
1842
|
}
|
|
1803
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1804
|
-
const id = (0, import_types4.serviceId)(serviceName);
|
|
1843
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1844
|
+
const id = (0, import_types4.serviceId)(serviceName, env);
|
|
1805
1845
|
if (graph.hasNode(id)) return id;
|
|
1806
1846
|
const node = {
|
|
1807
1847
|
id,
|
|
1808
1848
|
type: import_types4.NodeType.ServiceNode,
|
|
1809
1849
|
name: serviceName,
|
|
1810
1850
|
language: "unknown",
|
|
1811
|
-
discoveredVia: "otel"
|
|
1851
|
+
discoveredVia: "otel",
|
|
1852
|
+
...env !== "unknown" ? { env } : {}
|
|
1812
1853
|
};
|
|
1813
1854
|
graph.addNode(id, node);
|
|
1814
1855
|
return id;
|
|
@@ -1943,7 +1984,8 @@ function sanitizeAttributes(attrs) {
|
|
|
1943
1984
|
async function handleSpan(ctx, span) {
|
|
1944
1985
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1945
1986
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1946
|
-
const
|
|
1987
|
+
const env = span.env ?? "unknown";
|
|
1988
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1947
1989
|
const isError = span.statusCode === 2;
|
|
1948
1990
|
cacheSpanService(span, nowMs);
|
|
1949
1991
|
let affectedNode = sourceId;
|
|
@@ -1966,7 +2008,7 @@ async function handleSpan(ctx, span) {
|
|
|
1966
2008
|
const host = pickAddress(span);
|
|
1967
2009
|
let resolvedViaAddress = false;
|
|
1968
2010
|
if (host && host !== span.service) {
|
|
1969
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
2011
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1970
2012
|
if (targetId && targetId !== sourceId) {
|
|
1971
2013
|
upsertObservedEdge(
|
|
1972
2014
|
ctx.graph,
|
|
@@ -1993,9 +2035,9 @@ async function handleSpan(ctx, span) {
|
|
|
1993
2035
|
}
|
|
1994
2036
|
}
|
|
1995
2037
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1996
|
-
const
|
|
1997
|
-
if (
|
|
1998
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
2038
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
2039
|
+
if (parent && parent.service !== span.service) {
|
|
2040
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1999
2041
|
upsertObservedEdge(
|
|
2000
2042
|
ctx.graph,
|
|
2001
2043
|
import_types4.EdgeType.CALLS,
|
|
@@ -2191,6 +2233,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
2191
2233
|
throw err;
|
|
2192
2234
|
}
|
|
2193
2235
|
}
|
|
2236
|
+
function mergeSnapshot(graph, snapshot) {
|
|
2237
|
+
const exported = snapshot.graph;
|
|
2238
|
+
let nodesAdded = 0;
|
|
2239
|
+
let edgesAdded = 0;
|
|
2240
|
+
for (const node of exported.nodes ?? []) {
|
|
2241
|
+
if (graph.hasNode(node.key)) continue;
|
|
2242
|
+
if (!node.attributes) continue;
|
|
2243
|
+
graph.addNode(node.key, node.attributes);
|
|
2244
|
+
nodesAdded++;
|
|
2245
|
+
}
|
|
2246
|
+
for (const edge of exported.edges ?? []) {
|
|
2247
|
+
const attrs = edge.attributes;
|
|
2248
|
+
if (!attrs) continue;
|
|
2249
|
+
const id = edge.key ?? attrs.id;
|
|
2250
|
+
if (!id) continue;
|
|
2251
|
+
if (graph.hasEdge(id)) continue;
|
|
2252
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
2253
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
2254
|
+
edgesAdded++;
|
|
2255
|
+
}
|
|
2256
|
+
return { nodesAdded, edgesAdded };
|
|
2257
|
+
}
|
|
2194
2258
|
|
|
2195
2259
|
// src/extract/services.ts
|
|
2196
2260
|
init_cjs_shims();
|
|
@@ -2587,6 +2651,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2587
2651
|
}
|
|
2588
2652
|
return [...found];
|
|
2589
2653
|
}
|
|
2654
|
+
function detectJsFramework(pkg) {
|
|
2655
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2656
|
+
if (deps["next"] !== void 0) return "next";
|
|
2657
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2658
|
+
for (const k of Object.keys(deps)) {
|
|
2659
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2660
|
+
}
|
|
2661
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2662
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2663
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2664
|
+
return void 0;
|
|
2665
|
+
}
|
|
2590
2666
|
async function discoverNodeService(scanPath, dir) {
|
|
2591
2667
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2592
2668
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2598,6 +2674,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2598
2674
|
return null;
|
|
2599
2675
|
}
|
|
2600
2676
|
if (!pkg.name) return null;
|
|
2677
|
+
const framework = detectJsFramework(pkg);
|
|
2601
2678
|
const node = {
|
|
2602
2679
|
id: (0, import_types6.serviceId)(pkg.name),
|
|
2603
2680
|
type: import_types6.NodeType.ServiceNode,
|
|
@@ -2606,7 +2683,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2606
2683
|
version: pkg.version,
|
|
2607
2684
|
dependencies: pkg.dependencies ?? {},
|
|
2608
2685
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2609
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2686
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2687
|
+
...framework ? { framework } : {}
|
|
2610
2688
|
};
|
|
2611
2689
|
return { pkg, dir, node };
|
|
2612
2690
|
}
|
|
@@ -4461,25 +4539,138 @@ function canonicalJson(value) {
|
|
|
4461
4539
|
});
|
|
4462
4540
|
}
|
|
4463
4541
|
|
|
4464
|
-
// src/
|
|
4542
|
+
// src/persist.ts
|
|
4465
4543
|
init_cjs_shims();
|
|
4544
|
+
var import_node_fs19 = require("fs");
|
|
4466
4545
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4546
|
+
var import_types20 = require("@neat.is/types");
|
|
4547
|
+
var SCHEMA_VERSION = 4;
|
|
4548
|
+
function migrateV1ToV2(payload) {
|
|
4549
|
+
const nodes = payload.graph.nodes;
|
|
4550
|
+
if (Array.isArray(nodes)) {
|
|
4551
|
+
for (const node of nodes) {
|
|
4552
|
+
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4553
|
+
delete node.attributes.pgDriverVersion;
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
return { ...payload, schemaVersion: 2 };
|
|
4558
|
+
}
|
|
4559
|
+
function migrateV3ToV4(payload) {
|
|
4560
|
+
return { ...payload, schemaVersion: 4 };
|
|
4561
|
+
}
|
|
4562
|
+
function migrateV2ToV3(payload) {
|
|
4563
|
+
const edges = payload.graph.edges;
|
|
4564
|
+
if (Array.isArray(edges)) {
|
|
4565
|
+
for (const edge of edges) {
|
|
4566
|
+
const attrs = edge.attributes;
|
|
4567
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4568
|
+
attrs.provenance = import_types20.Provenance.OBSERVED;
|
|
4569
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4570
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4571
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4572
|
+
if (type && source && target) {
|
|
4573
|
+
const newId = (0, import_types20.observedEdgeId)(source, target, type);
|
|
4574
|
+
attrs.id = newId;
|
|
4575
|
+
if (edge.key) edge.key = newId;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
return { ...payload, schemaVersion: 3 };
|
|
4580
|
+
}
|
|
4581
|
+
async function ensureDir(filePath) {
|
|
4582
|
+
await import_node_fs19.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4583
|
+
}
|
|
4584
|
+
async function saveGraphToDisk(graph, outPath) {
|
|
4585
|
+
await ensureDir(outPath);
|
|
4586
|
+
const payload = {
|
|
4587
|
+
schemaVersion: SCHEMA_VERSION,
|
|
4588
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4589
|
+
graph: graph.export()
|
|
4590
|
+
};
|
|
4591
|
+
const tmp = `${outPath}.tmp`;
|
|
4592
|
+
await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4593
|
+
await import_node_fs19.promises.rename(tmp, outPath);
|
|
4594
|
+
}
|
|
4595
|
+
async function loadGraphFromDisk(graph, outPath) {
|
|
4596
|
+
let raw;
|
|
4597
|
+
try {
|
|
4598
|
+
raw = await import_node_fs19.promises.readFile(outPath, "utf8");
|
|
4599
|
+
} catch (err) {
|
|
4600
|
+
if (err.code === "ENOENT") return;
|
|
4601
|
+
throw err;
|
|
4602
|
+
}
|
|
4603
|
+
let payload = JSON.parse(raw);
|
|
4604
|
+
if (payload.schemaVersion === 1) {
|
|
4605
|
+
payload = migrateV1ToV2(payload);
|
|
4606
|
+
}
|
|
4607
|
+
if (payload.schemaVersion === 2) {
|
|
4608
|
+
payload = migrateV2ToV3(payload);
|
|
4609
|
+
}
|
|
4610
|
+
if (payload.schemaVersion === 3) {
|
|
4611
|
+
payload = migrateV3ToV4(payload);
|
|
4612
|
+
}
|
|
4613
|
+
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4614
|
+
throw new Error(
|
|
4615
|
+
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
4618
|
+
graph.clear();
|
|
4619
|
+
graph.import(payload.graph);
|
|
4620
|
+
}
|
|
4621
|
+
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4622
|
+
let stopped = false;
|
|
4623
|
+
const tick = async () => {
|
|
4624
|
+
if (stopped) return;
|
|
4625
|
+
try {
|
|
4626
|
+
await saveGraphToDisk(graph, outPath);
|
|
4627
|
+
} catch (err) {
|
|
4628
|
+
console.error("persist: periodic save failed", err);
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
const interval = setInterval(() => {
|
|
4632
|
+
void tick();
|
|
4633
|
+
}, intervalMs);
|
|
4634
|
+
const onSignal = (signal) => {
|
|
4635
|
+
void (async () => {
|
|
4636
|
+
try {
|
|
4637
|
+
await saveGraphToDisk(graph, outPath);
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
console.error(`persist: ${signal} save failed`, err);
|
|
4640
|
+
} finally {
|
|
4641
|
+
process.exit(0);
|
|
4642
|
+
}
|
|
4643
|
+
})();
|
|
4644
|
+
};
|
|
4645
|
+
process.on("SIGTERM", onSignal);
|
|
4646
|
+
process.on("SIGINT", onSignal);
|
|
4647
|
+
return () => {
|
|
4648
|
+
stopped = true;
|
|
4649
|
+
clearInterval(interval);
|
|
4650
|
+
process.off("SIGTERM", onSignal);
|
|
4651
|
+
process.off("SIGINT", onSignal);
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
// src/projects.ts
|
|
4656
|
+
init_cjs_shims();
|
|
4657
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4467
4658
|
function pathsForProject(project, baseDir) {
|
|
4468
4659
|
if (project === DEFAULT_PROJECT) {
|
|
4469
4660
|
return {
|
|
4470
|
-
snapshotPath:
|
|
4471
|
-
errorsPath:
|
|
4472
|
-
staleEventsPath:
|
|
4473
|
-
embeddingsCachePath:
|
|
4474
|
-
policyViolationsPath:
|
|
4661
|
+
snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
|
|
4662
|
+
errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
|
|
4663
|
+
staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
|
|
4664
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
|
|
4665
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
|
|
4475
4666
|
};
|
|
4476
4667
|
}
|
|
4477
4668
|
return {
|
|
4478
|
-
snapshotPath:
|
|
4479
|
-
errorsPath:
|
|
4480
|
-
staleEventsPath:
|
|
4481
|
-
embeddingsCachePath:
|
|
4482
|
-
policyViolationsPath:
|
|
4669
|
+
snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
|
|
4670
|
+
errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4671
|
+
staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4672
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
|
|
4673
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4483
4674
|
};
|
|
4484
4675
|
}
|
|
4485
4676
|
var Projects = class {
|
|
@@ -4519,23 +4710,23 @@ function parseExtraProjects(raw) {
|
|
|
4519
4710
|
|
|
4520
4711
|
// src/registry.ts
|
|
4521
4712
|
init_cjs_shims();
|
|
4522
|
-
var
|
|
4713
|
+
var import_node_fs20 = require("fs");
|
|
4523
4714
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4524
|
-
var
|
|
4525
|
-
var
|
|
4715
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4716
|
+
var import_types21 = require("@neat.is/types");
|
|
4526
4717
|
function neatHome() {
|
|
4527
4718
|
const override = process.env.NEAT_HOME;
|
|
4528
|
-
if (override && override.length > 0) return
|
|
4529
|
-
return
|
|
4719
|
+
if (override && override.length > 0) return import_node_path33.default.resolve(override);
|
|
4720
|
+
return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4530
4721
|
}
|
|
4531
4722
|
function registryPath() {
|
|
4532
|
-
return
|
|
4723
|
+
return import_node_path33.default.join(neatHome(), "projects.json");
|
|
4533
4724
|
}
|
|
4534
4725
|
async function readRegistry() {
|
|
4535
4726
|
const file = registryPath();
|
|
4536
4727
|
let raw;
|
|
4537
4728
|
try {
|
|
4538
|
-
raw = await
|
|
4729
|
+
raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
4539
4730
|
} catch (err) {
|
|
4540
4731
|
if (err.code === "ENOENT") {
|
|
4541
4732
|
return { version: 1, projects: [] };
|
|
@@ -4543,7 +4734,7 @@ async function readRegistry() {
|
|
|
4543
4734
|
throw err;
|
|
4544
4735
|
}
|
|
4545
4736
|
const parsed = JSON.parse(raw);
|
|
4546
|
-
return
|
|
4737
|
+
return import_types21.RegistryFileSchema.parse(parsed);
|
|
4547
4738
|
}
|
|
4548
4739
|
async function getProject(name) {
|
|
4549
4740
|
const reg = await readRegistry();
|
|
@@ -4734,11 +4925,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4734
4925
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4735
4926
|
const parsed = [];
|
|
4736
4927
|
for (const c of candidates) {
|
|
4737
|
-
const r =
|
|
4928
|
+
const r = import_types22.DivergenceTypeSchema.safeParse(c);
|
|
4738
4929
|
if (!r.success) {
|
|
4739
4930
|
return reply.code(400).send({
|
|
4740
4931
|
error: `unknown divergence type "${c}"`,
|
|
4741
|
-
allowed:
|
|
4932
|
+
allowed: import_types22.DivergenceTypeSchema.options
|
|
4742
4933
|
});
|
|
4743
4934
|
}
|
|
4744
4935
|
parsed.push(r.data);
|
|
@@ -4883,6 +5074,35 @@ function registerRoutes(scope, ctx) {
|
|
|
4883
5074
|
}
|
|
4884
5075
|
}
|
|
4885
5076
|
);
|
|
5077
|
+
scope.post("/snapshot", async (req, reply) => {
|
|
5078
|
+
const proj = resolveProject(registry, req, reply);
|
|
5079
|
+
if (!proj) return;
|
|
5080
|
+
const body = req.body;
|
|
5081
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5082
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5083
|
+
}
|
|
5084
|
+
const snap = body.snapshot;
|
|
5085
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5086
|
+
return reply.code(400).send({
|
|
5087
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
try {
|
|
5091
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5092
|
+
return {
|
|
5093
|
+
project: proj.name,
|
|
5094
|
+
nodesAdded: result.nodesAdded,
|
|
5095
|
+
edgesAdded: result.edgesAdded,
|
|
5096
|
+
nodeCount: proj.graph.order,
|
|
5097
|
+
edgeCount: proj.graph.size
|
|
5098
|
+
};
|
|
5099
|
+
} catch (err) {
|
|
5100
|
+
return reply.code(400).send({
|
|
5101
|
+
error: "snapshot merge failed",
|
|
5102
|
+
details: err.message
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
});
|
|
4886
5106
|
scope.post("/graph/scan", async (req, reply) => {
|
|
4887
5107
|
const proj = resolveProject(registry, req, reply);
|
|
4888
5108
|
if (!proj) return;
|
|
@@ -4922,7 +5142,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4922
5142
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4923
5143
|
let violations = await log.readAll();
|
|
4924
5144
|
if (req.query.severity) {
|
|
4925
|
-
const sev =
|
|
5145
|
+
const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4926
5146
|
if (!sev.success) {
|
|
4927
5147
|
return reply.code(400).send({
|
|
4928
5148
|
error: "invalid severity",
|
|
@@ -4939,7 +5159,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4939
5159
|
scope.post("/policies/check", async (req, reply) => {
|
|
4940
5160
|
const proj = resolveProject(registry, req, reply);
|
|
4941
5161
|
if (!proj) return;
|
|
4942
|
-
const parsed =
|
|
5162
|
+
const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4943
5163
|
if (!parsed.success) {
|
|
4944
5164
|
return reply.code(400).send({
|
|
4945
5165
|
error: "invalid /policies/check body",
|
|
@@ -4976,7 +5196,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4976
5196
|
async function buildApi(opts) {
|
|
4977
5197
|
const app = (0, import_fastify.default)({ logger: false });
|
|
4978
5198
|
await app.register(import_cors.default, { origin: true });
|
|
4979
|
-
mountBearerAuth(app, {
|
|
5199
|
+
mountBearerAuth(app, {
|
|
5200
|
+
token: opts.authToken,
|
|
5201
|
+
trustProxy: opts.trustProxy,
|
|
5202
|
+
publicRead: opts.publicRead
|
|
5203
|
+
});
|
|
5204
|
+
app.get("/api/config", async () => ({
|
|
5205
|
+
publicRead: opts.publicRead === true,
|
|
5206
|
+
authProxy: opts.trustProxy === true
|
|
5207
|
+
}));
|
|
4980
5208
|
const startedAt = opts.startedAt ?? Date.now();
|
|
4981
5209
|
const registry = buildLegacyRegistry(opts);
|
|
4982
5210
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5038,113 +5266,6 @@ async function buildApi(opts) {
|
|
|
5038
5266
|
return app;
|
|
5039
5267
|
}
|
|
5040
5268
|
|
|
5041
|
-
// src/persist.ts
|
|
5042
|
-
init_cjs_shims();
|
|
5043
|
-
var import_node_fs20 = require("fs");
|
|
5044
|
-
var import_node_path33 = __toESM(require("path"), 1);
|
|
5045
|
-
var import_types22 = require("@neat.is/types");
|
|
5046
|
-
var SCHEMA_VERSION = 3;
|
|
5047
|
-
function migrateV1ToV2(payload) {
|
|
5048
|
-
const nodes = payload.graph.nodes;
|
|
5049
|
-
if (Array.isArray(nodes)) {
|
|
5050
|
-
for (const node of nodes) {
|
|
5051
|
-
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
5052
|
-
delete node.attributes.pgDriverVersion;
|
|
5053
|
-
}
|
|
5054
|
-
}
|
|
5055
|
-
}
|
|
5056
|
-
return { ...payload, schemaVersion: 2 };
|
|
5057
|
-
}
|
|
5058
|
-
function migrateV2ToV3(payload) {
|
|
5059
|
-
const edges = payload.graph.edges;
|
|
5060
|
-
if (Array.isArray(edges)) {
|
|
5061
|
-
for (const edge of edges) {
|
|
5062
|
-
const attrs = edge.attributes;
|
|
5063
|
-
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5064
|
-
attrs.provenance = import_types22.Provenance.OBSERVED;
|
|
5065
|
-
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5066
|
-
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5067
|
-
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5068
|
-
if (type && source && target) {
|
|
5069
|
-
const newId = (0, import_types22.observedEdgeId)(source, target, type);
|
|
5070
|
-
attrs.id = newId;
|
|
5071
|
-
if (edge.key) edge.key = newId;
|
|
5072
|
-
}
|
|
5073
|
-
}
|
|
5074
|
-
}
|
|
5075
|
-
return { ...payload, schemaVersion: 3 };
|
|
5076
|
-
}
|
|
5077
|
-
async function ensureDir(filePath) {
|
|
5078
|
-
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
|
|
5079
|
-
}
|
|
5080
|
-
async function saveGraphToDisk(graph, outPath) {
|
|
5081
|
-
await ensureDir(outPath);
|
|
5082
|
-
const payload = {
|
|
5083
|
-
schemaVersion: SCHEMA_VERSION,
|
|
5084
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5085
|
-
graph: graph.export()
|
|
5086
|
-
};
|
|
5087
|
-
const tmp = `${outPath}.tmp`;
|
|
5088
|
-
await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
5089
|
-
await import_node_fs20.promises.rename(tmp, outPath);
|
|
5090
|
-
}
|
|
5091
|
-
async function loadGraphFromDisk(graph, outPath) {
|
|
5092
|
-
let raw;
|
|
5093
|
-
try {
|
|
5094
|
-
raw = await import_node_fs20.promises.readFile(outPath, "utf8");
|
|
5095
|
-
} catch (err) {
|
|
5096
|
-
if (err.code === "ENOENT") return;
|
|
5097
|
-
throw err;
|
|
5098
|
-
}
|
|
5099
|
-
let payload = JSON.parse(raw);
|
|
5100
|
-
if (payload.schemaVersion === 1) {
|
|
5101
|
-
payload = migrateV1ToV2(payload);
|
|
5102
|
-
}
|
|
5103
|
-
if (payload.schemaVersion === 2) {
|
|
5104
|
-
payload = migrateV2ToV3(payload);
|
|
5105
|
-
}
|
|
5106
|
-
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
5107
|
-
throw new Error(
|
|
5108
|
-
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5109
|
-
);
|
|
5110
|
-
}
|
|
5111
|
-
graph.clear();
|
|
5112
|
-
graph.import(payload.graph);
|
|
5113
|
-
}
|
|
5114
|
-
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
5115
|
-
let stopped = false;
|
|
5116
|
-
const tick = async () => {
|
|
5117
|
-
if (stopped) return;
|
|
5118
|
-
try {
|
|
5119
|
-
await saveGraphToDisk(graph, outPath);
|
|
5120
|
-
} catch (err) {
|
|
5121
|
-
console.error("persist: periodic save failed", err);
|
|
5122
|
-
}
|
|
5123
|
-
};
|
|
5124
|
-
const interval = setInterval(() => {
|
|
5125
|
-
void tick();
|
|
5126
|
-
}, intervalMs);
|
|
5127
|
-
const onSignal = (signal) => {
|
|
5128
|
-
void (async () => {
|
|
5129
|
-
try {
|
|
5130
|
-
await saveGraphToDisk(graph, outPath);
|
|
5131
|
-
} catch (err) {
|
|
5132
|
-
console.error(`persist: ${signal} save failed`, err);
|
|
5133
|
-
} finally {
|
|
5134
|
-
process.exit(0);
|
|
5135
|
-
}
|
|
5136
|
-
})();
|
|
5137
|
-
};
|
|
5138
|
-
process.on("SIGTERM", onSignal);
|
|
5139
|
-
process.on("SIGINT", onSignal);
|
|
5140
|
-
return () => {
|
|
5141
|
-
stopped = true;
|
|
5142
|
-
clearInterval(interval);
|
|
5143
|
-
process.off("SIGTERM", onSignal);
|
|
5144
|
-
process.off("SIGINT", onSignal);
|
|
5145
|
-
};
|
|
5146
|
-
}
|
|
5147
|
-
|
|
5148
5269
|
// src/server.ts
|
|
5149
5270
|
init_otel();
|
|
5150
5271
|
init_otel_grpc();
|
|
@@ -5489,7 +5610,8 @@ async function main() {
|
|
|
5489
5610
|
const app = await buildApi({
|
|
5490
5611
|
projects: registry,
|
|
5491
5612
|
authToken: auth.authToken,
|
|
5492
|
-
trustProxy: auth.trustProxy
|
|
5613
|
+
trustProxy: auth.trustProxy,
|
|
5614
|
+
publicRead: auth.publicRead
|
|
5493
5615
|
});
|
|
5494
5616
|
await app.listen({ port, host });
|
|
5495
5617
|
console.log(`neat-core listening on http://${host}:${port}`);
|