@neat.is/core 0.3.0 → 0.3.2
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-QYUB3FQL.js → chunk-4ASCXBZF.js} +12 -124
- package/dist/chunk-4ASCXBZF.js.map +1 -0
- package/dist/{chunk-I5KODOJV.js → chunk-66Z5IEFY.js} +126 -7
- package/dist/chunk-66Z5IEFY.js.map +1 -0
- package/dist/{chunk-B7UUGIXB.js → chunk-FIXKIYNF.js} +818 -18
- package/dist/chunk-FIXKIYNF.js.map +1 -0
- package/dist/chunk-G3PDTGOW.js +125 -0
- package/dist/chunk-G3PDTGOW.js.map +1 -0
- package/dist/cli.cjs +76 -27
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +82 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +141 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -11
- package/dist/neatd.cjs +1900 -36
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +19 -5
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-USFL3OI3.js → otel-grpc-J4O2SIBZ.js} +3 -3
- package/dist/server.js +6 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-B7UUGIXB.js.map +0 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-I5KODOJV.js.map +0 -1
- package/dist/chunk-NVCEZXL7.js +0 -836
- package/dist/chunk-NVCEZXL7.js.map +0 -1
- package/dist/chunk-QYUB3FQL.js.map +0 -1
- package/dist/otel-grpc-USFL3OI3.js.map +0 -1
- /package/dist/{chunk-DGUM43GV.js.map → otel-grpc-J4O2SIBZ.js.map} +0 -0
package/dist/neatd.cjs
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,15 +30,355 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
33
|
+
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var getImportMetaUrl, importMetaUrl;
|
|
35
|
+
var init_cjs_shims = __esm({
|
|
36
|
+
"../../node_modules/tsup/assets/cjs_shims.js"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/otel-grpc.ts
|
|
44
|
+
var otel_grpc_exports = {};
|
|
45
|
+
__export(otel_grpc_exports, {
|
|
46
|
+
reshapeGrpcRequest: () => reshapeGrpcRequest,
|
|
47
|
+
startOtelGrpcReceiver: () => startOtelGrpcReceiver
|
|
48
|
+
});
|
|
49
|
+
function bytesToHex(buf) {
|
|
50
|
+
if (!buf) return "";
|
|
51
|
+
return Buffer.isBuffer(buf) ? buf.toString("hex") : "";
|
|
52
|
+
}
|
|
53
|
+
function nanosToString(n) {
|
|
54
|
+
if (n === void 0 || n === null) return "0";
|
|
55
|
+
return typeof n === "string" ? n : String(n);
|
|
56
|
+
}
|
|
57
|
+
function reshapeAttributes(attrs) {
|
|
58
|
+
const out = (attrs ?? []).map((kv) => ({
|
|
59
|
+
key: kv.key ?? "",
|
|
60
|
+
value: kv.value ? {
|
|
61
|
+
stringValue: kv.value.string_value,
|
|
62
|
+
boolValue: kv.value.bool_value,
|
|
63
|
+
intValue: kv.value.int_value,
|
|
64
|
+
doubleValue: kv.value.double_value,
|
|
65
|
+
arrayValue: kv.value.array_value ? {
|
|
66
|
+
values: (kv.value.array_value.values ?? []).map((v) => ({
|
|
67
|
+
stringValue: v.string_value,
|
|
68
|
+
boolValue: v.bool_value,
|
|
69
|
+
intValue: v.int_value,
|
|
70
|
+
doubleValue: v.double_value
|
|
71
|
+
}))
|
|
72
|
+
} : void 0
|
|
73
|
+
} : void 0
|
|
74
|
+
}));
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function reshapeGrpcRequest(req2) {
|
|
78
|
+
return {
|
|
79
|
+
resourceSpans: (req2.resource_spans ?? []).map((rs) => ({
|
|
80
|
+
resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : void 0,
|
|
81
|
+
scopeSpans: (rs.scope_spans ?? []).map((ss) => ({
|
|
82
|
+
spans: (ss.spans ?? []).map((s) => ({
|
|
83
|
+
traceId: bytesToHex(s.trace_id),
|
|
84
|
+
spanId: bytesToHex(s.span_id),
|
|
85
|
+
parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : void 0,
|
|
86
|
+
name: s.name,
|
|
87
|
+
kind: s.kind,
|
|
88
|
+
startTimeUnixNano: nanosToString(s.start_time_unix_nano),
|
|
89
|
+
endTimeUnixNano: nanosToString(s.end_time_unix_nano),
|
|
90
|
+
attributes: reshapeAttributes(s.attributes),
|
|
91
|
+
events: (s.events ?? []).map((e) => ({
|
|
92
|
+
name: e.name,
|
|
93
|
+
timeUnixNano: nanosToString(e.time_unix_nano),
|
|
94
|
+
attributes: reshapeAttributes(e.attributes)
|
|
95
|
+
})),
|
|
96
|
+
status: s.status ? { code: s.status.code, message: s.status.message } : void 0
|
|
97
|
+
}))
|
|
98
|
+
}))
|
|
99
|
+
}))
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function resolveProtoRoot() {
|
|
103
|
+
const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path31.default.resolve(here, "..", "proto");
|
|
105
|
+
}
|
|
106
|
+
function loadTraceService() {
|
|
107
|
+
const protoRoot = resolveProtoRoot();
|
|
108
|
+
const def = protoLoader.loadSync(
|
|
109
|
+
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
110
|
+
{
|
|
111
|
+
keepCase: true,
|
|
112
|
+
longs: String,
|
|
113
|
+
enums: Number,
|
|
114
|
+
defaults: true,
|
|
115
|
+
oneofs: true,
|
|
116
|
+
includeDirs: [protoRoot]
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
const pkg = grpc.loadPackageDefinition(def);
|
|
120
|
+
return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service;
|
|
121
|
+
}
|
|
122
|
+
async function startOtelGrpcReceiver(opts) {
|
|
123
|
+
const server = new grpc.Server();
|
|
124
|
+
const service = loadTraceService();
|
|
125
|
+
server.addService(service, {
|
|
126
|
+
Export: (call, callback) => {
|
|
127
|
+
void (async () => {
|
|
128
|
+
try {
|
|
129
|
+
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
130
|
+
const spans = parseOtlpRequest(reshaped);
|
|
131
|
+
for (const span of spans) {
|
|
132
|
+
await opts.onSpan(span);
|
|
133
|
+
}
|
|
134
|
+
callback(null, { partial_success: {} });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
callback({
|
|
137
|
+
code: grpc.status.INTERNAL,
|
|
138
|
+
message: err instanceof Error ? err.message : String(err)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
const host = opts.host ?? "0.0.0.0";
|
|
145
|
+
const port = opts.port ?? 4317;
|
|
146
|
+
const boundPort = await new Promise((resolve, reject) => {
|
|
147
|
+
server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {
|
|
148
|
+
if (err) return reject(err);
|
|
149
|
+
resolve(p);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
address: `${host}:${boundPort}`,
|
|
154
|
+
stop: () => new Promise((resolve) => {
|
|
155
|
+
server.tryShutdown(() => resolve());
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var import_node_url, import_node_path31, grpc, protoLoader;
|
|
160
|
+
var init_otel_grpc = __esm({
|
|
161
|
+
"src/otel-grpc.ts"() {
|
|
162
|
+
"use strict";
|
|
163
|
+
init_cjs_shims();
|
|
164
|
+
import_node_url = require("url");
|
|
165
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
166
|
+
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
|
+
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
|
+
init_otel();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// src/otel.ts
|
|
173
|
+
function extractExceptionFromEvents(events) {
|
|
174
|
+
if (!events) return void 0;
|
|
175
|
+
for (const ev of events) {
|
|
176
|
+
if (ev.name !== "exception") continue;
|
|
177
|
+
const attrs = attrsToRecord(ev.attributes);
|
|
178
|
+
const out = {};
|
|
179
|
+
const t = attrs["exception.type"];
|
|
180
|
+
const m = attrs["exception.message"];
|
|
181
|
+
const s = attrs["exception.stacktrace"];
|
|
182
|
+
if (typeof t === "string") out.type = t;
|
|
183
|
+
if (typeof m === "string") out.message = m;
|
|
184
|
+
if (typeof s === "string") out.stacktrace = s;
|
|
185
|
+
if (out.type || out.message || out.stacktrace) return out;
|
|
186
|
+
}
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
function flattenAttribute(v) {
|
|
190
|
+
if (!v) return null;
|
|
191
|
+
if (v.stringValue !== void 0) return v.stringValue;
|
|
192
|
+
if (v.boolValue !== void 0) return v.boolValue;
|
|
193
|
+
if (v.intValue !== void 0) {
|
|
194
|
+
return typeof v.intValue === "string" ? Number(v.intValue) : v.intValue;
|
|
195
|
+
}
|
|
196
|
+
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
197
|
+
if (v.arrayValue?.values) {
|
|
198
|
+
return v.arrayValue.values.map((x) => flattenAttribute(x));
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
function attrsToRecord(attrs) {
|
|
203
|
+
const out = {};
|
|
204
|
+
if (!attrs) return out;
|
|
205
|
+
for (const kv of attrs) {
|
|
206
|
+
if (kv.key) out[kv.key] = flattenAttribute(kv.value);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function durationNanos(start, end) {
|
|
211
|
+
if (!start || !end) return 0n;
|
|
212
|
+
try {
|
|
213
|
+
return BigInt(end) - BigInt(start);
|
|
214
|
+
} catch {
|
|
215
|
+
return 0n;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function isoFromUnixNano(nanos) {
|
|
219
|
+
if (!nanos || nanos === "0") return void 0;
|
|
220
|
+
try {
|
|
221
|
+
const ms = Number(BigInt(nanos) / 1000000n);
|
|
222
|
+
if (!Number.isFinite(ms)) return void 0;
|
|
223
|
+
return new Date(ms).toISOString();
|
|
224
|
+
} catch {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function parseOtlpRequest(body) {
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const rs of body.resourceSpans ?? []) {
|
|
231
|
+
const resourceAttrs = attrsToRecord(rs.resource?.attributes);
|
|
232
|
+
const service = typeof resourceAttrs["service.name"] === "string" ? resourceAttrs["service.name"] : "unknown";
|
|
233
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
234
|
+
for (const span of ss.spans ?? []) {
|
|
235
|
+
const attrs = attrsToRecord(span.attributes);
|
|
236
|
+
const parsed = {
|
|
237
|
+
service,
|
|
238
|
+
traceId: span.traceId ?? "",
|
|
239
|
+
spanId: span.spanId ?? "",
|
|
240
|
+
parentSpanId: span.parentSpanId || void 0,
|
|
241
|
+
name: span.name ?? "",
|
|
242
|
+
kind: span.kind,
|
|
243
|
+
startTimeUnixNano: span.startTimeUnixNano ?? "0",
|
|
244
|
+
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
245
|
+
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
246
|
+
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
247
|
+
attributes: attrs,
|
|
248
|
+
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
249
|
+
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
250
|
+
statusCode: span.status?.code,
|
|
251
|
+
errorMessage: span.status?.message,
|
|
252
|
+
exception: extractExceptionFromEvents(span.events)
|
|
253
|
+
};
|
|
254
|
+
out.push(parsed);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function loadProtobufDecoder() {
|
|
261
|
+
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
262
|
+
const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
|
|
264
|
+
const root = new import_protobufjs.default.Root();
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
|
|
266
|
+
root.loadSync(
|
|
267
|
+
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
|
+
{ keepCase: true }
|
|
269
|
+
);
|
|
270
|
+
exportTraceServiceRequestType = root.lookupType(
|
|
271
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
272
|
+
);
|
|
273
|
+
return exportTraceServiceRequestType;
|
|
274
|
+
}
|
|
275
|
+
async function decodeProtobufBody(buf) {
|
|
276
|
+
const Type = loadProtobufDecoder();
|
|
277
|
+
const decoded = Type.decode(buf).toJSON();
|
|
278
|
+
const { reshapeGrpcRequest: reshapeGrpcRequest2 } = await Promise.resolve().then(() => (init_otel_grpc(), otel_grpc_exports));
|
|
279
|
+
return reshapeGrpcRequest2(decoded);
|
|
280
|
+
}
|
|
281
|
+
async function buildOtelReceiver(opts) {
|
|
282
|
+
const app = (0, import_fastify2.default)({
|
|
283
|
+
logger: false,
|
|
284
|
+
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
285
|
+
});
|
|
286
|
+
const queue = [];
|
|
287
|
+
let draining = false;
|
|
288
|
+
let drainPromise = Promise.resolve();
|
|
289
|
+
const drain = async () => {
|
|
290
|
+
if (draining) return;
|
|
291
|
+
draining = true;
|
|
292
|
+
try {
|
|
293
|
+
while (queue.length > 0) {
|
|
294
|
+
const span = queue.shift();
|
|
295
|
+
try {
|
|
296
|
+
await opts.onSpan(span);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.warn(`[neat] otel handler error: ${err.message}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
draining = false;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const enqueue = (spans) => {
|
|
306
|
+
if (spans.length === 0) return;
|
|
307
|
+
for (const s of spans) queue.push(s);
|
|
308
|
+
drainPromise = drainPromise.then(() => drain());
|
|
309
|
+
};
|
|
310
|
+
app.addContentTypeParser(
|
|
311
|
+
"application/x-protobuf",
|
|
312
|
+
{ parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
|
|
313
|
+
(_req, body, done) => {
|
|
314
|
+
done(null, body);
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
app.get("/health", async () => ({ ok: true }));
|
|
318
|
+
app.post("/v1/traces", async (req2, reply) => {
|
|
319
|
+
const ct = (req2.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
320
|
+
let body;
|
|
321
|
+
if (ct === "application/x-protobuf") {
|
|
322
|
+
try {
|
|
323
|
+
body = await decodeProtobufBody(req2.body);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
return reply.code(400).send({
|
|
326
|
+
error: `protobuf decode failed: ${err.message}`
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} else if (!ct || ct === "application/json") {
|
|
330
|
+
body = req2.body ?? {};
|
|
331
|
+
} else {
|
|
332
|
+
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
333
|
+
}
|
|
334
|
+
const spans = parseOtlpRequest(body);
|
|
335
|
+
if (opts.onErrorSpanSync) {
|
|
336
|
+
try {
|
|
337
|
+
for (const span of spans) {
|
|
338
|
+
if (span.statusCode === 2) await opts.onErrorSpanSync(span);
|
|
339
|
+
}
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return reply.code(500).send({
|
|
342
|
+
error: `error-event write failed: ${err.message}`
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
enqueue(spans);
|
|
347
|
+
return reply.code(200).send({ partialSuccess: {} });
|
|
348
|
+
});
|
|
349
|
+
const decorated = app;
|
|
350
|
+
decorated.flushPending = async () => {
|
|
351
|
+
while (queue.length > 0 || draining) {
|
|
352
|
+
await drainPromise;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return decorated;
|
|
356
|
+
}
|
|
357
|
+
var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
358
|
+
var init_otel = __esm({
|
|
359
|
+
"src/otel.ts"() {
|
|
360
|
+
"use strict";
|
|
361
|
+
init_cjs_shims();
|
|
362
|
+
import_node_path32 = __toESM(require("path"), 1);
|
|
363
|
+
import_node_url2 = require("url");
|
|
364
|
+
import_fastify2 = __toESM(require("fastify"), 1);
|
|
365
|
+
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
366
|
+
exportTraceServiceRequestType = null;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
26
370
|
// src/neatd.ts
|
|
27
|
-
|
|
28
|
-
var
|
|
371
|
+
init_cjs_shims();
|
|
372
|
+
var import_node_fs20 = require("fs");
|
|
373
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
29
374
|
|
|
30
375
|
// src/daemon.ts
|
|
31
|
-
|
|
32
|
-
var
|
|
376
|
+
init_cjs_shims();
|
|
377
|
+
var import_node_fs19 = require("fs");
|
|
378
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
33
379
|
|
|
34
380
|
// src/graph.ts
|
|
381
|
+
init_cjs_shims();
|
|
35
382
|
var import_graphology = __toESM(require("graphology"), 1);
|
|
36
383
|
var MultiDirectedGraph = import_graphology.default.MultiDirectedGraph;
|
|
37
384
|
var DEFAULT_PROJECT = "default";
|
|
@@ -55,16 +402,25 @@ function resetGraph(project) {
|
|
|
55
402
|
graphs.delete(project);
|
|
56
403
|
}
|
|
57
404
|
|
|
405
|
+
// src/extract.ts
|
|
406
|
+
init_cjs_shims();
|
|
407
|
+
|
|
408
|
+
// src/extract/index.ts
|
|
409
|
+
init_cjs_shims();
|
|
410
|
+
|
|
58
411
|
// src/ingest.ts
|
|
412
|
+
init_cjs_shims();
|
|
59
413
|
var import_node_fs3 = require("fs");
|
|
60
414
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
61
415
|
|
|
62
416
|
// src/policy.ts
|
|
417
|
+
init_cjs_shims();
|
|
63
418
|
var import_node_fs2 = require("fs");
|
|
64
419
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
65
420
|
var import_types2 = require("@neat.is/types");
|
|
66
421
|
|
|
67
422
|
// src/compat.ts
|
|
423
|
+
init_cjs_shims();
|
|
68
424
|
var import_node_fs = require("fs");
|
|
69
425
|
var import_node_os = __toESM(require("os"), 1);
|
|
70
426
|
var import_node_path = __toESM(require("path"), 1);
|
|
@@ -377,6 +733,7 @@ function deprecatedApis() {
|
|
|
377
733
|
}
|
|
378
734
|
|
|
379
735
|
// src/events.ts
|
|
736
|
+
init_cjs_shims();
|
|
380
737
|
var import_node_events = require("events");
|
|
381
738
|
var EVENT_BUS_CHANNEL = "event";
|
|
382
739
|
var NeatEventBus = class extends import_node_events.EventEmitter {
|
|
@@ -388,8 +745,22 @@ function emitNeatEvent(envelope) {
|
|
|
388
745
|
}
|
|
389
746
|
|
|
390
747
|
// src/traverse.ts
|
|
748
|
+
init_cjs_shims();
|
|
391
749
|
var import_types = require("@neat.is/types");
|
|
750
|
+
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
392
751
|
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
752
|
+
function bestEdgeBySource(graph, edgeIds) {
|
|
753
|
+
const best = /* @__PURE__ */ new Map();
|
|
754
|
+
for (const id of edgeIds) {
|
|
755
|
+
const e = graph.getEdgeAttributes(id);
|
|
756
|
+
if (e.provenance === import_types.Provenance.FRONTIER) continue;
|
|
757
|
+
const cur = best.get(e.source);
|
|
758
|
+
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
759
|
+
best.set(e.source, e);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return best;
|
|
763
|
+
}
|
|
393
764
|
function bestEdgeByTarget(graph, edgeIds) {
|
|
394
765
|
const best = /* @__PURE__ */ new Map();
|
|
395
766
|
for (const id of edgeIds) {
|
|
@@ -457,6 +828,29 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
457
828
|
}
|
|
458
829
|
return Math.max(0, Math.min(1, product));
|
|
459
830
|
}
|
|
831
|
+
function longestIncomingWalk(graph, start, maxDepth) {
|
|
832
|
+
let best = { path: [start], edges: [] };
|
|
833
|
+
const visited = /* @__PURE__ */ new Set([start]);
|
|
834
|
+
function step(node, path36, edges) {
|
|
835
|
+
if (path36.length > best.path.length) {
|
|
836
|
+
best = { path: [...path36], edges: [...edges] };
|
|
837
|
+
}
|
|
838
|
+
if (path36.length - 1 >= maxDepth) return;
|
|
839
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
840
|
+
for (const [srcId, edge] of incoming) {
|
|
841
|
+
if (visited.has(srcId)) continue;
|
|
842
|
+
visited.add(srcId);
|
|
843
|
+
path36.push(srcId);
|
|
844
|
+
edges.push(edge);
|
|
845
|
+
step(srcId, path36, edges);
|
|
846
|
+
path36.pop();
|
|
847
|
+
edges.pop();
|
|
848
|
+
visited.delete(srcId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
step(start, [start], []);
|
|
852
|
+
return best;
|
|
853
|
+
}
|
|
460
854
|
function databaseRootCauseShape(graph, origin, walk) {
|
|
461
855
|
const targetDb = origin;
|
|
462
856
|
const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
|
|
@@ -529,6 +923,24 @@ var rootCauseShapes = {
|
|
|
529
923
|
[import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
|
|
530
924
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape
|
|
531
925
|
};
|
|
926
|
+
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
927
|
+
if (!graph.hasNode(errorNodeId)) return null;
|
|
928
|
+
const origin = graph.getNodeAttributes(errorNodeId);
|
|
929
|
+
const shape = rootCauseShapes[origin.type];
|
|
930
|
+
if (!shape) return null;
|
|
931
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
932
|
+
const match = shape(graph, origin, walk);
|
|
933
|
+
if (!match) return null;
|
|
934
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
935
|
+
return import_types.RootCauseResultSchema.parse({
|
|
936
|
+
rootCauseNode: match.rootCauseNode,
|
|
937
|
+
rootCauseReason: reason,
|
|
938
|
+
traversalPath: walk.path,
|
|
939
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
940
|
+
confidence: confidenceFromMix(walk.edges),
|
|
941
|
+
fixRecommendation: match.fixRecommendation
|
|
942
|
+
});
|
|
943
|
+
}
|
|
532
944
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
533
945
|
if (!graph.hasNode(nodeId)) {
|
|
534
946
|
return import_types.BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
|
|
@@ -570,6 +982,48 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
570
982
|
totalAffected: affectedNodes.length
|
|
571
983
|
});
|
|
572
984
|
}
|
|
985
|
+
var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
|
|
986
|
+
var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
|
|
987
|
+
function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
|
|
988
|
+
if (!graph.hasNode(nodeId)) {
|
|
989
|
+
return import_types.TransitiveDependenciesResultSchema.parse({
|
|
990
|
+
origin: nodeId,
|
|
991
|
+
depth,
|
|
992
|
+
dependencies: [],
|
|
993
|
+
total: 0
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
const seen = /* @__PURE__ */ new Map();
|
|
997
|
+
const queue = [{ nodeId, distance: 0, edge: null }];
|
|
998
|
+
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
999
|
+
while (queue.length > 0) {
|
|
1000
|
+
const frame = queue.shift();
|
|
1001
|
+
if (frame.distance > 0 && frame.edge) {
|
|
1002
|
+
seen.set(frame.nodeId, {
|
|
1003
|
+
nodeId: frame.nodeId,
|
|
1004
|
+
distance: frame.distance,
|
|
1005
|
+
edgeType: frame.edge.type,
|
|
1006
|
+
provenance: frame.edge.provenance
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (frame.distance >= depth) continue;
|
|
1010
|
+
const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1011
|
+
for (const [tgtId, edge] of outgoing) {
|
|
1012
|
+
if (enqueued.has(tgtId)) continue;
|
|
1013
|
+
enqueued.add(tgtId);
|
|
1014
|
+
queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const dependencies = [...seen.values()].sort(
|
|
1018
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
1019
|
+
);
|
|
1020
|
+
return import_types.TransitiveDependenciesResultSchema.parse({
|
|
1021
|
+
origin: nodeId,
|
|
1022
|
+
depth,
|
|
1023
|
+
dependencies,
|
|
1024
|
+
total: dependencies.length
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
573
1027
|
|
|
574
1028
|
// src/policy.ts
|
|
575
1029
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -839,6 +1293,54 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
839
1293
|
}
|
|
840
1294
|
return out;
|
|
841
1295
|
}
|
|
1296
|
+
async function loadPolicyFile(policyPath) {
|
|
1297
|
+
let raw;
|
|
1298
|
+
try {
|
|
1299
|
+
raw = await import_node_fs2.promises.readFile(policyPath, "utf8");
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
if (err.code === "ENOENT") return [];
|
|
1302
|
+
throw err;
|
|
1303
|
+
}
|
|
1304
|
+
const json = JSON.parse(raw);
|
|
1305
|
+
const file = import_types2.PolicyFileSchema.parse(json);
|
|
1306
|
+
return file.policies;
|
|
1307
|
+
}
|
|
1308
|
+
var PolicyViolationsLog = class {
|
|
1309
|
+
path;
|
|
1310
|
+
project;
|
|
1311
|
+
seen = null;
|
|
1312
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
1313
|
+
this.path = logPath;
|
|
1314
|
+
this.project = project;
|
|
1315
|
+
}
|
|
1316
|
+
async append(v) {
|
|
1317
|
+
if (!this.seen) await this.hydrate();
|
|
1318
|
+
if (this.seen.has(v.id)) return false;
|
|
1319
|
+
this.seen.add(v.id);
|
|
1320
|
+
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1321
|
+
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1322
|
+
emitNeatEvent({
|
|
1323
|
+
type: "policy-violation",
|
|
1324
|
+
project: this.project,
|
|
1325
|
+
payload: { violation: v }
|
|
1326
|
+
});
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
async readAll() {
|
|
1330
|
+
try {
|
|
1331
|
+
const raw = await import_node_fs2.promises.readFile(this.path, "utf8");
|
|
1332
|
+
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
if (err.code === "ENOENT") return [];
|
|
1335
|
+
throw err;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async hydrate() {
|
|
1339
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
1340
|
+
const existing = await this.readAll();
|
|
1341
|
+
for (const v of existing) this.seen.add(v.id);
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
842
1344
|
|
|
843
1345
|
// src/ingest.ts
|
|
844
1346
|
var import_types3 = require("@neat.is/types");
|
|
@@ -853,7 +1355,342 @@ var DEFAULT_STALE_THRESHOLDS = {
|
|
|
853
1355
|
CONFIGURED_BY: DAY_MS,
|
|
854
1356
|
RUNS_ON: DAY_MS
|
|
855
1357
|
};
|
|
1358
|
+
function nowIso(ctx) {
|
|
1359
|
+
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1360
|
+
}
|
|
1361
|
+
function pickAttr(span, ...keys) {
|
|
1362
|
+
for (const k of keys) {
|
|
1363
|
+
const v = span.attributes[k];
|
|
1364
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
1365
|
+
}
|
|
1366
|
+
return void 0;
|
|
1367
|
+
}
|
|
1368
|
+
function hostFromUrl(u) {
|
|
1369
|
+
if (!u) return void 0;
|
|
1370
|
+
try {
|
|
1371
|
+
return new URL(u).hostname;
|
|
1372
|
+
} catch {
|
|
1373
|
+
return void 0;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function pickAddress(span) {
|
|
1377
|
+
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1378
|
+
}
|
|
1379
|
+
function makeObservedEdgeId(type, source, target) {
|
|
1380
|
+
return (0, import_types3.observedEdgeId)(source, target, type);
|
|
1381
|
+
}
|
|
1382
|
+
function makeInferredEdgeId(type, source, target) {
|
|
1383
|
+
return (0, import_types3.inferredEdgeId)(source, target, type);
|
|
1384
|
+
}
|
|
1385
|
+
var INFERRED_CONFIDENCE = 0.6;
|
|
1386
|
+
var STITCH_MAX_DEPTH = 2;
|
|
1387
|
+
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
856
1388
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1389
|
+
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
1390
|
+
function parentSpanKey(traceId, spanId) {
|
|
1391
|
+
return `${traceId}:${spanId}`;
|
|
1392
|
+
}
|
|
1393
|
+
function cacheSpanService(span, now) {
|
|
1394
|
+
if (!span.traceId || !span.spanId) return;
|
|
1395
|
+
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1396
|
+
parentSpanCache.delete(key);
|
|
1397
|
+
parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
|
|
1398
|
+
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1399
|
+
const oldest = parentSpanCache.keys().next().value;
|
|
1400
|
+
if (!oldest) break;
|
|
1401
|
+
parentSpanCache.delete(oldest);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function lookupParentSpanService(traceId, parentSpanId, now) {
|
|
1405
|
+
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1406
|
+
if (!entry2) return null;
|
|
1407
|
+
if (entry2.expiresAt <= now) {
|
|
1408
|
+
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
return entry2.service;
|
|
1412
|
+
}
|
|
1413
|
+
function resolveServiceId(graph, host) {
|
|
1414
|
+
const direct = (0, import_types3.serviceId)(host);
|
|
1415
|
+
if (graph.hasNode(direct)) return direct;
|
|
1416
|
+
let found = null;
|
|
1417
|
+
graph.forEachNode((id, attrs) => {
|
|
1418
|
+
if (found) return;
|
|
1419
|
+
const a = attrs;
|
|
1420
|
+
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1421
|
+
if (a.name === host) {
|
|
1422
|
+
found = id;
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (a.aliases && a.aliases.includes(host)) {
|
|
1426
|
+
found = id;
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
return found;
|
|
1430
|
+
}
|
|
1431
|
+
function frontierIdFor(host) {
|
|
1432
|
+
return (0, import_types3.frontierId)(host);
|
|
1433
|
+
}
|
|
1434
|
+
function ensureServiceNode(graph, serviceName) {
|
|
1435
|
+
const id = (0, import_types3.serviceId)(serviceName);
|
|
1436
|
+
if (graph.hasNode(id)) return id;
|
|
1437
|
+
const node = {
|
|
1438
|
+
id,
|
|
1439
|
+
type: import_types3.NodeType.ServiceNode,
|
|
1440
|
+
name: serviceName,
|
|
1441
|
+
language: "unknown",
|
|
1442
|
+
discoveredVia: "otel"
|
|
1443
|
+
};
|
|
1444
|
+
graph.addNode(id, node);
|
|
1445
|
+
return id;
|
|
1446
|
+
}
|
|
1447
|
+
function ensureDatabaseNode(graph, host, engine) {
|
|
1448
|
+
const id = (0, import_types3.databaseId)(host);
|
|
1449
|
+
if (graph.hasNode(id)) return id;
|
|
1450
|
+
const node = {
|
|
1451
|
+
id,
|
|
1452
|
+
type: import_types3.NodeType.DatabaseNode,
|
|
1453
|
+
name: host,
|
|
1454
|
+
engine,
|
|
1455
|
+
engineVersion: "unknown",
|
|
1456
|
+
compatibleDrivers: [],
|
|
1457
|
+
host,
|
|
1458
|
+
discoveredVia: "otel"
|
|
1459
|
+
};
|
|
1460
|
+
graph.addNode(id, node);
|
|
1461
|
+
return id;
|
|
1462
|
+
}
|
|
1463
|
+
function ensureFrontierNode(graph, host, ts) {
|
|
1464
|
+
const id = frontierIdFor(host);
|
|
1465
|
+
if (graph.hasNode(id)) {
|
|
1466
|
+
const existing = graph.getNodeAttributes(id);
|
|
1467
|
+
graph.replaceNodeAttributes(id, { ...existing, lastObserved: ts });
|
|
1468
|
+
return id;
|
|
1469
|
+
}
|
|
1470
|
+
const node = {
|
|
1471
|
+
id,
|
|
1472
|
+
type: import_types3.NodeType.FrontierNode,
|
|
1473
|
+
name: host,
|
|
1474
|
+
host,
|
|
1475
|
+
firstObserved: ts,
|
|
1476
|
+
lastObserved: ts
|
|
1477
|
+
};
|
|
1478
|
+
graph.addNode(id, node);
|
|
1479
|
+
return id;
|
|
1480
|
+
}
|
|
1481
|
+
function upsertFrontierEdge(graph, type, source, target, ts) {
|
|
1482
|
+
const id = (0, import_types3.frontierEdgeId)(source, target, type);
|
|
1483
|
+
if (graph.hasEdge(id)) {
|
|
1484
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1485
|
+
const updated = {
|
|
1486
|
+
...existing,
|
|
1487
|
+
provenance: import_types3.Provenance.FRONTIER,
|
|
1488
|
+
lastObserved: ts,
|
|
1489
|
+
callCount: (existing.callCount ?? 0) + 1
|
|
1490
|
+
};
|
|
1491
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const edge = {
|
|
1495
|
+
id,
|
|
1496
|
+
source,
|
|
1497
|
+
target,
|
|
1498
|
+
type,
|
|
1499
|
+
provenance: import_types3.Provenance.FRONTIER,
|
|
1500
|
+
confidence: 1,
|
|
1501
|
+
lastObserved: ts,
|
|
1502
|
+
callCount: 1
|
|
1503
|
+
};
|
|
1504
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1505
|
+
}
|
|
1506
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1507
|
+
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1508
|
+
const id = makeObservedEdgeId(type, source, target);
|
|
1509
|
+
if (graph.hasEdge(id)) {
|
|
1510
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1511
|
+
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1512
|
+
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1513
|
+
const updated = {
|
|
1514
|
+
...existing,
|
|
1515
|
+
provenance: import_types3.Provenance.OBSERVED,
|
|
1516
|
+
lastObserved: ts,
|
|
1517
|
+
callCount: newSpanCount,
|
|
1518
|
+
signal: {
|
|
1519
|
+
spanCount: newSpanCount,
|
|
1520
|
+
errorCount: newErrorCount,
|
|
1521
|
+
lastObservedAgeMs: 0
|
|
1522
|
+
},
|
|
1523
|
+
confidence: 1
|
|
1524
|
+
};
|
|
1525
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1526
|
+
return { edge: updated, created: false };
|
|
1527
|
+
}
|
|
1528
|
+
const edge = {
|
|
1529
|
+
id,
|
|
1530
|
+
source,
|
|
1531
|
+
target,
|
|
1532
|
+
type,
|
|
1533
|
+
provenance: import_types3.Provenance.OBSERVED,
|
|
1534
|
+
confidence: 1,
|
|
1535
|
+
lastObserved: ts,
|
|
1536
|
+
callCount: 1,
|
|
1537
|
+
signal: {
|
|
1538
|
+
spanCount: 1,
|
|
1539
|
+
errorCount: isError ? 1 : 0,
|
|
1540
|
+
lastObservedAgeMs: 0
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1544
|
+
return { edge, created: true };
|
|
1545
|
+
}
|
|
1546
|
+
function stitchTrace(graph, sourceServiceId, ts) {
|
|
1547
|
+
if (!graph.hasNode(sourceServiceId)) return;
|
|
1548
|
+
const visited = /* @__PURE__ */ new Set([sourceServiceId]);
|
|
1549
|
+
const queue = [{ nodeId: sourceServiceId, depth: 0 }];
|
|
1550
|
+
while (queue.length > 0) {
|
|
1551
|
+
const { nodeId, depth } = queue.shift();
|
|
1552
|
+
if (depth >= STITCH_MAX_DEPTH) continue;
|
|
1553
|
+
const outbound = graph.outboundEdges(nodeId);
|
|
1554
|
+
for (const edgeId of outbound) {
|
|
1555
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1556
|
+
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
1557
|
+
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
1558
|
+
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
1559
|
+
if (!visited.has(edge.target)) {
|
|
1560
|
+
visited.add(edge.target);
|
|
1561
|
+
queue.push({ nodeId: edge.target, depth: depth + 1 });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
function upsertInferredEdge(graph, type, source, target, ts) {
|
|
1567
|
+
const id = makeInferredEdgeId(type, source, target);
|
|
1568
|
+
if (graph.hasEdge(id)) {
|
|
1569
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1570
|
+
const updated = { ...existing, lastObserved: ts };
|
|
1571
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const edge = {
|
|
1575
|
+
id,
|
|
1576
|
+
source,
|
|
1577
|
+
target,
|
|
1578
|
+
type,
|
|
1579
|
+
provenance: import_types3.Provenance.INFERRED,
|
|
1580
|
+
confidence: INFERRED_CONFIDENCE,
|
|
1581
|
+
lastObserved: ts
|
|
1582
|
+
};
|
|
1583
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1584
|
+
}
|
|
1585
|
+
async function appendErrorEvent(ctx, ev) {
|
|
1586
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
1587
|
+
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1588
|
+
}
|
|
1589
|
+
function buildErrorEventForReceiver(span) {
|
|
1590
|
+
if (span.statusCode !== 2) return null;
|
|
1591
|
+
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1592
|
+
return {
|
|
1593
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
1594
|
+
timestamp: ts,
|
|
1595
|
+
service: span.service,
|
|
1596
|
+
traceId: span.traceId,
|
|
1597
|
+
spanId: span.spanId,
|
|
1598
|
+
errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
|
|
1599
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1600
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1601
|
+
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function makeErrorSpanWriter(errorsPath) {
|
|
1605
|
+
return async (span) => {
|
|
1606
|
+
const ev = buildErrorEventForReceiver(span);
|
|
1607
|
+
if (!ev) return;
|
|
1608
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
|
|
1609
|
+
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
async function handleSpan(ctx, span) {
|
|
1613
|
+
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1614
|
+
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1615
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service);
|
|
1616
|
+
const isError = span.statusCode === 2;
|
|
1617
|
+
cacheSpanService(span, nowMs);
|
|
1618
|
+
let affectedNode = sourceId;
|
|
1619
|
+
if (span.dbSystem) {
|
|
1620
|
+
const host = pickAddress(span);
|
|
1621
|
+
if (host) {
|
|
1622
|
+
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
1623
|
+
const targetId = (0, import_types3.databaseId)(host);
|
|
1624
|
+
const result = upsertObservedEdge(
|
|
1625
|
+
ctx.graph,
|
|
1626
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
1627
|
+
sourceId,
|
|
1628
|
+
targetId,
|
|
1629
|
+
ts,
|
|
1630
|
+
isError
|
|
1631
|
+
);
|
|
1632
|
+
if (result) affectedNode = targetId;
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
const host = pickAddress(span);
|
|
1636
|
+
let resolvedViaAddress = false;
|
|
1637
|
+
if (host && host !== span.service) {
|
|
1638
|
+
const targetId = resolveServiceId(ctx.graph, host);
|
|
1639
|
+
if (targetId && targetId !== sourceId) {
|
|
1640
|
+
upsertObservedEdge(
|
|
1641
|
+
ctx.graph,
|
|
1642
|
+
import_types3.EdgeType.CALLS,
|
|
1643
|
+
sourceId,
|
|
1644
|
+
targetId,
|
|
1645
|
+
ts,
|
|
1646
|
+
isError
|
|
1647
|
+
);
|
|
1648
|
+
affectedNode = targetId;
|
|
1649
|
+
resolvedViaAddress = true;
|
|
1650
|
+
} else if (!targetId) {
|
|
1651
|
+
const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
|
|
1652
|
+
if (ctx.graph.hasNode(sourceId)) {
|
|
1653
|
+
upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
|
|
1654
|
+
}
|
|
1655
|
+
affectedNode = frontierId2;
|
|
1656
|
+
resolvedViaAddress = true;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1660
|
+
const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
|
|
1661
|
+
if (parentService && parentService !== span.service) {
|
|
1662
|
+
const parentId = ensureServiceNode(ctx.graph, parentService);
|
|
1663
|
+
upsertObservedEdge(
|
|
1664
|
+
ctx.graph,
|
|
1665
|
+
import_types3.EdgeType.CALLS,
|
|
1666
|
+
parentId,
|
|
1667
|
+
sourceId,
|
|
1668
|
+
ts,
|
|
1669
|
+
isError
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (span.statusCode === 2) {
|
|
1675
|
+
stitchTrace(ctx.graph, sourceId, ts);
|
|
1676
|
+
if (ctx.writeErrorEventInline !== false) {
|
|
1677
|
+
const ev = {
|
|
1678
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
1679
|
+
timestamp: ts,
|
|
1680
|
+
service: span.service,
|
|
1681
|
+
traceId: span.traceId,
|
|
1682
|
+
spanId: span.spanId,
|
|
1683
|
+
errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
|
|
1684
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1685
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1686
|
+
affectedNode
|
|
1687
|
+
};
|
|
1688
|
+
await appendErrorEvent(ctx, ev);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
void affectedNode;
|
|
1692
|
+
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
1693
|
+
}
|
|
857
1694
|
function promoteFrontierNodes(graph, opts = {}) {
|
|
858
1695
|
const aliasIndex = /* @__PURE__ */ new Map();
|
|
859
1696
|
graph.forEachNode((id, attrs) => {
|
|
@@ -927,8 +1764,27 @@ function pickLater(a, b) {
|
|
|
927
1764
|
if (!b) return a;
|
|
928
1765
|
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
929
1766
|
}
|
|
1767
|
+
async function readStaleEvents(staleEventsPath) {
|
|
1768
|
+
try {
|
|
1769
|
+
const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
|
|
1770
|
+
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
if (err.code === "ENOENT") return [];
|
|
1773
|
+
throw err;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function readErrorEvents(errorsPath) {
|
|
1777
|
+
try {
|
|
1778
|
+
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
1779
|
+
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
if (err.code === "ENOENT") return [];
|
|
1782
|
+
throw err;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
930
1785
|
|
|
931
1786
|
// src/extract/services.ts
|
|
1787
|
+
init_cjs_shims();
|
|
932
1788
|
var import_node_fs7 = require("fs");
|
|
933
1789
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
934
1790
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
@@ -936,6 +1792,7 @@ var import_minimatch2 = require("minimatch");
|
|
|
936
1792
|
var import_types5 = require("@neat.is/types");
|
|
937
1793
|
|
|
938
1794
|
// src/extract/shared.ts
|
|
1795
|
+
init_cjs_shims();
|
|
939
1796
|
var import_node_fs4 = require("fs");
|
|
940
1797
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
941
1798
|
var import_yaml = require("yaml");
|
|
@@ -978,6 +1835,7 @@ async function exists(p) {
|
|
|
978
1835
|
}
|
|
979
1836
|
|
|
980
1837
|
// src/extract/python.ts
|
|
1838
|
+
init_cjs_shims();
|
|
981
1839
|
var import_node_fs5 = require("fs");
|
|
982
1840
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
983
1841
|
var import_smol_toml = require("smol-toml");
|
|
@@ -1044,6 +1902,7 @@ function pythonToPackage(service) {
|
|
|
1044
1902
|
}
|
|
1045
1903
|
|
|
1046
1904
|
// src/extract/owners.ts
|
|
1905
|
+
init_cjs_shims();
|
|
1047
1906
|
var import_node_fs6 = require("fs");
|
|
1048
1907
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
1049
1908
|
var import_minimatch = require("minimatch");
|
|
@@ -1294,6 +2153,7 @@ function addServiceNodes(graph, services) {
|
|
|
1294
2153
|
}
|
|
1295
2154
|
|
|
1296
2155
|
// src/extract/aliases.ts
|
|
2156
|
+
init_cjs_shims();
|
|
1297
2157
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
1298
2158
|
var import_node_fs8 = require("fs");
|
|
1299
2159
|
var import_yaml2 = require("yaml");
|
|
@@ -1458,10 +2318,12 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
1458
2318
|
}
|
|
1459
2319
|
|
|
1460
2320
|
// src/extract/databases/index.ts
|
|
2321
|
+
init_cjs_shims();
|
|
1461
2322
|
var import_node_path16 = __toESM(require("path"), 1);
|
|
1462
2323
|
var import_types7 = require("@neat.is/types");
|
|
1463
2324
|
|
|
1464
2325
|
// src/extract/databases/db-config-yaml.ts
|
|
2326
|
+
init_cjs_shims();
|
|
1465
2327
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
1466
2328
|
async function parse(serviceDir) {
|
|
1467
2329
|
const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
|
|
@@ -1481,10 +2343,12 @@ async function parse(serviceDir) {
|
|
|
1481
2343
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
1482
2344
|
|
|
1483
2345
|
// src/extract/databases/dotenv.ts
|
|
2346
|
+
init_cjs_shims();
|
|
1484
2347
|
var import_node_fs10 = require("fs");
|
|
1485
2348
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
1486
2349
|
|
|
1487
2350
|
// src/extract/databases/shared.ts
|
|
2351
|
+
init_cjs_shims();
|
|
1488
2352
|
var import_node_fs9 = require("fs");
|
|
1489
2353
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
1490
2354
|
function schemeToEngine(scheme) {
|
|
@@ -1609,6 +2473,7 @@ async function parse2(serviceDir) {
|
|
|
1609
2473
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
1610
2474
|
|
|
1611
2475
|
// src/extract/databases/prisma.ts
|
|
2476
|
+
init_cjs_shims();
|
|
1612
2477
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
1613
2478
|
async function parse3(serviceDir) {
|
|
1614
2479
|
const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
|
|
@@ -1639,6 +2504,7 @@ async function parse3(serviceDir) {
|
|
|
1639
2504
|
var prismaParser = { name: "prisma", parse: parse3 };
|
|
1640
2505
|
|
|
1641
2506
|
// src/extract/databases/drizzle.ts
|
|
2507
|
+
init_cjs_shims();
|
|
1642
2508
|
var DIALECT_TO_ENGINE = {
|
|
1643
2509
|
postgresql: "postgresql",
|
|
1644
2510
|
postgres: "postgresql",
|
|
@@ -1690,6 +2556,7 @@ async function parse4(serviceDir) {
|
|
|
1690
2556
|
var drizzleParser = { name: "drizzle", parse: parse4 };
|
|
1691
2557
|
|
|
1692
2558
|
// src/extract/databases/knex.ts
|
|
2559
|
+
init_cjs_shims();
|
|
1693
2560
|
var CLIENT_TO_ENGINE = {
|
|
1694
2561
|
pg: "postgresql",
|
|
1695
2562
|
postgres: "postgresql",
|
|
@@ -1740,6 +2607,7 @@ async function parse5(serviceDir) {
|
|
|
1740
2607
|
var knexParser = { name: "knex", parse: parse5 };
|
|
1741
2608
|
|
|
1742
2609
|
// src/extract/databases/ormconfig.ts
|
|
2610
|
+
init_cjs_shims();
|
|
1743
2611
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
1744
2612
|
async function parse6(serviceDir) {
|
|
1745
2613
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
@@ -1768,6 +2636,7 @@ async function parse6(serviceDir) {
|
|
|
1768
2636
|
var ormconfigParser = { name: "ormconfig", parse: parse6 };
|
|
1769
2637
|
|
|
1770
2638
|
// src/extract/databases/typeorm.ts
|
|
2639
|
+
init_cjs_shims();
|
|
1771
2640
|
async function parse7(serviceDir) {
|
|
1772
2641
|
const filePath = await findFirst(serviceDir, [
|
|
1773
2642
|
"data-source.ts",
|
|
@@ -1801,6 +2670,7 @@ async function parse7(serviceDir) {
|
|
|
1801
2670
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
1802
2671
|
|
|
1803
2672
|
// src/extract/databases/sequelize.ts
|
|
2673
|
+
init_cjs_shims();
|
|
1804
2674
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
1805
2675
|
async function parse8(serviceDir) {
|
|
1806
2676
|
const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
|
|
@@ -1829,6 +2699,7 @@ async function parse8(serviceDir) {
|
|
|
1829
2699
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
1830
2700
|
|
|
1831
2701
|
// src/extract/databases/docker-compose.ts
|
|
2702
|
+
init_cjs_shims();
|
|
1832
2703
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
1833
2704
|
function portFromService(svc) {
|
|
1834
2705
|
for (const raw of svc.ports ?? []) {
|
|
@@ -2063,6 +2934,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2063
2934
|
}
|
|
2064
2935
|
|
|
2065
2936
|
// src/extract/configs.ts
|
|
2937
|
+
init_cjs_shims();
|
|
2066
2938
|
var import_node_fs11 = require("fs");
|
|
2067
2939
|
var import_node_path17 = __toESM(require("path"), 1);
|
|
2068
2940
|
var import_types8 = require("@neat.is/types");
|
|
@@ -2118,9 +2990,11 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2118
2990
|
}
|
|
2119
2991
|
|
|
2120
2992
|
// src/extract/calls/index.ts
|
|
2993
|
+
init_cjs_shims();
|
|
2121
2994
|
var import_types14 = require("@neat.is/types");
|
|
2122
2995
|
|
|
2123
2996
|
// src/extract/calls/http.ts
|
|
2997
|
+
init_cjs_shims();
|
|
2124
2998
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
2125
2999
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
2126
3000
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
@@ -2128,6 +3002,7 @@ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
|
2128
3002
|
var import_types9 = require("@neat.is/types");
|
|
2129
3003
|
|
|
2130
3004
|
// src/extract/calls/shared.ts
|
|
3005
|
+
init_cjs_shims();
|
|
2131
3006
|
var import_node_fs12 = require("fs");
|
|
2132
3007
|
var import_node_path18 = __toESM(require("path"), 1);
|
|
2133
3008
|
async function walkSourceFiles(dir) {
|
|
@@ -2260,6 +3135,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2260
3135
|
}
|
|
2261
3136
|
|
|
2262
3137
|
// src/extract/calls/kafka.ts
|
|
3138
|
+
init_cjs_shims();
|
|
2263
3139
|
var import_node_path20 = __toESM(require("path"), 1);
|
|
2264
3140
|
var import_types10 = require("@neat.is/types");
|
|
2265
3141
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -2299,6 +3175,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
2299
3175
|
}
|
|
2300
3176
|
|
|
2301
3177
|
// src/extract/calls/redis.ts
|
|
3178
|
+
init_cjs_shims();
|
|
2302
3179
|
var import_node_path21 = __toESM(require("path"), 1);
|
|
2303
3180
|
var import_types11 = require("@neat.is/types");
|
|
2304
3181
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
@@ -2328,6 +3205,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
2328
3205
|
}
|
|
2329
3206
|
|
|
2330
3207
|
// src/extract/calls/aws.ts
|
|
3208
|
+
init_cjs_shims();
|
|
2331
3209
|
var import_node_path22 = __toESM(require("path"), 1);
|
|
2332
3210
|
var import_types12 = require("@neat.is/types");
|
|
2333
3211
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -2382,6 +3260,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
2382
3260
|
}
|
|
2383
3261
|
|
|
2384
3262
|
// src/extract/calls/grpc.ts
|
|
3263
|
+
init_cjs_shims();
|
|
2385
3264
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
2386
3265
|
var import_types13 = require("@neat.is/types");
|
|
2387
3266
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
@@ -2482,11 +3361,16 @@ async function addCallEdges(graph, services) {
|
|
|
2482
3361
|
};
|
|
2483
3362
|
}
|
|
2484
3363
|
|
|
3364
|
+
// src/extract/infra/index.ts
|
|
3365
|
+
init_cjs_shims();
|
|
3366
|
+
|
|
2485
3367
|
// src/extract/infra/docker-compose.ts
|
|
3368
|
+
init_cjs_shims();
|
|
2486
3369
|
var import_node_path24 = __toESM(require("path"), 1);
|
|
2487
3370
|
var import_types16 = require("@neat.is/types");
|
|
2488
3371
|
|
|
2489
3372
|
// src/extract/infra/shared.ts
|
|
3373
|
+
init_cjs_shims();
|
|
2490
3374
|
var import_types15 = require("@neat.is/types");
|
|
2491
3375
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
2492
3376
|
return {
|
|
@@ -2586,6 +3470,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
2586
3470
|
}
|
|
2587
3471
|
|
|
2588
3472
|
// src/extract/infra/dockerfile.ts
|
|
3473
|
+
init_cjs_shims();
|
|
2589
3474
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
2590
3475
|
var import_node_fs13 = require("fs");
|
|
2591
3476
|
var import_types17 = require("@neat.is/types");
|
|
@@ -2645,6 +3530,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
2645
3530
|
}
|
|
2646
3531
|
|
|
2647
3532
|
// src/extract/infra/terraform.ts
|
|
3533
|
+
init_cjs_shims();
|
|
2648
3534
|
var import_node_fs14 = require("fs");
|
|
2649
3535
|
var import_node_path26 = __toESM(require("path"), 1);
|
|
2650
3536
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
@@ -2683,6 +3569,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
2683
3569
|
}
|
|
2684
3570
|
|
|
2685
3571
|
// src/extract/infra/k8s.ts
|
|
3572
|
+
init_cjs_shims();
|
|
2686
3573
|
var import_node_fs15 = require("fs");
|
|
2687
3574
|
var import_node_path27 = __toESM(require("path"), 1);
|
|
2688
3575
|
var import_yaml3 = require("yaml");
|
|
@@ -2778,6 +3665,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
2778
3665
|
}
|
|
2779
3666
|
|
|
2780
3667
|
// src/persist.ts
|
|
3668
|
+
init_cjs_shims();
|
|
2781
3669
|
var import_node_fs16 = require("fs");
|
|
2782
3670
|
var import_node_path28 = __toESM(require("path"), 1);
|
|
2783
3671
|
var SCHEMA_VERSION = 2;
|
|
@@ -2861,6 +3749,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
2861
3749
|
}
|
|
2862
3750
|
|
|
2863
3751
|
// src/projects.ts
|
|
3752
|
+
init_cjs_shims();
|
|
2864
3753
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
2865
3754
|
function pathsForProject(project, baseDir) {
|
|
2866
3755
|
if (project === DEFAULT_PROJECT) {
|
|
@@ -2880,12 +3769,363 @@ function pathsForProject(project, baseDir) {
|
|
|
2880
3769
|
policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
2881
3770
|
};
|
|
2882
3771
|
}
|
|
3772
|
+
var Projects = class {
|
|
3773
|
+
contexts = /* @__PURE__ */ new Map();
|
|
3774
|
+
upsert(ctx) {
|
|
3775
|
+
this.contexts.set(ctx.name, ctx);
|
|
3776
|
+
}
|
|
3777
|
+
set(name, init) {
|
|
3778
|
+
const ctx = {
|
|
3779
|
+
name,
|
|
3780
|
+
graph: init.graph ?? getGraph(name),
|
|
3781
|
+
scanPath: init.scanPath,
|
|
3782
|
+
paths: init.paths,
|
|
3783
|
+
searchIndex: init.searchIndex
|
|
3784
|
+
};
|
|
3785
|
+
this.contexts.set(name, ctx);
|
|
3786
|
+
return ctx;
|
|
3787
|
+
}
|
|
3788
|
+
get(name) {
|
|
3789
|
+
return this.contexts.get(name);
|
|
3790
|
+
}
|
|
3791
|
+
has(name) {
|
|
3792
|
+
return this.contexts.has(name);
|
|
3793
|
+
}
|
|
3794
|
+
list() {
|
|
3795
|
+
return [...this.contexts.keys()].sort();
|
|
3796
|
+
}
|
|
3797
|
+
attachSearchIndex(name, index) {
|
|
3798
|
+
const ctx = this.contexts.get(name);
|
|
3799
|
+
if (ctx) ctx.searchIndex = index;
|
|
3800
|
+
}
|
|
3801
|
+
};
|
|
2883
3802
|
|
|
2884
|
-
// src/
|
|
3803
|
+
// src/api.ts
|
|
3804
|
+
init_cjs_shims();
|
|
3805
|
+
var import_fastify = __toESM(require("fastify"), 1);
|
|
3806
|
+
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3807
|
+
var import_types20 = require("@neat.is/types");
|
|
3808
|
+
|
|
3809
|
+
// src/divergences.ts
|
|
3810
|
+
init_cjs_shims();
|
|
3811
|
+
var import_types18 = require("@neat.is/types");
|
|
3812
|
+
function bucketKey(source, target, type) {
|
|
3813
|
+
return `${type}|${source}|${target}`;
|
|
3814
|
+
}
|
|
3815
|
+
function bucketEdges(graph) {
|
|
3816
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3817
|
+
graph.forEachEdge((id, attrs) => {
|
|
3818
|
+
const e = attrs;
|
|
3819
|
+
const parsed = (0, import_types18.parseEdgeId)(id);
|
|
3820
|
+
const provenance = parsed?.provenance ?? e.provenance;
|
|
3821
|
+
const key = bucketKey(e.source, e.target, e.type);
|
|
3822
|
+
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3823
|
+
switch (provenance) {
|
|
3824
|
+
case import_types18.Provenance.EXTRACTED:
|
|
3825
|
+
cur.extracted = e;
|
|
3826
|
+
break;
|
|
3827
|
+
case import_types18.Provenance.OBSERVED:
|
|
3828
|
+
cur.observed = e;
|
|
3829
|
+
break;
|
|
3830
|
+
case import_types18.Provenance.INFERRED:
|
|
3831
|
+
cur.inferred = e;
|
|
3832
|
+
break;
|
|
3833
|
+
case import_types18.Provenance.FRONTIER:
|
|
3834
|
+
cur.frontier = e;
|
|
3835
|
+
break;
|
|
3836
|
+
default:
|
|
3837
|
+
if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
|
|
3838
|
+
}
|
|
3839
|
+
buckets.set(key, cur);
|
|
3840
|
+
});
|
|
3841
|
+
return buckets;
|
|
3842
|
+
}
|
|
3843
|
+
function nodeIsFrontier(graph, nodeId) {
|
|
3844
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
3845
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
3846
|
+
return attrs.type === import_types18.NodeType.FrontierNode;
|
|
3847
|
+
}
|
|
3848
|
+
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3849
|
+
if (!graph.hasNode(sourceId)) return false;
|
|
3850
|
+
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3851
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3852
|
+
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3853
|
+
}
|
|
3854
|
+
return false;
|
|
3855
|
+
}
|
|
3856
|
+
function clampConfidence(n) {
|
|
3857
|
+
if (!Number.isFinite(n)) return 0;
|
|
3858
|
+
return Math.max(0, Math.min(1, n));
|
|
3859
|
+
}
|
|
3860
|
+
function reasonForMissingObserved(source, target, type) {
|
|
3861
|
+
return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
|
|
3862
|
+
}
|
|
3863
|
+
function reasonForMissingExtracted(source, target, type) {
|
|
3864
|
+
return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
|
|
3865
|
+
}
|
|
3866
|
+
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3867
|
+
var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
|
|
3868
|
+
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
3869
|
+
function detectMissingDivergences(graph, bucket) {
|
|
3870
|
+
const out = [];
|
|
3871
|
+
if (bucket.extracted && !bucket.observed) {
|
|
3872
|
+
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3873
|
+
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3874
|
+
out.push({
|
|
3875
|
+
type: "missing-observed",
|
|
3876
|
+
source: bucket.source,
|
|
3877
|
+
target: bucket.target,
|
|
3878
|
+
edgeType: bucket.type,
|
|
3879
|
+
extracted: bucket.extracted,
|
|
3880
|
+
confidence: sourceHasTraffic ? 1 : 0.5,
|
|
3881
|
+
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3882
|
+
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
if (bucket.observed && !bucket.extracted) {
|
|
3887
|
+
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3888
|
+
out.push({
|
|
3889
|
+
type: "missing-extracted",
|
|
3890
|
+
source: bucket.source,
|
|
3891
|
+
target: bucket.target,
|
|
3892
|
+
edgeType: bucket.type,
|
|
3893
|
+
observed: bucket.observed,
|
|
3894
|
+
confidence: cascaded,
|
|
3895
|
+
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
3896
|
+
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
return out;
|
|
3900
|
+
}
|
|
3901
|
+
function declaredHostFor(svc) {
|
|
3902
|
+
const raw = svc.dbConnectionTarget?.trim();
|
|
3903
|
+
if (!raw) return null;
|
|
3904
|
+
const colon = raw.lastIndexOf(":");
|
|
3905
|
+
if (colon === -1) return raw;
|
|
3906
|
+
const port = raw.slice(colon + 1);
|
|
3907
|
+
if (/^\d+$/.test(port)) return raw.slice(0, colon);
|
|
3908
|
+
return raw;
|
|
3909
|
+
}
|
|
3910
|
+
function hasExtractedConfiguredBy(graph, svcId) {
|
|
3911
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3912
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3913
|
+
if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
|
|
3914
|
+
return true;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return false;
|
|
3918
|
+
}
|
|
3919
|
+
function detectHostMismatch(graph, svcId, svc) {
|
|
3920
|
+
const declaredHost = declaredHostFor(svc);
|
|
3921
|
+
if (!declaredHost) return [];
|
|
3922
|
+
if (!hasExtractedConfiguredBy(graph, svcId)) return [];
|
|
3923
|
+
const out = [];
|
|
3924
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3925
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
3926
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
3927
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
3928
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
3929
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
3930
|
+
const observedHost = target.host?.trim();
|
|
3931
|
+
if (!observedHost) continue;
|
|
3932
|
+
if (observedHost === declaredHost) continue;
|
|
3933
|
+
out.push({
|
|
3934
|
+
type: "host-mismatch",
|
|
3935
|
+
source: svcId,
|
|
3936
|
+
target: edge.target,
|
|
3937
|
+
extractedHost: declaredHost,
|
|
3938
|
+
observedHost,
|
|
3939
|
+
confidence: clampConfidence(confidenceForEdge(edge)),
|
|
3940
|
+
reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
|
|
3941
|
+
recommendation: RECOMMENDATION_HOST_MISMATCH
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
return out;
|
|
3945
|
+
}
|
|
3946
|
+
function detectCompatDivergences(graph, svcId, svc) {
|
|
3947
|
+
const out = [];
|
|
3948
|
+
const deps = svc.dependencies ?? {};
|
|
3949
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3950
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
3951
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
3952
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
3953
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
3954
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
3955
|
+
for (const pair of compatPairs()) {
|
|
3956
|
+
if (pair.engine !== target.engine) continue;
|
|
3957
|
+
const declared = deps[pair.driver];
|
|
3958
|
+
if (!declared) continue;
|
|
3959
|
+
const result = checkCompatibility(
|
|
3960
|
+
pair.driver,
|
|
3961
|
+
declared,
|
|
3962
|
+
target.engine,
|
|
3963
|
+
target.engineVersion
|
|
3964
|
+
);
|
|
3965
|
+
if (!result.compatible && result.reason) {
|
|
3966
|
+
out.push({
|
|
3967
|
+
type: "version-mismatch",
|
|
3968
|
+
source: svcId,
|
|
3969
|
+
target: edge.target,
|
|
3970
|
+
extractedVersion: declared,
|
|
3971
|
+
observedVersion: target.engineVersion,
|
|
3972
|
+
compatibility: "incompatible",
|
|
3973
|
+
confidence: 1,
|
|
3974
|
+
reason: result.reason,
|
|
3975
|
+
recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
for (const rule of deprecatedApis()) {
|
|
3980
|
+
const declared = deps[rule.package];
|
|
3981
|
+
if (!declared) continue;
|
|
3982
|
+
const result = checkDeprecatedApi(rule, declared);
|
|
3983
|
+
if (!result.compatible && result.reason) {
|
|
3984
|
+
const ruleRef = {
|
|
3985
|
+
kind: rule.kind ?? "deprecated-api",
|
|
3986
|
+
reason: result.reason,
|
|
3987
|
+
package: rule.package
|
|
3988
|
+
};
|
|
3989
|
+
out.push({
|
|
3990
|
+
type: "compat-violation",
|
|
3991
|
+
source: svcId,
|
|
3992
|
+
target: edge.target,
|
|
3993
|
+
rule: ruleRef,
|
|
3994
|
+
observed: edge,
|
|
3995
|
+
confidence: 1,
|
|
3996
|
+
reason: result.reason,
|
|
3997
|
+
recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
return out;
|
|
4003
|
+
}
|
|
4004
|
+
function involvesNode(d, nodeId) {
|
|
4005
|
+
return d.source === nodeId || d.target === nodeId;
|
|
4006
|
+
}
|
|
4007
|
+
function computeDivergences(graph, opts = {}) {
|
|
4008
|
+
const all = [];
|
|
4009
|
+
const buckets = bucketEdges(graph);
|
|
4010
|
+
for (const bucket of buckets.values()) {
|
|
4011
|
+
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
4012
|
+
}
|
|
4013
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
4014
|
+
const n = attrs;
|
|
4015
|
+
if (n.type !== import_types18.NodeType.ServiceNode) return;
|
|
4016
|
+
const svc = n;
|
|
4017
|
+
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4018
|
+
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4019
|
+
});
|
|
4020
|
+
let filtered = all;
|
|
4021
|
+
if (opts.type) {
|
|
4022
|
+
const allowed = opts.type;
|
|
4023
|
+
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
4024
|
+
}
|
|
4025
|
+
if (opts.minConfidence !== void 0) {
|
|
4026
|
+
const threshold = opts.minConfidence;
|
|
4027
|
+
filtered = filtered.filter((d) => d.confidence >= threshold);
|
|
4028
|
+
}
|
|
4029
|
+
if (opts.node) {
|
|
4030
|
+
const target = opts.node;
|
|
4031
|
+
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4032
|
+
}
|
|
4033
|
+
filtered.sort((a, b) => {
|
|
4034
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4035
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4036
|
+
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4037
|
+
return a.target.localeCompare(b.target);
|
|
4038
|
+
});
|
|
4039
|
+
return import_types18.DivergenceResultSchema.parse({
|
|
4040
|
+
divergences: filtered,
|
|
4041
|
+
totalAffected: filtered.length,
|
|
4042
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// src/diff.ts
|
|
4047
|
+
init_cjs_shims();
|
|
2885
4048
|
var import_node_fs17 = require("fs");
|
|
4049
|
+
async function loadSnapshotForDiff(target) {
|
|
4050
|
+
if (/^https?:\/\//i.test(target)) {
|
|
4051
|
+
const res = await fetch(target);
|
|
4052
|
+
if (!res.ok) {
|
|
4053
|
+
throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
|
|
4054
|
+
}
|
|
4055
|
+
return await res.json();
|
|
4056
|
+
}
|
|
4057
|
+
const raw = await import_node_fs17.promises.readFile(target, "utf8");
|
|
4058
|
+
return JSON.parse(raw);
|
|
4059
|
+
}
|
|
4060
|
+
function indexEntries(entries) {
|
|
4061
|
+
const m = /* @__PURE__ */ new Map();
|
|
4062
|
+
if (!entries) return m;
|
|
4063
|
+
for (const entry2 of entries) {
|
|
4064
|
+
const id = entry2.attributes?.id ?? entry2.key;
|
|
4065
|
+
if (!id) continue;
|
|
4066
|
+
m.set(id, entry2.attributes);
|
|
4067
|
+
}
|
|
4068
|
+
return m;
|
|
4069
|
+
}
|
|
4070
|
+
function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4071
|
+
const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
|
|
4072
|
+
const baseEdges = indexEntries(baseSnapshot.graph?.edges);
|
|
4073
|
+
const liveNodes = /* @__PURE__ */ new Map();
|
|
4074
|
+
liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
|
|
4075
|
+
const liveEdges = /* @__PURE__ */ new Map();
|
|
4076
|
+
liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
|
|
4077
|
+
const result = {
|
|
4078
|
+
base: { exportedAt: baseSnapshot.exportedAt },
|
|
4079
|
+
current: { exportedAt: currentExportedAt },
|
|
4080
|
+
added: { nodes: [], edges: [] },
|
|
4081
|
+
removed: { nodes: [], edges: [] },
|
|
4082
|
+
changed: { nodes: [], edges: [] }
|
|
4083
|
+
};
|
|
4084
|
+
for (const [id, after] of liveNodes) {
|
|
4085
|
+
const before = baseNodes.get(id);
|
|
4086
|
+
if (!before) {
|
|
4087
|
+
result.added.nodes.push(after);
|
|
4088
|
+
} else if (!shallowEqual(before, after)) {
|
|
4089
|
+
result.changed.nodes.push({ id, before, after });
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
for (const [id, before] of baseNodes) {
|
|
4093
|
+
if (!liveNodes.has(id)) result.removed.nodes.push(before);
|
|
4094
|
+
}
|
|
4095
|
+
for (const [id, after] of liveEdges) {
|
|
4096
|
+
const before = baseEdges.get(id);
|
|
4097
|
+
if (!before) {
|
|
4098
|
+
result.added.edges.push(after);
|
|
4099
|
+
} else if (!shallowEqual(before, after)) {
|
|
4100
|
+
result.changed.edges.push({ id, before, after });
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
for (const [id, before] of baseEdges) {
|
|
4104
|
+
if (!liveEdges.has(id)) result.removed.edges.push(before);
|
|
4105
|
+
}
|
|
4106
|
+
return result;
|
|
4107
|
+
}
|
|
4108
|
+
function shallowEqual(a, b) {
|
|
4109
|
+
return canonicalJson(a) === canonicalJson(b);
|
|
4110
|
+
}
|
|
4111
|
+
function canonicalJson(value) {
|
|
4112
|
+
return JSON.stringify(value, (_key, v) => {
|
|
4113
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
4114
|
+
return Object.keys(v).sort().reduce((acc, k) => {
|
|
4115
|
+
acc[k] = v[k];
|
|
4116
|
+
return acc;
|
|
4117
|
+
}, {});
|
|
4118
|
+
}
|
|
4119
|
+
return v;
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// src/registry.ts
|
|
4124
|
+
init_cjs_shims();
|
|
4125
|
+
var import_node_fs18 = require("fs");
|
|
2886
4126
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
2887
4127
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
2888
|
-
var
|
|
4128
|
+
var import_types19 = require("@neat.is/types");
|
|
2889
4129
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
2890
4130
|
var LOCK_RETRY_MS = 50;
|
|
2891
4131
|
function neatHome() {
|
|
@@ -2900,23 +4140,23 @@ function registryLockPath() {
|
|
|
2900
4140
|
return import_node_path30.default.join(neatHome(), "projects.json.lock");
|
|
2901
4141
|
}
|
|
2902
4142
|
async function writeAtomically(target, contents) {
|
|
2903
|
-
await
|
|
4143
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
|
|
2904
4144
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
2905
|
-
const fd = await
|
|
4145
|
+
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
2906
4146
|
try {
|
|
2907
4147
|
await fd.writeFile(contents, "utf8");
|
|
2908
4148
|
await fd.sync();
|
|
2909
4149
|
} finally {
|
|
2910
4150
|
await fd.close();
|
|
2911
4151
|
}
|
|
2912
|
-
await
|
|
4152
|
+
await import_node_fs18.promises.rename(tmp, target);
|
|
2913
4153
|
}
|
|
2914
4154
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
2915
4155
|
const deadline = Date.now() + timeoutMs;
|
|
2916
|
-
await
|
|
4156
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
|
|
2917
4157
|
while (true) {
|
|
2918
4158
|
try {
|
|
2919
|
-
const fd = await
|
|
4159
|
+
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
2920
4160
|
await fd.close();
|
|
2921
4161
|
return;
|
|
2922
4162
|
} catch (err) {
|
|
@@ -2932,7 +4172,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
2932
4172
|
}
|
|
2933
4173
|
}
|
|
2934
4174
|
async function releaseLock(lockPath) {
|
|
2935
|
-
await
|
|
4175
|
+
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
2936
4176
|
});
|
|
2937
4177
|
}
|
|
2938
4178
|
async function withLock(fn) {
|
|
@@ -2948,7 +4188,7 @@ async function readRegistry() {
|
|
|
2948
4188
|
const file = registryPath();
|
|
2949
4189
|
let raw;
|
|
2950
4190
|
try {
|
|
2951
|
-
raw = await
|
|
4191
|
+
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
2952
4192
|
} catch (err) {
|
|
2953
4193
|
if (err.code === "ENOENT") {
|
|
2954
4194
|
return { version: 1, projects: [] };
|
|
@@ -2956,22 +4196,26 @@ async function readRegistry() {
|
|
|
2956
4196
|
throw err;
|
|
2957
4197
|
}
|
|
2958
4198
|
const parsed = JSON.parse(raw);
|
|
2959
|
-
return
|
|
4199
|
+
return import_types19.RegistryFileSchema.parse(parsed);
|
|
2960
4200
|
}
|
|
2961
4201
|
async function writeRegistry(reg) {
|
|
2962
|
-
const validated =
|
|
4202
|
+
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
2963
4203
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
2964
4204
|
}
|
|
4205
|
+
async function getProject(name) {
|
|
4206
|
+
const reg = await readRegistry();
|
|
4207
|
+
return reg.projects.find((p) => p.name === name);
|
|
4208
|
+
}
|
|
2965
4209
|
async function listProjects() {
|
|
2966
4210
|
const reg = await readRegistry();
|
|
2967
4211
|
return reg.projects;
|
|
2968
4212
|
}
|
|
2969
|
-
async function setStatus(name,
|
|
4213
|
+
async function setStatus(name, status2) {
|
|
2970
4214
|
return withLock(async () => {
|
|
2971
4215
|
const reg = await readRegistry();
|
|
2972
4216
|
const entry2 = reg.projects.find((p) => p.name === name);
|
|
2973
4217
|
if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
|
|
2974
|
-
entry2.status =
|
|
4218
|
+
entry2.status = status2;
|
|
2975
4219
|
await writeRegistry(reg);
|
|
2976
4220
|
return entry2;
|
|
2977
4221
|
});
|
|
@@ -2986,17 +4230,511 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
|
|
|
2986
4230
|
});
|
|
2987
4231
|
}
|
|
2988
4232
|
|
|
4233
|
+
// src/streaming.ts
|
|
4234
|
+
init_cjs_shims();
|
|
4235
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
4236
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
4237
|
+
function handleSse(req2, reply, opts) {
|
|
4238
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
4239
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
4240
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
4241
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
4242
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
4243
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
4244
|
+
reply.raw.flushHeaders?.();
|
|
4245
|
+
let pending = 0;
|
|
4246
|
+
let dropped = false;
|
|
4247
|
+
const closeConnection = () => {
|
|
4248
|
+
if (dropped) return;
|
|
4249
|
+
dropped = true;
|
|
4250
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
4251
|
+
clearInterval(heartbeat);
|
|
4252
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
4253
|
+
};
|
|
4254
|
+
const writeFrame = (frame) => {
|
|
4255
|
+
if (dropped) return;
|
|
4256
|
+
if (pending >= backpressureCap) {
|
|
4257
|
+
const errFrame = `event: error
|
|
4258
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
4259
|
+
|
|
4260
|
+
`;
|
|
4261
|
+
reply.raw.write(errFrame);
|
|
4262
|
+
closeConnection();
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
pending++;
|
|
4266
|
+
reply.raw.write(frame, () => {
|
|
4267
|
+
pending = Math.max(0, pending - 1);
|
|
4268
|
+
});
|
|
4269
|
+
};
|
|
4270
|
+
const listener = (envelope) => {
|
|
4271
|
+
if (envelope.project !== opts.project) return;
|
|
4272
|
+
writeFrame(`event: ${envelope.type}
|
|
4273
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
4274
|
+
|
|
4275
|
+
`);
|
|
4276
|
+
};
|
|
4277
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
4278
|
+
const heartbeat = setInterval(() => {
|
|
4279
|
+
if (dropped) return;
|
|
4280
|
+
reply.raw.write(":heartbeat\n\n");
|
|
4281
|
+
}, heartbeatMs);
|
|
4282
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
4283
|
+
req2.raw.on("close", closeConnection);
|
|
4284
|
+
reply.raw.on("close", closeConnection);
|
|
4285
|
+
reply.raw.on("error", closeConnection);
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// src/api.ts
|
|
4289
|
+
function serializeGraph(graph) {
|
|
4290
|
+
const nodes = [];
|
|
4291
|
+
graph.forEachNode((_id, attrs) => {
|
|
4292
|
+
nodes.push(attrs);
|
|
4293
|
+
});
|
|
4294
|
+
const edges = [];
|
|
4295
|
+
graph.forEachEdge((_id, attrs) => {
|
|
4296
|
+
edges.push(attrs);
|
|
4297
|
+
});
|
|
4298
|
+
return { nodes, edges };
|
|
4299
|
+
}
|
|
4300
|
+
function projectFromReq(req2) {
|
|
4301
|
+
const params = req2.params;
|
|
4302
|
+
return params.project ?? DEFAULT_PROJECT;
|
|
4303
|
+
}
|
|
4304
|
+
function resolveProject(registry, req2, reply) {
|
|
4305
|
+
const name = projectFromReq(req2);
|
|
4306
|
+
const ctx = registry.get(name);
|
|
4307
|
+
if (!ctx) {
|
|
4308
|
+
void reply.code(404).send({ error: "project not found", project: name });
|
|
4309
|
+
return null;
|
|
4310
|
+
}
|
|
4311
|
+
return ctx;
|
|
4312
|
+
}
|
|
4313
|
+
function buildLegacyRegistry(opts) {
|
|
4314
|
+
if (opts.projects) return opts.projects;
|
|
4315
|
+
if (!opts.graph) {
|
|
4316
|
+
throw new Error("buildApi: either `projects` or `graph` must be provided");
|
|
4317
|
+
}
|
|
4318
|
+
const registry = new Projects();
|
|
4319
|
+
const paths = pathsForProject(DEFAULT_PROJECT, "");
|
|
4320
|
+
registry.set(DEFAULT_PROJECT, {
|
|
4321
|
+
graph: opts.graph,
|
|
4322
|
+
scanPath: opts.scanPath,
|
|
4323
|
+
paths: {
|
|
4324
|
+
snapshotPath: paths.snapshotPath,
|
|
4325
|
+
errorsPath: opts.errorsPath ?? paths.errorsPath,
|
|
4326
|
+
staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
|
|
4327
|
+
embeddingsCachePath: paths.embeddingsCachePath,
|
|
4328
|
+
policyViolationsPath: paths.policyViolationsPath
|
|
4329
|
+
},
|
|
4330
|
+
searchIndex: opts.searchIndex
|
|
4331
|
+
});
|
|
4332
|
+
return registry;
|
|
4333
|
+
}
|
|
4334
|
+
function registerRoutes(scope, ctx) {
|
|
4335
|
+
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4336
|
+
scope.get("/events", (req2, reply) => {
|
|
4337
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4338
|
+
if (!proj) return;
|
|
4339
|
+
handleSse(req2, reply, { project: proj.name });
|
|
4340
|
+
});
|
|
4341
|
+
scope.get("/health", async (req2, reply) => {
|
|
4342
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4343
|
+
if (!proj) return;
|
|
4344
|
+
const uptimeMs = Date.now() - startedAt;
|
|
4345
|
+
return {
|
|
4346
|
+
ok: true,
|
|
4347
|
+
project: proj.name,
|
|
4348
|
+
uptimeMs,
|
|
4349
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
4350
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
4351
|
+
// the canonical triple and lets the extras pass through.
|
|
4352
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
4353
|
+
nodeCount: proj.graph.order,
|
|
4354
|
+
edgeCount: proj.graph.size,
|
|
4355
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4356
|
+
};
|
|
4357
|
+
});
|
|
4358
|
+
scope.get("/graph", async (req2, reply) => {
|
|
4359
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4360
|
+
if (!proj) return;
|
|
4361
|
+
return serializeGraph(proj.graph);
|
|
4362
|
+
});
|
|
4363
|
+
scope.get(
|
|
4364
|
+
"/graph/node/:id",
|
|
4365
|
+
async (req2, reply) => {
|
|
4366
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4367
|
+
if (!proj) return;
|
|
4368
|
+
const { id } = req2.params;
|
|
4369
|
+
if (!proj.graph.hasNode(id)) {
|
|
4370
|
+
return reply.code(404).send({ error: "node not found", id });
|
|
4371
|
+
}
|
|
4372
|
+
return { node: proj.graph.getNodeAttributes(id) };
|
|
4373
|
+
}
|
|
4374
|
+
);
|
|
4375
|
+
scope.get(
|
|
4376
|
+
"/graph/edges/:id",
|
|
4377
|
+
async (req2, reply) => {
|
|
4378
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4379
|
+
if (!proj) return;
|
|
4380
|
+
const { id } = req2.params;
|
|
4381
|
+
if (!proj.graph.hasNode(id)) {
|
|
4382
|
+
return reply.code(404).send({ error: "node not found", id });
|
|
4383
|
+
}
|
|
4384
|
+
const inbound = proj.graph.inboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
|
|
4385
|
+
const outbound = proj.graph.outboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
|
|
4386
|
+
return { inbound, outbound };
|
|
4387
|
+
}
|
|
4388
|
+
);
|
|
4389
|
+
scope.get("/graph/dependencies/:nodeId", async (req2, reply) => {
|
|
4390
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4391
|
+
if (!proj) return;
|
|
4392
|
+
const { nodeId } = req2.params;
|
|
4393
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4394
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4395
|
+
}
|
|
4396
|
+
const depth = req2.query.depth ? Number(req2.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
|
|
4397
|
+
if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
|
|
4398
|
+
return reply.code(400).send({
|
|
4399
|
+
error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
4403
|
+
});
|
|
4404
|
+
scope.get("/graph/divergences", async (req2, reply) => {
|
|
4405
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4406
|
+
if (!proj) return;
|
|
4407
|
+
let typeFilter;
|
|
4408
|
+
if (req2.query.type) {
|
|
4409
|
+
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4410
|
+
const parsed = [];
|
|
4411
|
+
for (const c of candidates) {
|
|
4412
|
+
const r = import_types20.DivergenceTypeSchema.safeParse(c);
|
|
4413
|
+
if (!r.success) {
|
|
4414
|
+
return reply.code(400).send({
|
|
4415
|
+
error: `unknown divergence type "${c}"`,
|
|
4416
|
+
allowed: import_types20.DivergenceTypeSchema.options
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
parsed.push(r.data);
|
|
4420
|
+
}
|
|
4421
|
+
typeFilter = new Set(parsed);
|
|
4422
|
+
}
|
|
4423
|
+
let minConfidence;
|
|
4424
|
+
if (req2.query.minConfidence !== void 0) {
|
|
4425
|
+
const n = Number(req2.query.minConfidence);
|
|
4426
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4427
|
+
return reply.code(400).send({
|
|
4428
|
+
error: "minConfidence must be a number in [0, 1]"
|
|
4429
|
+
});
|
|
4430
|
+
}
|
|
4431
|
+
minConfidence = n;
|
|
4432
|
+
}
|
|
4433
|
+
return computeDivergences(proj.graph, {
|
|
4434
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
4435
|
+
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
4436
|
+
...req2.query.node ? { node: req2.query.node } : {}
|
|
4437
|
+
});
|
|
4438
|
+
});
|
|
4439
|
+
scope.get("/incidents", async (req2, reply) => {
|
|
4440
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4441
|
+
if (!proj) return;
|
|
4442
|
+
const epath = errorsPathFor(proj);
|
|
4443
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4444
|
+
const events = await readErrorEvents(epath);
|
|
4445
|
+
const total = events.length;
|
|
4446
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : 50;
|
|
4447
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
4448
|
+
const sliced = events.slice(0, safeLimit);
|
|
4449
|
+
return { count: sliced.length, total, events: sliced };
|
|
4450
|
+
});
|
|
4451
|
+
scope.get("/stale-events", async (req2, reply) => {
|
|
4452
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4453
|
+
if (!proj) return;
|
|
4454
|
+
const spath = staleEventsPathFor(proj);
|
|
4455
|
+
if (!spath) return { count: 0, total: 0, events: [] };
|
|
4456
|
+
const events = await readStaleEvents(spath);
|
|
4457
|
+
const filtered = req2.query.edgeType ? events.filter((e) => e.edgeType === req2.query.edgeType) : events;
|
|
4458
|
+
const ordered = [...filtered].reverse();
|
|
4459
|
+
const total = ordered.length;
|
|
4460
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : 50;
|
|
4461
|
+
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
4462
|
+
return { count: sliced.length, total, events: sliced };
|
|
4463
|
+
});
|
|
4464
|
+
scope.get(
|
|
4465
|
+
"/incidents/:nodeId",
|
|
4466
|
+
async (req2, reply) => {
|
|
4467
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4468
|
+
if (!proj) return;
|
|
4469
|
+
const { nodeId } = req2.params;
|
|
4470
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4471
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4472
|
+
}
|
|
4473
|
+
const epath = errorsPathFor(proj);
|
|
4474
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4475
|
+
const events = await readErrorEvents(epath);
|
|
4476
|
+
const filtered = events.filter(
|
|
4477
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
4478
|
+
);
|
|
4479
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
4480
|
+
}
|
|
4481
|
+
);
|
|
4482
|
+
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
4483
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4484
|
+
if (!proj) return;
|
|
4485
|
+
const { nodeId } = req2.params;
|
|
4486
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4487
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4488
|
+
}
|
|
4489
|
+
let errorEvent;
|
|
4490
|
+
const epath = errorsPathFor(proj);
|
|
4491
|
+
if (req2.query.errorId && epath) {
|
|
4492
|
+
const events = await readErrorEvents(epath);
|
|
4493
|
+
errorEvent = events.find((e) => e.id === req2.query.errorId);
|
|
4494
|
+
if (!errorEvent) {
|
|
4495
|
+
return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
4499
|
+
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
4500
|
+
return result;
|
|
4501
|
+
});
|
|
4502
|
+
scope.get("/graph/blast-radius/:nodeId", async (req2, reply) => {
|
|
4503
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4504
|
+
if (!proj) return;
|
|
4505
|
+
const { nodeId } = req2.params;
|
|
4506
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4507
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4508
|
+
}
|
|
4509
|
+
const depth = req2.query.depth ? Number(req2.query.depth) : void 0;
|
|
4510
|
+
if (depth !== void 0 && (!Number.isFinite(depth) || depth < 0)) {
|
|
4511
|
+
return reply.code(400).send({ error: "depth must be a non-negative number" });
|
|
4512
|
+
}
|
|
4513
|
+
return getBlastRadius(proj.graph, nodeId, depth);
|
|
4514
|
+
});
|
|
4515
|
+
scope.get("/search", async (req2, reply) => {
|
|
4516
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4517
|
+
if (!proj) return;
|
|
4518
|
+
const raw = (req2.query.q ?? "").trim();
|
|
4519
|
+
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
4520
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : void 0;
|
|
4521
|
+
const safeLimit = limit !== void 0 && Number.isFinite(limit) && limit > 0 ? limit : void 0;
|
|
4522
|
+
if (proj.searchIndex) {
|
|
4523
|
+
const result = await proj.searchIndex.search(raw, safeLimit);
|
|
4524
|
+
return {
|
|
4525
|
+
query: result.query,
|
|
4526
|
+
provider: result.provider,
|
|
4527
|
+
matches: result.matches.map((m) => ({ ...m.node, score: m.score }))
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
const q = raw.toLowerCase();
|
|
4531
|
+
const matches = [];
|
|
4532
|
+
proj.graph.forEachNode((id, attrs) => {
|
|
4533
|
+
const name = attrs.name ?? "";
|
|
4534
|
+
if (id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {
|
|
4535
|
+
matches.push({ ...attrs, score: 1 });
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
return {
|
|
4539
|
+
query: q,
|
|
4540
|
+
provider: "substring",
|
|
4541
|
+
matches: matches.slice(0, safeLimit)
|
|
4542
|
+
};
|
|
4543
|
+
});
|
|
4544
|
+
scope.get(
|
|
4545
|
+
"/graph/diff",
|
|
4546
|
+
async (req2, reply) => {
|
|
4547
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4548
|
+
if (!proj) return;
|
|
4549
|
+
const against = req2.query.against;
|
|
4550
|
+
if (!against) {
|
|
4551
|
+
return reply.code(400).send({ error: "query parameter `against` is required" });
|
|
4552
|
+
}
|
|
4553
|
+
try {
|
|
4554
|
+
const snapshot = await loadSnapshotForDiff(against);
|
|
4555
|
+
return computeGraphDiff(proj.graph, snapshot);
|
|
4556
|
+
} catch (err) {
|
|
4557
|
+
return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
);
|
|
4561
|
+
scope.post("/graph/scan", async (req2, reply) => {
|
|
4562
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4563
|
+
if (!proj) return;
|
|
4564
|
+
if (!proj.scanPath) {
|
|
4565
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
4566
|
+
}
|
|
4567
|
+
const result = await extractFromDirectory(proj.graph, proj.scanPath);
|
|
4568
|
+
return {
|
|
4569
|
+
project: proj.name,
|
|
4570
|
+
scanned: proj.scanPath,
|
|
4571
|
+
nodesAdded: result.nodesAdded,
|
|
4572
|
+
edgesAdded: result.edgesAdded,
|
|
4573
|
+
nodeCount: proj.graph.order,
|
|
4574
|
+
edgeCount: proj.graph.size
|
|
4575
|
+
};
|
|
4576
|
+
});
|
|
4577
|
+
scope.get("/policies", async (req2, reply) => {
|
|
4578
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4579
|
+
if (!proj) return;
|
|
4580
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
4581
|
+
if (!policyPath) {
|
|
4582
|
+
return { version: 1, policies: [] };
|
|
4583
|
+
}
|
|
4584
|
+
try {
|
|
4585
|
+
const policies = await loadPolicyFile(policyPath);
|
|
4586
|
+
return { version: 1, policies };
|
|
4587
|
+
} catch (err) {
|
|
4588
|
+
return reply.code(400).send({
|
|
4589
|
+
error: "policy.json failed to parse",
|
|
4590
|
+
details: err.message
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
});
|
|
4594
|
+
scope.get("/policies/violations", async (req2, reply) => {
|
|
4595
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4596
|
+
if (!proj) return;
|
|
4597
|
+
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4598
|
+
let violations = await log.readAll();
|
|
4599
|
+
if (req2.query.severity) {
|
|
4600
|
+
const sev = import_types20.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
4601
|
+
if (!sev.success) {
|
|
4602
|
+
return reply.code(400).send({
|
|
4603
|
+
error: "invalid severity",
|
|
4604
|
+
details: sev.error.format()
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
violations = violations.filter((v) => v.severity === sev.data);
|
|
4608
|
+
}
|
|
4609
|
+
if (req2.query.policyId) {
|
|
4610
|
+
violations = violations.filter((v) => v.policyId === req2.query.policyId);
|
|
4611
|
+
}
|
|
4612
|
+
return { violations };
|
|
4613
|
+
});
|
|
4614
|
+
scope.post("/policies/check", async (req2, reply) => {
|
|
4615
|
+
const proj = resolveProject(registry, req2, reply);
|
|
4616
|
+
if (!proj) return;
|
|
4617
|
+
const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
4618
|
+
if (!parsed.success) {
|
|
4619
|
+
return reply.code(400).send({
|
|
4620
|
+
error: "invalid /policies/check body",
|
|
4621
|
+
details: parsed.error.format()
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
4625
|
+
let policies = [];
|
|
4626
|
+
if (policyPath) {
|
|
4627
|
+
try {
|
|
4628
|
+
policies = await loadPolicyFile(policyPath);
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
return reply.code(400).send({
|
|
4631
|
+
error: "policy.json failed to parse",
|
|
4632
|
+
details: err.message
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
const evalCtx = { now: () => Date.now() };
|
|
4637
|
+
if (!parsed.data.hypotheticalAction) {
|
|
4638
|
+
const violations2 = evaluateAllPolicies(proj.graph, policies, evalCtx);
|
|
4639
|
+
const blocking2 = violations2.filter((v) => v.onViolation === "block");
|
|
4640
|
+
return { allowed: blocking2.length === 0, violations: violations2 };
|
|
4641
|
+
}
|
|
4642
|
+
const violations = evaluateAllPolicies(proj.graph, policies, evalCtx);
|
|
4643
|
+
const blocking = violations.filter((v) => v.onViolation === "block");
|
|
4644
|
+
return {
|
|
4645
|
+
allowed: blocking.length === 0,
|
|
4646
|
+
hypotheticalAction: parsed.data.hypotheticalAction,
|
|
4647
|
+
violations
|
|
4648
|
+
};
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
async function buildApi(opts) {
|
|
4652
|
+
const app = (0, import_fastify.default)({ logger: false });
|
|
4653
|
+
await app.register(import_cors.default, { origin: true });
|
|
4654
|
+
const startedAt = opts.startedAt ?? Date.now();
|
|
4655
|
+
const registry = buildLegacyRegistry(opts);
|
|
4656
|
+
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
4657
|
+
const legacyStaleExplicit = !opts.projects && opts.staleEventsPath !== void 0;
|
|
4658
|
+
const errorsPathFor = (proj) => {
|
|
4659
|
+
if (proj.name === DEFAULT_PROJECT && !opts.projects) {
|
|
4660
|
+
return legacyErrorsExplicit ? opts.errorsPath : void 0;
|
|
4661
|
+
}
|
|
4662
|
+
return proj.paths.errorsPath;
|
|
4663
|
+
};
|
|
4664
|
+
const staleEventsPathFor = (proj) => {
|
|
4665
|
+
if (proj.name === DEFAULT_PROJECT && !opts.projects) {
|
|
4666
|
+
return legacyStaleExplicit ? opts.staleEventsPath : void 0;
|
|
4667
|
+
}
|
|
4668
|
+
return proj.paths.staleEventsPath;
|
|
4669
|
+
};
|
|
4670
|
+
const policyFilePathFor = (proj) => {
|
|
4671
|
+
if (!proj.scanPath) return void 0;
|
|
4672
|
+
return `${proj.scanPath}/policy.json`;
|
|
4673
|
+
};
|
|
4674
|
+
const routeCtx = {
|
|
4675
|
+
registry,
|
|
4676
|
+
startedAt,
|
|
4677
|
+
errorsPathFor,
|
|
4678
|
+
staleEventsPathFor,
|
|
4679
|
+
policyFilePathFor
|
|
4680
|
+
};
|
|
4681
|
+
app.get("/projects", async (_req, reply) => {
|
|
4682
|
+
try {
|
|
4683
|
+
return await listProjects();
|
|
4684
|
+
} catch (err) {
|
|
4685
|
+
return reply.code(500).send({
|
|
4686
|
+
error: "failed to read project registry",
|
|
4687
|
+
details: err.message
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
});
|
|
4691
|
+
app.get("/projects/:project", async (req2, reply) => {
|
|
4692
|
+
try {
|
|
4693
|
+
const entry2 = await getProject(req2.params.project);
|
|
4694
|
+
if (!entry2) {
|
|
4695
|
+
return reply.code(404).send({ error: "project not found", project: req2.params.project });
|
|
4696
|
+
}
|
|
4697
|
+
return { project: entry2 };
|
|
4698
|
+
} catch (err) {
|
|
4699
|
+
return reply.code(500).send({
|
|
4700
|
+
error: "failed to read project registry",
|
|
4701
|
+
details: err.message
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
registerRoutes(app, routeCtx);
|
|
4706
|
+
await app.register(
|
|
4707
|
+
async (scope) => {
|
|
4708
|
+
registerRoutes(scope, routeCtx);
|
|
4709
|
+
},
|
|
4710
|
+
{ prefix: "/projects/:project" }
|
|
4711
|
+
);
|
|
4712
|
+
return app;
|
|
4713
|
+
}
|
|
4714
|
+
|
|
2989
4715
|
// src/daemon.ts
|
|
4716
|
+
init_otel();
|
|
2990
4717
|
function neatHomeFor(opts) {
|
|
2991
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
4718
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
|
|
2992
4719
|
const env = process.env.NEAT_HOME;
|
|
2993
|
-
if (env && env.length > 0) return
|
|
4720
|
+
if (env && env.length > 0) return import_node_path33.default.resolve(env);
|
|
2994
4721
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2995
|
-
return
|
|
4722
|
+
return import_node_path33.default.join(home, ".neat");
|
|
4723
|
+
}
|
|
4724
|
+
function routeSpanToProject(serviceName, projects) {
|
|
4725
|
+
if (!serviceName) return DEFAULT_PROJECT;
|
|
4726
|
+
for (const entry2 of projects) {
|
|
4727
|
+
if (entry2.status !== "active") continue;
|
|
4728
|
+
if (entry2.languages.length === 0) {
|
|
4729
|
+
}
|
|
4730
|
+
if (entry2.name === serviceName) return entry2.name;
|
|
4731
|
+
}
|
|
4732
|
+
return DEFAULT_PROJECT;
|
|
2996
4733
|
}
|
|
2997
4734
|
async function bootstrapProject(entry2) {
|
|
4735
|
+
const paths = pathsForProject(entry2.name, import_node_path33.default.join(entry2.path, "neat-out"));
|
|
2998
4736
|
try {
|
|
2999
|
-
const stat = await
|
|
4737
|
+
const stat = await import_node_fs19.promises.stat(entry2.path);
|
|
3000
4738
|
if (!stat.isDirectory()) {
|
|
3001
4739
|
throw new Error(`registered path ${entry2.path} is not a directory`);
|
|
3002
4740
|
}
|
|
@@ -3009,6 +4747,7 @@ async function bootstrapProject(entry2) {
|
|
|
3009
4747
|
// output; nothing routes to it because it's not 'active'.
|
|
3010
4748
|
graph: getGraph(`__broken__:${entry2.name}`),
|
|
3011
4749
|
outPath: "",
|
|
4750
|
+
paths,
|
|
3012
4751
|
stopPersist: () => {
|
|
3013
4752
|
},
|
|
3014
4753
|
status: "broken",
|
|
@@ -3017,10 +4756,7 @@ async function bootstrapProject(entry2) {
|
|
|
3017
4756
|
}
|
|
3018
4757
|
resetGraph(entry2.name);
|
|
3019
4758
|
const graph = getGraph(entry2.name);
|
|
3020
|
-
const outPath =
|
|
3021
|
-
entry2.name,
|
|
3022
|
-
import_node_path31.default.join(entry2.path, "neat-out")
|
|
3023
|
-
).snapshotPath;
|
|
4759
|
+
const outPath = paths.snapshotPath;
|
|
3024
4760
|
await loadGraphFromDisk(graph, outPath);
|
|
3025
4761
|
await extractFromDirectory(graph, entry2.path);
|
|
3026
4762
|
const stopPersist = startPersistLoop(graph, outPath);
|
|
@@ -3030,24 +4766,58 @@ async function bootstrapProject(entry2) {
|
|
|
3030
4766
|
entry: entry2,
|
|
3031
4767
|
graph,
|
|
3032
4768
|
outPath,
|
|
4769
|
+
paths,
|
|
3033
4770
|
stopPersist,
|
|
3034
4771
|
status: "active"
|
|
3035
4772
|
};
|
|
3036
4773
|
}
|
|
4774
|
+
function resolveRestPort(opts) {
|
|
4775
|
+
if (typeof opts.restPort === "number") return opts.restPort;
|
|
4776
|
+
const env = process.env.PORT;
|
|
4777
|
+
if (env && env.length > 0) {
|
|
4778
|
+
const n = Number.parseInt(env, 10);
|
|
4779
|
+
if (Number.isFinite(n)) return n;
|
|
4780
|
+
}
|
|
4781
|
+
return 8080;
|
|
4782
|
+
}
|
|
4783
|
+
function resolveOtlpPort(opts) {
|
|
4784
|
+
if (typeof opts.otlpPort === "number") return opts.otlpPort;
|
|
4785
|
+
const env = process.env.OTEL_PORT;
|
|
4786
|
+
if (env && env.length > 0) {
|
|
4787
|
+
const n = Number.parseInt(env, 10);
|
|
4788
|
+
if (Number.isFinite(n)) return n;
|
|
4789
|
+
}
|
|
4790
|
+
return 4318;
|
|
4791
|
+
}
|
|
4792
|
+
function resolveHost(opts) {
|
|
4793
|
+
if (opts.host && opts.host.length > 0) return opts.host;
|
|
4794
|
+
const env = process.env.HOST;
|
|
4795
|
+
if (env && env.length > 0) return env;
|
|
4796
|
+
return "0.0.0.0";
|
|
4797
|
+
}
|
|
3037
4798
|
async function startDaemon(opts = {}) {
|
|
3038
4799
|
const home = neatHomeFor(opts);
|
|
3039
4800
|
const regPath = registryPath();
|
|
3040
4801
|
try {
|
|
3041
|
-
await
|
|
4802
|
+
await import_node_fs19.promises.access(regPath);
|
|
3042
4803
|
} catch {
|
|
3043
4804
|
throw new Error(
|
|
3044
4805
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
3045
4806
|
);
|
|
3046
4807
|
}
|
|
3047
|
-
const pidPath =
|
|
4808
|
+
const pidPath = import_node_path33.default.join(home, "neatd.pid");
|
|
3048
4809
|
await writeAtomically(pidPath, `${process.pid}
|
|
3049
4810
|
`);
|
|
3050
4811
|
const slots = /* @__PURE__ */ new Map();
|
|
4812
|
+
const registry = new Projects();
|
|
4813
|
+
function upsertRegistryFromSlot(slot) {
|
|
4814
|
+
if (slot.status !== "active") return;
|
|
4815
|
+
registry.set(slot.entry.name, {
|
|
4816
|
+
scanPath: slot.entry.path,
|
|
4817
|
+
paths: slot.paths,
|
|
4818
|
+
graph: slot.graph
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
3051
4821
|
async function loadAll() {
|
|
3052
4822
|
const projects = await listProjects();
|
|
3053
4823
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3057,6 +4827,7 @@ async function startDaemon(opts = {}) {
|
|
|
3057
4827
|
try {
|
|
3058
4828
|
const slot = await bootstrapProject(entry2);
|
|
3059
4829
|
slots.set(entry2.name, slot);
|
|
4830
|
+
upsertRegistryFromSlot(slot);
|
|
3060
4831
|
if (slot.status === "broken") {
|
|
3061
4832
|
console.warn(`neatd: project "${entry2.name}" broken \u2014 ${slot.errorReason}`);
|
|
3062
4833
|
} else {
|
|
@@ -3081,6 +4852,80 @@ async function startDaemon(opts = {}) {
|
|
|
3081
4852
|
}
|
|
3082
4853
|
}
|
|
3083
4854
|
await loadAll();
|
|
4855
|
+
const bind = opts.bindListeners !== false;
|
|
4856
|
+
let restApp = null;
|
|
4857
|
+
let otlpApp = null;
|
|
4858
|
+
let restAddress = "";
|
|
4859
|
+
let otlpAddress = "";
|
|
4860
|
+
if (bind) {
|
|
4861
|
+
const host = resolveHost(opts);
|
|
4862
|
+
const restPort = resolveRestPort(opts);
|
|
4863
|
+
const otlpPort = resolveOtlpPort(opts);
|
|
4864
|
+
try {
|
|
4865
|
+
restApp = await buildApi({ projects: registry });
|
|
4866
|
+
restAddress = await restApp.listen({ port: restPort, host });
|
|
4867
|
+
console.log(`neatd: REST listening on ${restAddress}`);
|
|
4868
|
+
} catch (err) {
|
|
4869
|
+
for (const slot of slots.values()) {
|
|
4870
|
+
try {
|
|
4871
|
+
slot.stopPersist();
|
|
4872
|
+
} catch {
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
if (restApp) await restApp.close().catch(() => {
|
|
4876
|
+
});
|
|
4877
|
+
await import_node_fs19.promises.unlink(pidPath).catch(() => {
|
|
4878
|
+
});
|
|
4879
|
+
throw new Error(
|
|
4880
|
+
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
4881
|
+
);
|
|
4882
|
+
}
|
|
4883
|
+
try {
|
|
4884
|
+
otlpApp = await buildOtelReceiver({
|
|
4885
|
+
onSpan: async (span) => {
|
|
4886
|
+
const liveEntries = await listProjects().catch(() => []);
|
|
4887
|
+
const target = routeSpanToProject(span.service, liveEntries);
|
|
4888
|
+
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
4889
|
+
if (!slot || slot.status !== "active") return;
|
|
4890
|
+
await handleSpan(
|
|
4891
|
+
{
|
|
4892
|
+
graph: slot.graph,
|
|
4893
|
+
errorsPath: slot.paths.errorsPath,
|
|
4894
|
+
project: slot.entry.name,
|
|
4895
|
+
// Receiver already wrote the error event synchronously below.
|
|
4896
|
+
writeErrorEventInline: false
|
|
4897
|
+
},
|
|
4898
|
+
span
|
|
4899
|
+
);
|
|
4900
|
+
},
|
|
4901
|
+
onErrorSpanSync: async (span) => {
|
|
4902
|
+
const liveEntries = await listProjects().catch(() => []);
|
|
4903
|
+
const target = routeSpanToProject(span.service, liveEntries);
|
|
4904
|
+
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
4905
|
+
if (!slot || slot.status !== "active") return;
|
|
4906
|
+
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
4907
|
+
}
|
|
4908
|
+
});
|
|
4909
|
+
otlpAddress = await otlpApp.listen({ port: otlpPort, host });
|
|
4910
|
+
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
4911
|
+
} catch (err) {
|
|
4912
|
+
for (const slot of slots.values()) {
|
|
4913
|
+
try {
|
|
4914
|
+
slot.stopPersist();
|
|
4915
|
+
} catch {
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
if (restApp) await restApp.close().catch(() => {
|
|
4919
|
+
});
|
|
4920
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
4921
|
+
});
|
|
4922
|
+
await import_node_fs19.promises.unlink(pidPath).catch(() => {
|
|
4923
|
+
});
|
|
4924
|
+
throw new Error(
|
|
4925
|
+
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
4926
|
+
);
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
3084
4929
|
let reloading = null;
|
|
3085
4930
|
const reload = async () => {
|
|
3086
4931
|
if (reloading) return reloading;
|
|
@@ -3104,22 +4949,27 @@ async function startDaemon(opts = {}) {
|
|
|
3104
4949
|
if (stopped) return;
|
|
3105
4950
|
stopped = true;
|
|
3106
4951
|
process.off("SIGHUP", sighupHandler);
|
|
4952
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
4953
|
+
});
|
|
4954
|
+
if (restApp) await restApp.close().catch(() => {
|
|
4955
|
+
});
|
|
3107
4956
|
for (const slot of slots.values()) {
|
|
3108
4957
|
try {
|
|
3109
4958
|
slot.stopPersist();
|
|
3110
4959
|
} catch {
|
|
3111
4960
|
}
|
|
3112
4961
|
}
|
|
3113
|
-
await
|
|
4962
|
+
await import_node_fs19.promises.unlink(pidPath).catch(() => {
|
|
3114
4963
|
});
|
|
3115
4964
|
};
|
|
3116
|
-
return { slots, reload, stop, pidPath };
|
|
4965
|
+
return { slots, reload, stop, pidPath, restAddress, otlpAddress };
|
|
3117
4966
|
}
|
|
3118
4967
|
|
|
3119
4968
|
// src/web-spawn.ts
|
|
4969
|
+
init_cjs_shims();
|
|
3120
4970
|
var import_node_child_process = require("child_process");
|
|
3121
4971
|
var import_node_net = __toESM(require("net"), 1);
|
|
3122
|
-
var
|
|
4972
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
3123
4973
|
var DEFAULT_WEB_PORT = 6328;
|
|
3124
4974
|
async function assertPortFree(port) {
|
|
3125
4975
|
await new Promise((resolve, reject) => {
|
|
@@ -3147,7 +4997,10 @@ function resolveWebPackageDir() {
|
|
|
3147
4997
|
eval("require")
|
|
3148
4998
|
);
|
|
3149
4999
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
3150
|
-
return
|
|
5000
|
+
return import_node_path34.default.dirname(pkgJsonPath);
|
|
5001
|
+
}
|
|
5002
|
+
function resolveStandaloneServerEntry(webDir) {
|
|
5003
|
+
return import_node_path34.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
3151
5004
|
}
|
|
3152
5005
|
async function spawnWebUI(restPort) {
|
|
3153
5006
|
const portRaw = process.env.NEAT_WEB_PORT;
|
|
@@ -3157,13 +5010,22 @@ async function spawnWebUI(restPort) {
|
|
|
3157
5010
|
}
|
|
3158
5011
|
await assertPortFree(port);
|
|
3159
5012
|
const cwd = resolveWebPackageDir();
|
|
5013
|
+
const serverEntry = resolveStandaloneServerEntry(cwd);
|
|
5014
|
+
try {
|
|
5015
|
+
require.resolve(serverEntry);
|
|
5016
|
+
} catch {
|
|
5017
|
+
throw new Error(
|
|
5018
|
+
`neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
3160
5021
|
const env = {
|
|
3161
5022
|
...process.env,
|
|
3162
5023
|
PORT: String(port),
|
|
5024
|
+
HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
|
|
3163
5025
|
NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
|
|
3164
5026
|
};
|
|
3165
|
-
const child = (0, import_node_child_process.spawn)(
|
|
3166
|
-
cwd,
|
|
5027
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
|
|
5028
|
+
cwd: import_node_path34.default.dirname(serverEntry),
|
|
3167
5029
|
env,
|
|
3168
5030
|
stdio: ["ignore", "inherit", "inherit"],
|
|
3169
5031
|
detached: false
|
|
@@ -3200,14 +5062,14 @@ async function spawnWebUI(restPort) {
|
|
|
3200
5062
|
// src/neatd.ts
|
|
3201
5063
|
function neatHome2() {
|
|
3202
5064
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
3203
|
-
return
|
|
5065
|
+
return import_node_path35.default.resolve(process.env.NEAT_HOME);
|
|
3204
5066
|
}
|
|
3205
5067
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
3206
|
-
return
|
|
5068
|
+
return import_node_path35.default.join(home, ".neat");
|
|
3207
5069
|
}
|
|
3208
5070
|
async function readPid() {
|
|
3209
5071
|
try {
|
|
3210
|
-
const raw = await
|
|
5072
|
+
const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
3211
5073
|
const n = Number.parseInt(raw.trim(), 10);
|
|
3212
5074
|
return Number.isFinite(n) ? n : null;
|
|
3213
5075
|
} catch {
|
|
@@ -3225,6 +5087,8 @@ async function cmdStart() {
|
|
|
3225
5087
|
const handle = await startDaemon();
|
|
3226
5088
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
3227
5089
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
5090
|
+
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|
|
5091
|
+
if (handle.otlpAddress) console.log(`neatd: OTLP \u2192 ${handle.otlpAddress}`);
|
|
3228
5092
|
const skipWeb = process.env.NEAT_WEB_DISABLED === "1";
|
|
3229
5093
|
let web = null;
|
|
3230
5094
|
if (!skipWeb) {
|