@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.d.cts
CHANGED
|
@@ -114,6 +114,9 @@ interface BuildApiOptions {
|
|
|
114
114
|
errorsPath?: string;
|
|
115
115
|
staleEventsPath?: string;
|
|
116
116
|
searchIndex?: SearchIndex;
|
|
117
|
+
authToken?: string;
|
|
118
|
+
trustProxy?: boolean;
|
|
119
|
+
publicRead?: boolean;
|
|
117
120
|
}
|
|
118
121
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
119
122
|
|
|
@@ -128,6 +131,7 @@ interface ParsedSpan {
|
|
|
128
131
|
endTimeUnixNano: string;
|
|
129
132
|
startTimeIso?: string;
|
|
130
133
|
durationNanos: bigint;
|
|
134
|
+
env: string;
|
|
131
135
|
attributes: Record<string, AttributeValue>;
|
|
132
136
|
dbSystem?: string;
|
|
133
137
|
dbName?: string;
|
|
@@ -145,6 +149,8 @@ interface BuildOtelReceiverOptions {
|
|
|
145
149
|
onSpan: SpanHandler;
|
|
146
150
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
147
151
|
bodyLimit?: number;
|
|
152
|
+
authToken?: string;
|
|
153
|
+
trustProxy?: boolean;
|
|
148
154
|
}
|
|
149
155
|
interface OtlpKeyValue {
|
|
150
156
|
key: string;
|
|
@@ -200,6 +206,8 @@ declare function logSpanHandler(span: ParsedSpan): void;
|
|
|
200
206
|
|
|
201
207
|
interface BuildOtelGrpcReceiverOptions {
|
|
202
208
|
onSpan: SpanHandler;
|
|
209
|
+
authToken?: string;
|
|
210
|
+
trustProxy?: boolean;
|
|
203
211
|
}
|
|
204
212
|
interface OtelGrpcReceiver {
|
|
205
213
|
address: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,9 @@ interface BuildApiOptions {
|
|
|
114
114
|
errorsPath?: string;
|
|
115
115
|
staleEventsPath?: string;
|
|
116
116
|
searchIndex?: SearchIndex;
|
|
117
|
+
authToken?: string;
|
|
118
|
+
trustProxy?: boolean;
|
|
119
|
+
publicRead?: boolean;
|
|
117
120
|
}
|
|
118
121
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
119
122
|
|
|
@@ -128,6 +131,7 @@ interface ParsedSpan {
|
|
|
128
131
|
endTimeUnixNano: string;
|
|
129
132
|
startTimeIso?: string;
|
|
130
133
|
durationNanos: bigint;
|
|
134
|
+
env: string;
|
|
131
135
|
attributes: Record<string, AttributeValue>;
|
|
132
136
|
dbSystem?: string;
|
|
133
137
|
dbName?: string;
|
|
@@ -145,6 +149,8 @@ interface BuildOtelReceiverOptions {
|
|
|
145
149
|
onSpan: SpanHandler;
|
|
146
150
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
147
151
|
bodyLimit?: number;
|
|
152
|
+
authToken?: string;
|
|
153
|
+
trustProxy?: boolean;
|
|
148
154
|
}
|
|
149
155
|
interface OtlpKeyValue {
|
|
150
156
|
key: string;
|
|
@@ -200,6 +206,8 @@ declare function logSpanHandler(span: ParsedSpan): void;
|
|
|
200
206
|
|
|
201
207
|
interface BuildOtelGrpcReceiverOptions {
|
|
202
208
|
onSpan: SpanHandler;
|
|
209
|
+
authToken?: string;
|
|
210
|
+
trustProxy?: boolean;
|
|
203
211
|
}
|
|
204
212
|
interface OtelGrpcReceiver {
|
|
205
213
|
address: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-N6RPINEJ.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,15 +37,15 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-UPW4CMOH.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-IXIFJKMM.js";
|
|
44
44
|
import {
|
|
45
45
|
buildOtelReceiver,
|
|
46
46
|
logSpanHandler,
|
|
47
47
|
parseOtlpRequest
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-4V23KYOP.js";
|
|
49
49
|
export {
|
|
50
50
|
ProjectNameCollisionError,
|
|
51
51
|
addProject,
|
package/dist/neatd.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", (req2, reply, done) => {
|
|
60
|
+
const path39 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
61
|
+
for (const suffix of suffixes) {
|
|
62
|
+
if (path39 === suffix || path39.endsWith(suffix)) {
|
|
63
|
+
done();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req2.method)) {
|
|
68
|
+
done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const header = req2.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();
|
|
@@ -381,7 +491,7 @@ async function buildOtelReceiver(opts) {
|
|
|
381
491
|
};
|
|
382
492
|
return decorated;
|
|
383
493
|
}
|
|
384
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
494
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
385
495
|
var init_otel = __esm({
|
|
386
496
|
"src/otel.ts"() {
|
|
387
497
|
"use strict";
|
|
@@ -390,6 +500,10 @@ var init_otel = __esm({
|
|
|
390
500
|
import_node_url2 = require("url");
|
|
391
501
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
392
502
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
503
|
+
init_auth();
|
|
504
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
505
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
506
|
+
ENV_FALLBACK = "unknown";
|
|
393
507
|
exportTraceServiceRequestType = null;
|
|
394
508
|
exportTraceServiceResponseType = null;
|
|
395
509
|
cachedProtobufResponseBody = null;
|
|
@@ -1428,52 +1542,64 @@ function cacheSpanService(span, now) {
|
|
|
1428
1542
|
if (!span.traceId || !span.spanId) return;
|
|
1429
1543
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1430
1544
|
parentSpanCache.delete(key);
|
|
1431
|
-
parentSpanCache.set(key, {
|
|
1545
|
+
parentSpanCache.set(key, {
|
|
1546
|
+
service: span.service,
|
|
1547
|
+
env: span.env ?? "unknown",
|
|
1548
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1549
|
+
});
|
|
1432
1550
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1433
1551
|
const oldest = parentSpanCache.keys().next().value;
|
|
1434
1552
|
if (!oldest) break;
|
|
1435
1553
|
parentSpanCache.delete(oldest);
|
|
1436
1554
|
}
|
|
1437
1555
|
}
|
|
1438
|
-
function
|
|
1556
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1439
1557
|
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1440
1558
|
if (!entry2) return null;
|
|
1441
1559
|
if (entry2.expiresAt <= now) {
|
|
1442
1560
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1443
1561
|
return null;
|
|
1444
1562
|
}
|
|
1445
|
-
return entry2.service;
|
|
1563
|
+
return { service: entry2.service, env: entry2.env };
|
|
1446
1564
|
}
|
|
1447
|
-
function resolveServiceId(graph, host) {
|
|
1448
|
-
const
|
|
1449
|
-
if (graph.hasNode(
|
|
1450
|
-
|
|
1565
|
+
function resolveServiceId(graph, host, env) {
|
|
1566
|
+
const envTagged = (0, import_types3.serviceId)(host, env);
|
|
1567
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1568
|
+
const envLess = (0, import_types3.serviceId)(host);
|
|
1569
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1570
|
+
let sameEnv = null;
|
|
1571
|
+
let envLessMatch = null;
|
|
1572
|
+
let anyMatch = null;
|
|
1451
1573
|
graph.forEachNode((id, attrs) => {
|
|
1452
|
-
if (
|
|
1574
|
+
if (sameEnv) return;
|
|
1453
1575
|
const a = attrs;
|
|
1454
1576
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1455
|
-
|
|
1456
|
-
|
|
1577
|
+
const matchesByName = a.name === host;
|
|
1578
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1579
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1580
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1581
|
+
if (nodeEnv === env) {
|
|
1582
|
+
sameEnv = id;
|
|
1457
1583
|
return;
|
|
1458
1584
|
}
|
|
1459
|
-
if (
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1585
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1586
|
+
else if (!anyMatch) anyMatch = id;
|
|
1462
1587
|
});
|
|
1463
|
-
return
|
|
1588
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1464
1589
|
}
|
|
1465
1590
|
function frontierIdFor(host) {
|
|
1466
1591
|
return (0, import_types3.frontierId)(host);
|
|
1467
1592
|
}
|
|
1468
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1469
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1593
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1594
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1470
1595
|
if (graph.hasNode(id)) return id;
|
|
1471
1596
|
const node = {
|
|
1472
1597
|
id,
|
|
1473
1598
|
type: import_types3.NodeType.ServiceNode,
|
|
1474
1599
|
name: serviceName,
|
|
1475
1600
|
language: "unknown",
|
|
1476
|
-
discoveredVia: "otel"
|
|
1601
|
+
discoveredVia: "otel",
|
|
1602
|
+
...env !== "unknown" ? { env } : {}
|
|
1477
1603
|
};
|
|
1478
1604
|
graph.addNode(id, node);
|
|
1479
1605
|
return id;
|
|
@@ -1619,7 +1745,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1619
1745
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1620
1746
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1621
1747
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1622
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1748
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1623
1749
|
};
|
|
1624
1750
|
}
|
|
1625
1751
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1633,7 +1759,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1633
1759
|
async function handleSpan(ctx, span) {
|
|
1634
1760
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1635
1761
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1636
|
-
const
|
|
1762
|
+
const env = span.env ?? "unknown";
|
|
1763
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1637
1764
|
const isError = span.statusCode === 2;
|
|
1638
1765
|
cacheSpanService(span, nowMs);
|
|
1639
1766
|
let affectedNode = sourceId;
|
|
@@ -1656,7 +1783,7 @@ async function handleSpan(ctx, span) {
|
|
|
1656
1783
|
const host = pickAddress(span);
|
|
1657
1784
|
let resolvedViaAddress = false;
|
|
1658
1785
|
if (host && host !== span.service) {
|
|
1659
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1786
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1660
1787
|
if (targetId && targetId !== sourceId) {
|
|
1661
1788
|
upsertObservedEdge(
|
|
1662
1789
|
ctx.graph,
|
|
@@ -1683,9 +1810,9 @@ async function handleSpan(ctx, span) {
|
|
|
1683
1810
|
}
|
|
1684
1811
|
}
|
|
1685
1812
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1686
|
-
const
|
|
1687
|
-
if (
|
|
1688
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
1813
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1814
|
+
if (parent && parent.service !== span.service) {
|
|
1815
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1689
1816
|
upsertObservedEdge(
|
|
1690
1817
|
ctx.graph,
|
|
1691
1818
|
import_types3.EdgeType.CALLS,
|
|
@@ -1808,6 +1935,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
1808
1935
|
throw err;
|
|
1809
1936
|
}
|
|
1810
1937
|
}
|
|
1938
|
+
function mergeSnapshot(graph, snapshot) {
|
|
1939
|
+
const exported = snapshot.graph;
|
|
1940
|
+
let nodesAdded = 0;
|
|
1941
|
+
let edgesAdded = 0;
|
|
1942
|
+
for (const node of exported.nodes ?? []) {
|
|
1943
|
+
if (graph.hasNode(node.key)) continue;
|
|
1944
|
+
if (!node.attributes) continue;
|
|
1945
|
+
graph.addNode(node.key, node.attributes);
|
|
1946
|
+
nodesAdded++;
|
|
1947
|
+
}
|
|
1948
|
+
for (const edge of exported.edges ?? []) {
|
|
1949
|
+
const attrs = edge.attributes;
|
|
1950
|
+
if (!attrs) continue;
|
|
1951
|
+
const id = edge.key ?? attrs.id;
|
|
1952
|
+
if (!id) continue;
|
|
1953
|
+
if (graph.hasEdge(id)) continue;
|
|
1954
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
1955
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
1956
|
+
edgesAdded++;
|
|
1957
|
+
}
|
|
1958
|
+
return { nodesAdded, edgesAdded };
|
|
1959
|
+
}
|
|
1811
1960
|
|
|
1812
1961
|
// src/extract/services.ts
|
|
1813
1962
|
init_cjs_shims();
|
|
@@ -2204,6 +2353,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2204
2353
|
}
|
|
2205
2354
|
return [...found];
|
|
2206
2355
|
}
|
|
2356
|
+
function detectJsFramework(pkg) {
|
|
2357
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2358
|
+
if (deps["next"] !== void 0) return "next";
|
|
2359
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2360
|
+
for (const k of Object.keys(deps)) {
|
|
2361
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2362
|
+
}
|
|
2363
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2364
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2365
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2366
|
+
return void 0;
|
|
2367
|
+
}
|
|
2207
2368
|
async function discoverNodeService(scanPath, dir) {
|
|
2208
2369
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2209
2370
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2215,6 +2376,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2215
2376
|
return null;
|
|
2216
2377
|
}
|
|
2217
2378
|
if (!pkg.name) return null;
|
|
2379
|
+
const framework = detectJsFramework(pkg);
|
|
2218
2380
|
const node = {
|
|
2219
2381
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2220
2382
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2223,7 +2385,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2223
2385
|
version: pkg.version,
|
|
2224
2386
|
dependencies: pkg.dependencies ?? {},
|
|
2225
2387
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2226
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2388
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2389
|
+
...framework ? { framework } : {}
|
|
2227
2390
|
};
|
|
2228
2391
|
return { pkg, dir, node };
|
|
2229
2392
|
}
|
|
@@ -4006,7 +4169,7 @@ init_cjs_shims();
|
|
|
4006
4169
|
var import_node_fs18 = require("fs");
|
|
4007
4170
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4008
4171
|
var import_types19 = require("@neat.is/types");
|
|
4009
|
-
var SCHEMA_VERSION =
|
|
4172
|
+
var SCHEMA_VERSION = 4;
|
|
4010
4173
|
function migrateV1ToV2(payload) {
|
|
4011
4174
|
const nodes = payload.graph.nodes;
|
|
4012
4175
|
if (Array.isArray(nodes)) {
|
|
@@ -4018,6 +4181,9 @@ function migrateV1ToV2(payload) {
|
|
|
4018
4181
|
}
|
|
4019
4182
|
return { ...payload, schemaVersion: 2 };
|
|
4020
4183
|
}
|
|
4184
|
+
function migrateV3ToV4(payload) {
|
|
4185
|
+
return { ...payload, schemaVersion: 4 };
|
|
4186
|
+
}
|
|
4021
4187
|
function migrateV2ToV3(payload) {
|
|
4022
4188
|
const edges = payload.graph.edges;
|
|
4023
4189
|
if (Array.isArray(edges)) {
|
|
@@ -4066,6 +4232,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4066
4232
|
if (payload.schemaVersion === 2) {
|
|
4067
4233
|
payload = migrateV2ToV3(payload);
|
|
4068
4234
|
}
|
|
4235
|
+
if (payload.schemaVersion === 3) {
|
|
4236
|
+
payload = migrateV3ToV4(payload);
|
|
4237
|
+
}
|
|
4069
4238
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4070
4239
|
throw new Error(
|
|
4071
4240
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -4646,6 +4815,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4646
4815
|
}
|
|
4647
4816
|
|
|
4648
4817
|
// src/api.ts
|
|
4818
|
+
init_auth();
|
|
4649
4819
|
function serializeGraph(graph) {
|
|
4650
4820
|
const nodes = [];
|
|
4651
4821
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4918,6 +5088,35 @@ function registerRoutes(scope, ctx) {
|
|
|
4918
5088
|
}
|
|
4919
5089
|
}
|
|
4920
5090
|
);
|
|
5091
|
+
scope.post("/snapshot", async (req2, reply) => {
|
|
5092
|
+
const proj = resolveProject(registry, req2, reply);
|
|
5093
|
+
if (!proj) return;
|
|
5094
|
+
const body = req2.body;
|
|
5095
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5096
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5097
|
+
}
|
|
5098
|
+
const snap = body.snapshot;
|
|
5099
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5100
|
+
return reply.code(400).send({
|
|
5101
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
try {
|
|
5105
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5106
|
+
return {
|
|
5107
|
+
project: proj.name,
|
|
5108
|
+
nodesAdded: result.nodesAdded,
|
|
5109
|
+
edgesAdded: result.edgesAdded,
|
|
5110
|
+
nodeCount: proj.graph.order,
|
|
5111
|
+
edgeCount: proj.graph.size
|
|
5112
|
+
};
|
|
5113
|
+
} catch (err) {
|
|
5114
|
+
return reply.code(400).send({
|
|
5115
|
+
error: "snapshot merge failed",
|
|
5116
|
+
details: err.message
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
});
|
|
4921
5120
|
scope.post("/graph/scan", async (req2, reply) => {
|
|
4922
5121
|
const proj = resolveProject(registry, req2, reply);
|
|
4923
5122
|
if (!proj) return;
|
|
@@ -5011,6 +5210,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5011
5210
|
async function buildApi(opts) {
|
|
5012
5211
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5013
5212
|
await app.register(import_cors.default, { origin: true });
|
|
5213
|
+
mountBearerAuth(app, {
|
|
5214
|
+
token: opts.authToken,
|
|
5215
|
+
trustProxy: opts.trustProxy,
|
|
5216
|
+
publicRead: opts.publicRead
|
|
5217
|
+
});
|
|
5218
|
+
app.get("/api/config", async () => ({
|
|
5219
|
+
publicRead: opts.publicRead === true,
|
|
5220
|
+
authProxy: opts.trustProxy === true
|
|
5221
|
+
}));
|
|
5014
5222
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5015
5223
|
const registry = buildLegacyRegistry(opts);
|
|
5016
5224
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5074,6 +5282,7 @@ async function buildApi(opts) {
|
|
|
5074
5282
|
|
|
5075
5283
|
// src/daemon.ts
|
|
5076
5284
|
init_otel();
|
|
5285
|
+
init_auth();
|
|
5077
5286
|
function neatHomeFor(opts) {
|
|
5078
5287
|
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
5079
5288
|
const env = process.env.NEAT_HOME;
|
|
@@ -5280,8 +5489,15 @@ async function startDaemon(opts = {}) {
|
|
|
5280
5489
|
const host = resolveHost(opts);
|
|
5281
5490
|
const restPort = resolveRestPort(opts);
|
|
5282
5491
|
const otlpPort = resolveOtlpPort(opts);
|
|
5492
|
+
const auth = readAuthEnv();
|
|
5493
|
+
assertBindAuthority(host, auth.authToken);
|
|
5283
5494
|
try {
|
|
5284
|
-
restApp = await buildApi({
|
|
5495
|
+
restApp = await buildApi({
|
|
5496
|
+
projects: registry,
|
|
5497
|
+
authToken: auth.authToken,
|
|
5498
|
+
trustProxy: auth.trustProxy,
|
|
5499
|
+
publicRead: auth.publicRead
|
|
5500
|
+
});
|
|
5285
5501
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
5286
5502
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
5287
5503
|
} catch (err) {
|
|
@@ -5318,6 +5534,8 @@ async function startDaemon(opts = {}) {
|
|
|
5318
5534
|
}
|
|
5319
5535
|
try {
|
|
5320
5536
|
otlpApp = await buildOtelReceiver({
|
|
5537
|
+
authToken: auth.otelToken,
|
|
5538
|
+
trustProxy: auth.trustProxy,
|
|
5321
5539
|
onSpan: async (span) => {
|
|
5322
5540
|
const slot = await resolveTargetSlot(span.service);
|
|
5323
5541
|
if (!slot) return;
|
|
@@ -5397,6 +5615,9 @@ async function startDaemon(opts = {}) {
|
|
|
5397
5615
|
return { slots, reload, stop, pidPath, restAddress, otlpAddress };
|
|
5398
5616
|
}
|
|
5399
5617
|
|
|
5618
|
+
// src/neatd.ts
|
|
5619
|
+
init_auth();
|
|
5620
|
+
|
|
5400
5621
|
// src/web-spawn.ts
|
|
5401
5622
|
init_cjs_shims();
|
|
5402
5623
|
var import_node_child_process = require("child_process");
|
|
@@ -5592,7 +5813,16 @@ function restPortFromEnv() {
|
|
|
5592
5813
|
return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
|
|
5593
5814
|
}
|
|
5594
5815
|
async function cmdStart() {
|
|
5595
|
-
|
|
5816
|
+
let handle;
|
|
5817
|
+
try {
|
|
5818
|
+
handle = await startDaemon();
|
|
5819
|
+
} catch (err) {
|
|
5820
|
+
if (err instanceof BindAuthorityError) {
|
|
5821
|
+
console.error(`neatd: ${err.message}`);
|
|
5822
|
+
process.exit(1);
|
|
5823
|
+
}
|
|
5824
|
+
throw err;
|
|
5825
|
+
}
|
|
5596
5826
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
5597
5827
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
5598
5828
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|