@neat.is/core 0.3.7 → 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-75IBA4UL.js → chunk-4V23KYOP.js} +98 -2
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-IXIFJKMM.js} +16 -2
- package/dist/chunk-IXIFJKMM.js.map +1 -0
- package/dist/{chunk-EDHOCFOG.js → chunk-N6RPINEJ.js} +16 -5
- package/dist/chunk-N6RPINEJ.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-UPW4CMOH.js} +368 -270
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +2009 -421
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +1607 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +259 -29
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +387 -161
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +26 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- package/dist/chunk-ZU2RQRCN.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,91 @@ var init_cjs_shims = __esm({
|
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// src/auth.ts
|
|
44
|
+
function isLoopbackHost(host) {
|
|
45
|
+
if (!host) return false;
|
|
46
|
+
return LOOPBACK_HOSTS.has(host);
|
|
47
|
+
}
|
|
48
|
+
function assertBindAuthority(host, token) {
|
|
49
|
+
if (token && token.length > 0) return;
|
|
50
|
+
if (isLoopbackHost(host)) return;
|
|
51
|
+
throw new BindAuthorityError(host);
|
|
52
|
+
}
|
|
53
|
+
function mountBearerAuth(app, opts) {
|
|
54
|
+
if (!opts.token || opts.token.length === 0) return;
|
|
55
|
+
if (opts.trustProxy) return;
|
|
56
|
+
const expected = Buffer.from(opts.token, "utf8");
|
|
57
|
+
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
|
+
const publicRead = opts.publicRead === true;
|
|
59
|
+
app.addHook("preHandler", (req, reply, done) => {
|
|
60
|
+
const path37 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
61
|
+
for (const suffix of suffixes) {
|
|
62
|
+
if (path37 === suffix || path37.endsWith(suffix)) {
|
|
63
|
+
done();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
68
|
+
done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const header = req.headers.authorization;
|
|
72
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
73
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
77
|
+
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
78
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
done();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function parseBoolEnv(v) {
|
|
85
|
+
if (!v) return false;
|
|
86
|
+
return v === "true" || v === "1";
|
|
87
|
+
}
|
|
88
|
+
function readAuthEnv(env = process.env) {
|
|
89
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
90
|
+
const ot = env.NEAT_OTEL_TOKEN;
|
|
91
|
+
return {
|
|
92
|
+
authToken: t && t.length > 0 ? t : void 0,
|
|
93
|
+
otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
|
|
94
|
+
trustProxy: env.NEAT_AUTH_PROXY === "true",
|
|
95
|
+
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
|
|
99
|
+
var init_auth = __esm({
|
|
100
|
+
"src/auth.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
init_cjs_shims();
|
|
103
|
+
import_node_crypto = require("crypto");
|
|
104
|
+
LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
105
|
+
"127.0.0.1",
|
|
106
|
+
"localhost",
|
|
107
|
+
"::1",
|
|
108
|
+
"::ffff:127.0.0.1"
|
|
109
|
+
]);
|
|
110
|
+
BindAuthorityError = class extends Error {
|
|
111
|
+
constructor(host) {
|
|
112
|
+
super(
|
|
113
|
+
`NEAT refuses to bind on a public interface without \`NEAT_AUTH_TOKEN\` set (host="${host}"). Set the token or bind to loopback only.`
|
|
114
|
+
);
|
|
115
|
+
this.name = "BindAuthorityError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
119
|
+
DEFAULT_UNAUTH_SUFFIXES = [
|
|
120
|
+
"/health",
|
|
121
|
+
"/healthz",
|
|
122
|
+
"/readyz",
|
|
123
|
+
"/api/config"
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
43
128
|
// src/otel-grpc.ts
|
|
44
129
|
var otel_grpc_exports = {};
|
|
45
130
|
__export(otel_grpc_exports, {
|
|
@@ -122,8 +207,21 @@ function loadTraceService() {
|
|
|
122
207
|
async function startOtelGrpcReceiver(opts) {
|
|
123
208
|
const server = new grpc.Server();
|
|
124
209
|
const service = loadTraceService();
|
|
210
|
+
const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
|
|
211
|
+
const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
|
|
125
212
|
server.addService(service, {
|
|
126
213
|
Export: (call, callback) => {
|
|
214
|
+
if (requiresAuth) {
|
|
215
|
+
const meta = call.metadata.get("authorization");
|
|
216
|
+
const got = meta.length > 0 ? String(meta[0]) : "";
|
|
217
|
+
const a = Buffer.from(got, "utf8");
|
|
218
|
+
const b = Buffer.from(expectedHeader, "utf8");
|
|
219
|
+
const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
220
|
+
if (!ok) {
|
|
221
|
+
callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
127
225
|
void (async () => {
|
|
128
226
|
try {
|
|
129
227
|
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
@@ -156,13 +254,14 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
254
|
})
|
|
157
255
|
};
|
|
158
256
|
}
|
|
159
|
-
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
257
|
+
var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
|
|
160
258
|
var init_otel_grpc = __esm({
|
|
161
259
|
"src/otel-grpc.ts"() {
|
|
162
260
|
"use strict";
|
|
163
261
|
init_cjs_shims();
|
|
164
262
|
import_node_url = require("url");
|
|
165
263
|
import_node_path34 = __toESM(require("path"), 1);
|
|
264
|
+
import_node_crypto2 = require("crypto");
|
|
166
265
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
266
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
267
|
init_otel();
|
|
@@ -225,6 +324,15 @@ function isoFromUnixNano(nanos) {
|
|
|
225
324
|
return void 0;
|
|
226
325
|
}
|
|
227
326
|
}
|
|
327
|
+
function pickEnv(spanAttrs, resourceAttrs) {
|
|
328
|
+
for (const attrs of [spanAttrs, resourceAttrs]) {
|
|
329
|
+
for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
|
|
330
|
+
const v = attrs[key];
|
|
331
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return ENV_FALLBACK;
|
|
335
|
+
}
|
|
228
336
|
function parseOtlpRequest(body) {
|
|
229
337
|
const out = [];
|
|
230
338
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -244,6 +352,7 @@ function parseOtlpRequest(body) {
|
|
|
244
352
|
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
245
353
|
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
246
354
|
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
355
|
+
env: pickEnv(attrs, resourceAttrs),
|
|
247
356
|
attributes: attrs,
|
|
248
357
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
249
358
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
@@ -303,6 +412,7 @@ async function buildOtelReceiver(opts) {
|
|
|
303
412
|
logger: false,
|
|
304
413
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
305
414
|
});
|
|
415
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
306
416
|
const queue = [];
|
|
307
417
|
let draining = false;
|
|
308
418
|
let drainPromise = Promise.resolve();
|
|
@@ -389,7 +499,7 @@ function logSpanHandler(span) {
|
|
|
389
499
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
390
500
|
);
|
|
391
501
|
}
|
|
392
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
502
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
393
503
|
var init_otel = __esm({
|
|
394
504
|
"src/otel.ts"() {
|
|
395
505
|
"use strict";
|
|
@@ -398,6 +508,10 @@ var init_otel = __esm({
|
|
|
398
508
|
import_node_url2 = require("url");
|
|
399
509
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
400
510
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
511
|
+
init_auth();
|
|
512
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
513
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
514
|
+
ENV_FALLBACK = "unknown";
|
|
401
515
|
exportTraceServiceRequestType = null;
|
|
402
516
|
exportTraceServiceResponseType = null;
|
|
403
517
|
cachedProtobufResponseBody = null;
|
|
@@ -1494,52 +1608,64 @@ function cacheSpanService(span, now) {
|
|
|
1494
1608
|
if (!span.traceId || !span.spanId) return;
|
|
1495
1609
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1496
1610
|
parentSpanCache.delete(key);
|
|
1497
|
-
parentSpanCache.set(key, {
|
|
1611
|
+
parentSpanCache.set(key, {
|
|
1612
|
+
service: span.service,
|
|
1613
|
+
env: span.env ?? "unknown",
|
|
1614
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1615
|
+
});
|
|
1498
1616
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1499
1617
|
const oldest = parentSpanCache.keys().next().value;
|
|
1500
1618
|
if (!oldest) break;
|
|
1501
1619
|
parentSpanCache.delete(oldest);
|
|
1502
1620
|
}
|
|
1503
1621
|
}
|
|
1504
|
-
function
|
|
1622
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1505
1623
|
const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1506
1624
|
if (!entry) return null;
|
|
1507
1625
|
if (entry.expiresAt <= now) {
|
|
1508
1626
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1509
1627
|
return null;
|
|
1510
1628
|
}
|
|
1511
|
-
return entry.service;
|
|
1629
|
+
return { service: entry.service, env: entry.env };
|
|
1512
1630
|
}
|
|
1513
|
-
function resolveServiceId(graph, host) {
|
|
1514
|
-
const
|
|
1515
|
-
if (graph.hasNode(
|
|
1516
|
-
|
|
1631
|
+
function resolveServiceId(graph, host, env) {
|
|
1632
|
+
const envTagged = (0, import_types3.serviceId)(host, env);
|
|
1633
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1634
|
+
const envLess = (0, import_types3.serviceId)(host);
|
|
1635
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1636
|
+
let sameEnv = null;
|
|
1637
|
+
let envLessMatch = null;
|
|
1638
|
+
let anyMatch = null;
|
|
1517
1639
|
graph.forEachNode((id, attrs) => {
|
|
1518
|
-
if (
|
|
1640
|
+
if (sameEnv) return;
|
|
1519
1641
|
const a = attrs;
|
|
1520
1642
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1521
|
-
|
|
1522
|
-
|
|
1643
|
+
const matchesByName = a.name === host;
|
|
1644
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1645
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1646
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1647
|
+
if (nodeEnv === env) {
|
|
1648
|
+
sameEnv = id;
|
|
1523
1649
|
return;
|
|
1524
1650
|
}
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1651
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1652
|
+
else if (!anyMatch) anyMatch = id;
|
|
1528
1653
|
});
|
|
1529
|
-
return
|
|
1654
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1530
1655
|
}
|
|
1531
1656
|
function frontierIdFor(host) {
|
|
1532
1657
|
return (0, import_types3.frontierId)(host);
|
|
1533
1658
|
}
|
|
1534
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1535
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1659
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1660
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1536
1661
|
if (graph.hasNode(id)) return id;
|
|
1537
1662
|
const node = {
|
|
1538
1663
|
id,
|
|
1539
1664
|
type: import_types3.NodeType.ServiceNode,
|
|
1540
1665
|
name: serviceName,
|
|
1541
1666
|
language: "unknown",
|
|
1542
|
-
discoveredVia: "otel"
|
|
1667
|
+
discoveredVia: "otel",
|
|
1668
|
+
...env !== "unknown" ? { env } : {}
|
|
1543
1669
|
};
|
|
1544
1670
|
graph.addNode(id, node);
|
|
1545
1671
|
return id;
|
|
@@ -1685,7 +1811,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1685
1811
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1686
1812
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1687
1813
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1688
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1814
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1689
1815
|
};
|
|
1690
1816
|
}
|
|
1691
1817
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1699,7 +1825,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1699
1825
|
async function handleSpan(ctx, span) {
|
|
1700
1826
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1701
1827
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1702
|
-
const
|
|
1828
|
+
const env = span.env ?? "unknown";
|
|
1829
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1703
1830
|
const isError = span.statusCode === 2;
|
|
1704
1831
|
cacheSpanService(span, nowMs);
|
|
1705
1832
|
let affectedNode = sourceId;
|
|
@@ -1722,7 +1849,7 @@ async function handleSpan(ctx, span) {
|
|
|
1722
1849
|
const host = pickAddress(span);
|
|
1723
1850
|
let resolvedViaAddress = false;
|
|
1724
1851
|
if (host && host !== span.service) {
|
|
1725
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1852
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1726
1853
|
if (targetId && targetId !== sourceId) {
|
|
1727
1854
|
upsertObservedEdge(
|
|
1728
1855
|
ctx.graph,
|
|
@@ -1749,9 +1876,9 @@ async function handleSpan(ctx, span) {
|
|
|
1749
1876
|
}
|
|
1750
1877
|
}
|
|
1751
1878
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1754
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
1879
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1880
|
+
if (parent && parent.service !== span.service) {
|
|
1881
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1755
1882
|
upsertObservedEdge(
|
|
1756
1883
|
ctx.graph,
|
|
1757
1884
|
import_types3.EdgeType.CALLS,
|
|
@@ -1947,6 +2074,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
1947
2074
|
throw err;
|
|
1948
2075
|
}
|
|
1949
2076
|
}
|
|
2077
|
+
function mergeSnapshot(graph, snapshot) {
|
|
2078
|
+
const exported = snapshot.graph;
|
|
2079
|
+
let nodesAdded = 0;
|
|
2080
|
+
let edgesAdded = 0;
|
|
2081
|
+
for (const node of exported.nodes ?? []) {
|
|
2082
|
+
if (graph.hasNode(node.key)) continue;
|
|
2083
|
+
if (!node.attributes) continue;
|
|
2084
|
+
graph.addNode(node.key, node.attributes);
|
|
2085
|
+
nodesAdded++;
|
|
2086
|
+
}
|
|
2087
|
+
for (const edge of exported.edges ?? []) {
|
|
2088
|
+
const attrs = edge.attributes;
|
|
2089
|
+
if (!attrs) continue;
|
|
2090
|
+
const id = edge.key ?? attrs.id;
|
|
2091
|
+
if (!id) continue;
|
|
2092
|
+
if (graph.hasEdge(id)) continue;
|
|
2093
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
2094
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
2095
|
+
edgesAdded++;
|
|
2096
|
+
}
|
|
2097
|
+
return { nodesAdded, edgesAdded };
|
|
2098
|
+
}
|
|
1950
2099
|
|
|
1951
2100
|
// src/extract/services.ts
|
|
1952
2101
|
init_cjs_shims();
|
|
@@ -2343,6 +2492,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2343
2492
|
}
|
|
2344
2493
|
return [...found];
|
|
2345
2494
|
}
|
|
2495
|
+
function detectJsFramework(pkg) {
|
|
2496
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2497
|
+
if (deps["next"] !== void 0) return "next";
|
|
2498
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2499
|
+
for (const k of Object.keys(deps)) {
|
|
2500
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2501
|
+
}
|
|
2502
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2503
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2504
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2505
|
+
return void 0;
|
|
2506
|
+
}
|
|
2346
2507
|
async function discoverNodeService(scanPath, dir) {
|
|
2347
2508
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2348
2509
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2354,6 +2515,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2354
2515
|
return null;
|
|
2355
2516
|
}
|
|
2356
2517
|
if (!pkg.name) return null;
|
|
2518
|
+
const framework = detectJsFramework(pkg);
|
|
2357
2519
|
const node = {
|
|
2358
2520
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2359
2521
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2362,7 +2524,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2362
2524
|
version: pkg.version,
|
|
2363
2525
|
dependencies: pkg.dependencies ?? {},
|
|
2364
2526
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2365
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2527
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2528
|
+
...framework ? { framework } : {}
|
|
2366
2529
|
};
|
|
2367
2530
|
return { pkg, dir, node };
|
|
2368
2531
|
}
|
|
@@ -4145,7 +4308,7 @@ init_cjs_shims();
|
|
|
4145
4308
|
var import_node_fs18 = require("fs");
|
|
4146
4309
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4147
4310
|
var import_types19 = require("@neat.is/types");
|
|
4148
|
-
var SCHEMA_VERSION =
|
|
4311
|
+
var SCHEMA_VERSION = 4;
|
|
4149
4312
|
function migrateV1ToV2(payload) {
|
|
4150
4313
|
const nodes = payload.graph.nodes;
|
|
4151
4314
|
if (Array.isArray(nodes)) {
|
|
@@ -4157,6 +4320,9 @@ function migrateV1ToV2(payload) {
|
|
|
4157
4320
|
}
|
|
4158
4321
|
return { ...payload, schemaVersion: 2 };
|
|
4159
4322
|
}
|
|
4323
|
+
function migrateV3ToV4(payload) {
|
|
4324
|
+
return { ...payload, schemaVersion: 4 };
|
|
4325
|
+
}
|
|
4160
4326
|
function migrateV2ToV3(payload) {
|
|
4161
4327
|
const edges = payload.graph.edges;
|
|
4162
4328
|
if (Array.isArray(edges)) {
|
|
@@ -4205,6 +4371,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4205
4371
|
if (payload.schemaVersion === 2) {
|
|
4206
4372
|
payload = migrateV2ToV3(payload);
|
|
4207
4373
|
}
|
|
4374
|
+
if (payload.schemaVersion === 3) {
|
|
4375
|
+
payload = migrateV3ToV4(payload);
|
|
4376
|
+
}
|
|
4208
4377
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4209
4378
|
throw new Error(
|
|
4210
4379
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -4843,6 +5012,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4843
5012
|
}
|
|
4844
5013
|
|
|
4845
5014
|
// src/api.ts
|
|
5015
|
+
init_auth();
|
|
4846
5016
|
function serializeGraph(graph) {
|
|
4847
5017
|
const nodes = [];
|
|
4848
5018
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -5115,6 +5285,35 @@ function registerRoutes(scope, ctx) {
|
|
|
5115
5285
|
}
|
|
5116
5286
|
}
|
|
5117
5287
|
);
|
|
5288
|
+
scope.post("/snapshot", async (req, reply) => {
|
|
5289
|
+
const proj = resolveProject(registry, req, reply);
|
|
5290
|
+
if (!proj) return;
|
|
5291
|
+
const body = req.body;
|
|
5292
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5293
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5294
|
+
}
|
|
5295
|
+
const snap = body.snapshot;
|
|
5296
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5297
|
+
return reply.code(400).send({
|
|
5298
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
try {
|
|
5302
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5303
|
+
return {
|
|
5304
|
+
project: proj.name,
|
|
5305
|
+
nodesAdded: result.nodesAdded,
|
|
5306
|
+
edgesAdded: result.edgesAdded,
|
|
5307
|
+
nodeCount: proj.graph.order,
|
|
5308
|
+
edgeCount: proj.graph.size
|
|
5309
|
+
};
|
|
5310
|
+
} catch (err) {
|
|
5311
|
+
return reply.code(400).send({
|
|
5312
|
+
error: "snapshot merge failed",
|
|
5313
|
+
details: err.message
|
|
5314
|
+
});
|
|
5315
|
+
}
|
|
5316
|
+
});
|
|
5118
5317
|
scope.post("/graph/scan", async (req, reply) => {
|
|
5119
5318
|
const proj = resolveProject(registry, req, reply);
|
|
5120
5319
|
if (!proj) return;
|
|
@@ -5208,6 +5407,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5208
5407
|
async function buildApi(opts) {
|
|
5209
5408
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5210
5409
|
await app.register(import_cors.default, { origin: true });
|
|
5410
|
+
mountBearerAuth(app, {
|
|
5411
|
+
token: opts.authToken,
|
|
5412
|
+
trustProxy: opts.trustProxy,
|
|
5413
|
+
publicRead: opts.publicRead
|
|
5414
|
+
});
|
|
5415
|
+
app.get("/api/config", async () => ({
|
|
5416
|
+
publicRead: opts.publicRead === true,
|
|
5417
|
+
authProxy: opts.trustProxy === true
|
|
5418
|
+
}));
|
|
5211
5419
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5212
5420
|
const registry = buildLegacyRegistry(opts);
|
|
5213
5421
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5278,6 +5486,7 @@ init_cjs_shims();
|
|
|
5278
5486
|
var import_node_fs21 = require("fs");
|
|
5279
5487
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
5280
5488
|
init_otel();
|
|
5489
|
+
init_auth();
|
|
5281
5490
|
function neatHomeFor(opts) {
|
|
5282
5491
|
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
5283
5492
|
const env = process.env.NEAT_HOME;
|
|
@@ -5484,8 +5693,15 @@ async function startDaemon(opts = {}) {
|
|
|
5484
5693
|
const host = resolveHost(opts);
|
|
5485
5694
|
const restPort = resolveRestPort(opts);
|
|
5486
5695
|
const otlpPort = resolveOtlpPort(opts);
|
|
5696
|
+
const auth = readAuthEnv();
|
|
5697
|
+
assertBindAuthority(host, auth.authToken);
|
|
5487
5698
|
try {
|
|
5488
|
-
restApp = await buildApi({
|
|
5699
|
+
restApp = await buildApi({
|
|
5700
|
+
projects: registry,
|
|
5701
|
+
authToken: auth.authToken,
|
|
5702
|
+
trustProxy: auth.trustProxy,
|
|
5703
|
+
publicRead: auth.publicRead
|
|
5704
|
+
});
|
|
5489
5705
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
5490
5706
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
5491
5707
|
} catch (err) {
|
|
@@ -5522,6 +5738,8 @@ async function startDaemon(opts = {}) {
|
|
|
5522
5738
|
}
|
|
5523
5739
|
try {
|
|
5524
5740
|
otlpApp = await buildOtelReceiver({
|
|
5741
|
+
authToken: auth.otelToken,
|
|
5742
|
+
trustProxy: auth.trustProxy,
|
|
5525
5743
|
onSpan: async (span) => {
|
|
5526
5744
|
const slot = await resolveTargetSlot(span.service);
|
|
5527
5745
|
if (!slot) return;
|