@neat.is/core 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -39,6 +39,91 @@ var init_cjs_shims = __esm({
39
39
  }
40
40
  });
41
41
 
42
+ // src/auth.ts
43
+ function isLoopbackHost(host) {
44
+ if (!host) return false;
45
+ return LOOPBACK_HOSTS.has(host);
46
+ }
47
+ function assertBindAuthority(host, token) {
48
+ if (token && token.length > 0) return;
49
+ if (isLoopbackHost(host)) return;
50
+ throw new BindAuthorityError(host);
51
+ }
52
+ function mountBearerAuth(app, opts) {
53
+ if (!opts.token || opts.token.length === 0) return;
54
+ if (opts.trustProxy) return;
55
+ const expected = Buffer.from(opts.token, "utf8");
56
+ const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
57
+ const publicRead = opts.publicRead === true;
58
+ app.addHook("preHandler", (req, reply, done) => {
59
+ const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ for (const suffix of suffixes) {
61
+ if (path38 === suffix || path38.endsWith(suffix)) {
62
+ done();
63
+ return;
64
+ }
65
+ }
66
+ if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
67
+ done();
68
+ return;
69
+ }
70
+ const header = req.headers.authorization;
71
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
72
+ void reply.code(401).send({ error: "unauthorized" });
73
+ return;
74
+ }
75
+ const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
76
+ if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
77
+ void reply.code(401).send({ error: "unauthorized" });
78
+ return;
79
+ }
80
+ done();
81
+ });
82
+ }
83
+ function parseBoolEnv(v) {
84
+ if (!v) return false;
85
+ return v === "true" || v === "1";
86
+ }
87
+ function readAuthEnv(env = process.env) {
88
+ const t = env.NEAT_AUTH_TOKEN;
89
+ const ot = env.NEAT_OTEL_TOKEN;
90
+ return {
91
+ authToken: t && t.length > 0 ? t : void 0,
92
+ otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
93
+ trustProxy: env.NEAT_AUTH_PROXY === "true",
94
+ publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
95
+ };
96
+ }
97
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
98
+ var init_auth = __esm({
99
+ "src/auth.ts"() {
100
+ "use strict";
101
+ init_cjs_shims();
102
+ import_node_crypto = require("crypto");
103
+ LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
104
+ "127.0.0.1",
105
+ "localhost",
106
+ "::1",
107
+ "::ffff:127.0.0.1"
108
+ ]);
109
+ BindAuthorityError = class extends Error {
110
+ constructor(host) {
111
+ super(
112
+ `NEAT refuses to bind on a public interface without \`NEAT_AUTH_TOKEN\` set (host="${host}"). Set the token or bind to loopback only.`
113
+ );
114
+ this.name = "BindAuthorityError";
115
+ }
116
+ };
117
+ PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
118
+ DEFAULT_UNAUTH_SUFFIXES = [
119
+ "/health",
120
+ "/healthz",
121
+ "/readyz",
122
+ "/api/config"
123
+ ];
124
+ }
125
+ });
126
+
42
127
  // src/otel-grpc.ts
43
128
  var otel_grpc_exports = {};
44
129
  __export(otel_grpc_exports, {
@@ -121,8 +206,21 @@ function loadTraceService() {
121
206
  async function startOtelGrpcReceiver(opts) {
122
207
  const server = new grpc.Server();
123
208
  const service = loadTraceService();
209
+ const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
210
+ const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
124
211
  server.addService(service, {
125
212
  Export: (call, callback) => {
213
+ if (requiresAuth) {
214
+ const meta = call.metadata.get("authorization");
215
+ const got = meta.length > 0 ? String(meta[0]) : "";
216
+ const a = Buffer.from(got, "utf8");
217
+ const b = Buffer.from(expectedHeader, "utf8");
218
+ const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
219
+ if (!ok) {
220
+ callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
221
+ return;
222
+ }
223
+ }
126
224
  void (async () => {
127
225
  try {
128
226
  const reshaped = reshapeGrpcRequest(call.request ?? {});
@@ -155,13 +253,14 @@ async function startOtelGrpcReceiver(opts) {
155
253
  })
156
254
  };
157
255
  }
158
- var import_node_url, import_node_path34, grpc, protoLoader;
256
+ var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
159
257
  var init_otel_grpc = __esm({
160
258
  "src/otel-grpc.ts"() {
161
259
  "use strict";
162
260
  init_cjs_shims();
163
261
  import_node_url = require("url");
164
262
  import_node_path34 = __toESM(require("path"), 1);
263
+ import_node_crypto2 = require("crypto");
165
264
  grpc = __toESM(require("@grpc/grpc-js"), 1);
166
265
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
167
266
  init_otel();
@@ -224,6 +323,15 @@ function isoFromUnixNano(nanos) {
224
323
  return void 0;
225
324
  }
226
325
  }
326
+ function pickEnv(spanAttrs, resourceAttrs) {
327
+ for (const attrs of [spanAttrs, resourceAttrs]) {
328
+ for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
329
+ const v = attrs[key];
330
+ if (typeof v === "string" && v.length > 0) return v;
331
+ }
332
+ }
333
+ return ENV_FALLBACK;
334
+ }
227
335
  function parseOtlpRequest(body) {
228
336
  const out = [];
229
337
  for (const rs of body.resourceSpans ?? []) {
@@ -243,6 +351,7 @@ function parseOtlpRequest(body) {
243
351
  endTimeUnixNano: span.endTimeUnixNano ?? "0",
244
352
  startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
245
353
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
354
+ env: pickEnv(attrs, resourceAttrs),
246
355
  attributes: attrs,
247
356
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
248
357
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
@@ -302,6 +411,7 @@ async function buildOtelReceiver(opts) {
302
411
  logger: false,
303
412
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
304
413
  });
414
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
305
415
  const queue = [];
306
416
  let draining = false;
307
417
  let drainPromise = Promise.resolve();
@@ -380,7 +490,7 @@ async function buildOtelReceiver(opts) {
380
490
  };
381
491
  return decorated;
382
492
  }
383
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
493
+ var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
384
494
  var init_otel = __esm({
385
495
  "src/otel.ts"() {
386
496
  "use strict";
@@ -389,6 +499,10 @@ var init_otel = __esm({
389
499
  import_node_url2 = require("url");
390
500
  import_fastify2 = __toESM(require("fastify"), 1);
391
501
  import_protobufjs = __toESM(require("protobufjs"), 1);
502
+ init_auth();
503
+ ENV_ATTR_CANONICAL = "deployment.environment.name";
504
+ ENV_ATTR_COMPAT = "deployment.environment";
505
+ ENV_FALLBACK = "unknown";
392
506
  exportTraceServiceRequestType = null;
393
507
  exportTraceServiceResponseType = null;
394
508
  cachedProtobufResponseBody = null;
@@ -421,7 +535,7 @@ function getGraph(project = DEFAULT_PROJECT) {
421
535
  init_cjs_shims();
422
536
  var import_fastify = __toESM(require("fastify"), 1);
423
537
  var import_cors = __toESM(require("@fastify/cors"), 1);
424
- var import_types21 = require("@neat.is/types");
538
+ var import_types22 = require("@neat.is/types");
425
539
 
426
540
  // src/divergences.ts
427
541
  init_cjs_shims();
@@ -1678,52 +1792,64 @@ function cacheSpanService(span, now) {
1678
1792
  if (!span.traceId || !span.spanId) return;
1679
1793
  const key = parentSpanKey(span.traceId, span.spanId);
1680
1794
  parentSpanCache.delete(key);
1681
- parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1795
+ parentSpanCache.set(key, {
1796
+ service: span.service,
1797
+ env: span.env ?? "unknown",
1798
+ expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1799
+ });
1682
1800
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1683
1801
  const oldest = parentSpanCache.keys().next().value;
1684
1802
  if (!oldest) break;
1685
1803
  parentSpanCache.delete(oldest);
1686
1804
  }
1687
1805
  }
1688
- function lookupParentSpanService(traceId, parentSpanId, now) {
1806
+ function lookupParentSpan(traceId, parentSpanId, now) {
1689
1807
  const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1690
1808
  if (!entry) return null;
1691
1809
  if (entry.expiresAt <= now) {
1692
1810
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1693
1811
  return null;
1694
1812
  }
1695
- return entry.service;
1813
+ return { service: entry.service, env: entry.env };
1696
1814
  }
1697
- function resolveServiceId(graph, host) {
1698
- const direct = (0, import_types4.serviceId)(host);
1699
- if (graph.hasNode(direct)) return direct;
1700
- let found = null;
1815
+ function resolveServiceId(graph, host, env) {
1816
+ const envTagged = (0, import_types4.serviceId)(host, env);
1817
+ if (graph.hasNode(envTagged)) return envTagged;
1818
+ const envLess = (0, import_types4.serviceId)(host);
1819
+ if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
1820
+ let sameEnv = null;
1821
+ let envLessMatch = null;
1822
+ let anyMatch = null;
1701
1823
  graph.forEachNode((id, attrs) => {
1702
- if (found) return;
1824
+ if (sameEnv) return;
1703
1825
  const a = attrs;
1704
1826
  if (a.type !== import_types4.NodeType.ServiceNode) return;
1705
- if (a.name === host) {
1706
- found = id;
1827
+ const matchesByName = a.name === host;
1828
+ const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1829
+ if (!matchesByName && !matchesByAlias) return;
1830
+ const nodeEnv = a.env ?? "unknown";
1831
+ if (nodeEnv === env) {
1832
+ sameEnv = id;
1707
1833
  return;
1708
1834
  }
1709
- if (a.aliases && a.aliases.includes(host)) {
1710
- found = id;
1711
- }
1835
+ if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
1836
+ else if (!anyMatch) anyMatch = id;
1712
1837
  });
1713
- return found;
1838
+ return sameEnv ?? envLessMatch ?? anyMatch;
1714
1839
  }
1715
1840
  function frontierIdFor(host) {
1716
1841
  return (0, import_types4.frontierId)(host);
1717
1842
  }
1718
- function ensureServiceNode(graph, serviceName) {
1719
- const id = (0, import_types4.serviceId)(serviceName);
1843
+ function ensureServiceNode(graph, serviceName, env) {
1844
+ const id = (0, import_types4.serviceId)(serviceName, env);
1720
1845
  if (graph.hasNode(id)) return id;
1721
1846
  const node = {
1722
1847
  id,
1723
1848
  type: import_types4.NodeType.ServiceNode,
1724
1849
  name: serviceName,
1725
1850
  language: "unknown",
1726
- discoveredVia: "otel"
1851
+ discoveredVia: "otel",
1852
+ ...env !== "unknown" ? { env } : {}
1727
1853
  };
1728
1854
  graph.addNode(id, node);
1729
1855
  return id;
@@ -1858,7 +1984,8 @@ function sanitizeAttributes(attrs) {
1858
1984
  async function handleSpan(ctx, span) {
1859
1985
  const ts = span.startTimeIso ?? nowIso(ctx);
1860
1986
  const nowMs = ctx.now ? ctx.now() : Date.now();
1861
- const sourceId = ensureServiceNode(ctx.graph, span.service);
1987
+ const env = span.env ?? "unknown";
1988
+ const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1862
1989
  const isError = span.statusCode === 2;
1863
1990
  cacheSpanService(span, nowMs);
1864
1991
  let affectedNode = sourceId;
@@ -1881,7 +2008,7 @@ async function handleSpan(ctx, span) {
1881
2008
  const host = pickAddress(span);
1882
2009
  let resolvedViaAddress = false;
1883
2010
  if (host && host !== span.service) {
1884
- const targetId = resolveServiceId(ctx.graph, host);
2011
+ const targetId = resolveServiceId(ctx.graph, host, env);
1885
2012
  if (targetId && targetId !== sourceId) {
1886
2013
  upsertObservedEdge(
1887
2014
  ctx.graph,
@@ -1908,9 +2035,9 @@ async function handleSpan(ctx, span) {
1908
2035
  }
1909
2036
  }
1910
2037
  if (!resolvedViaAddress && span.parentSpanId) {
1911
- const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
1912
- if (parentService && parentService !== span.service) {
1913
- const parentId = ensureServiceNode(ctx.graph, parentService);
2038
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
2039
+ if (parent && parent.service !== span.service) {
2040
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1914
2041
  upsertObservedEdge(
1915
2042
  ctx.graph,
1916
2043
  import_types4.EdgeType.CALLS,
@@ -2106,6 +2233,28 @@ async function readErrorEvents(errorsPath) {
2106
2233
  throw err;
2107
2234
  }
2108
2235
  }
2236
+ function mergeSnapshot(graph, snapshot) {
2237
+ const exported = snapshot.graph;
2238
+ let nodesAdded = 0;
2239
+ let edgesAdded = 0;
2240
+ for (const node of exported.nodes ?? []) {
2241
+ if (graph.hasNode(node.key)) continue;
2242
+ if (!node.attributes) continue;
2243
+ graph.addNode(node.key, node.attributes);
2244
+ nodesAdded++;
2245
+ }
2246
+ for (const edge of exported.edges ?? []) {
2247
+ const attrs = edge.attributes;
2248
+ if (!attrs) continue;
2249
+ const id = edge.key ?? attrs.id;
2250
+ if (!id) continue;
2251
+ if (graph.hasEdge(id)) continue;
2252
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2253
+ graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
2254
+ edgesAdded++;
2255
+ }
2256
+ return { nodesAdded, edgesAdded };
2257
+ }
2109
2258
 
2110
2259
  // src/extract/services.ts
2111
2260
  init_cjs_shims();
@@ -2502,6 +2651,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2502
2651
  }
2503
2652
  return [...found];
2504
2653
  }
2654
+ function detectJsFramework(pkg) {
2655
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2656
+ if (deps["next"] !== void 0) return "next";
2657
+ if (deps["remix"] !== void 0) return "remix";
2658
+ for (const k of Object.keys(deps)) {
2659
+ if (k.startsWith("@remix-run/")) return "remix";
2660
+ }
2661
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
2662
+ if (deps["nuxt"] !== void 0) return "nuxt";
2663
+ if (deps["astro"] !== void 0) return "astro";
2664
+ return void 0;
2665
+ }
2505
2666
  async function discoverNodeService(scanPath, dir) {
2506
2667
  const pkgPath = import_node_path8.default.join(dir, "package.json");
2507
2668
  if (!await exists(pkgPath)) return null;
@@ -2513,6 +2674,7 @@ async function discoverNodeService(scanPath, dir) {
2513
2674
  return null;
2514
2675
  }
2515
2676
  if (!pkg.name) return null;
2677
+ const framework = detectJsFramework(pkg);
2516
2678
  const node = {
2517
2679
  id: (0, import_types6.serviceId)(pkg.name),
2518
2680
  type: import_types6.NodeType.ServiceNode,
@@ -2521,7 +2683,8 @@ async function discoverNodeService(scanPath, dir) {
2521
2683
  version: pkg.version,
2522
2684
  dependencies: pkg.dependencies ?? {},
2523
2685
  repoPath: import_node_path8.default.relative(scanPath, dir),
2524
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2686
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2687
+ ...framework ? { framework } : {}
2525
2688
  };
2526
2689
  return { pkg, dir, node };
2527
2690
  }
@@ -4376,25 +4539,138 @@ function canonicalJson(value) {
4376
4539
  });
4377
4540
  }
4378
4541
 
4379
- // src/projects.ts
4542
+ // src/persist.ts
4380
4543
  init_cjs_shims();
4544
+ var import_node_fs19 = require("fs");
4381
4545
  var import_node_path31 = __toESM(require("path"), 1);
4546
+ var import_types20 = require("@neat.is/types");
4547
+ var SCHEMA_VERSION = 4;
4548
+ function migrateV1ToV2(payload) {
4549
+ const nodes = payload.graph.nodes;
4550
+ if (Array.isArray(nodes)) {
4551
+ for (const node of nodes) {
4552
+ if (node.attributes && "pgDriverVersion" in node.attributes) {
4553
+ delete node.attributes.pgDriverVersion;
4554
+ }
4555
+ }
4556
+ }
4557
+ return { ...payload, schemaVersion: 2 };
4558
+ }
4559
+ function migrateV3ToV4(payload) {
4560
+ return { ...payload, schemaVersion: 4 };
4561
+ }
4562
+ function migrateV2ToV3(payload) {
4563
+ const edges = payload.graph.edges;
4564
+ if (Array.isArray(edges)) {
4565
+ for (const edge of edges) {
4566
+ const attrs = edge.attributes;
4567
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
4568
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4569
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
4570
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4571
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4572
+ if (type && source && target) {
4573
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4574
+ attrs.id = newId;
4575
+ if (edge.key) edge.key = newId;
4576
+ }
4577
+ }
4578
+ }
4579
+ return { ...payload, schemaVersion: 3 };
4580
+ }
4581
+ async function ensureDir(filePath) {
4582
+ await import_node_fs19.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4583
+ }
4584
+ async function saveGraphToDisk(graph, outPath) {
4585
+ await ensureDir(outPath);
4586
+ const payload = {
4587
+ schemaVersion: SCHEMA_VERSION,
4588
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4589
+ graph: graph.export()
4590
+ };
4591
+ const tmp = `${outPath}.tmp`;
4592
+ await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4593
+ await import_node_fs19.promises.rename(tmp, outPath);
4594
+ }
4595
+ async function loadGraphFromDisk(graph, outPath) {
4596
+ let raw;
4597
+ try {
4598
+ raw = await import_node_fs19.promises.readFile(outPath, "utf8");
4599
+ } catch (err) {
4600
+ if (err.code === "ENOENT") return;
4601
+ throw err;
4602
+ }
4603
+ let payload = JSON.parse(raw);
4604
+ if (payload.schemaVersion === 1) {
4605
+ payload = migrateV1ToV2(payload);
4606
+ }
4607
+ if (payload.schemaVersion === 2) {
4608
+ payload = migrateV2ToV3(payload);
4609
+ }
4610
+ if (payload.schemaVersion === 3) {
4611
+ payload = migrateV3ToV4(payload);
4612
+ }
4613
+ if (payload.schemaVersion !== SCHEMA_VERSION) {
4614
+ throw new Error(
4615
+ `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
4616
+ );
4617
+ }
4618
+ graph.clear();
4619
+ graph.import(payload.graph);
4620
+ }
4621
+ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4622
+ let stopped = false;
4623
+ const tick = async () => {
4624
+ if (stopped) return;
4625
+ try {
4626
+ await saveGraphToDisk(graph, outPath);
4627
+ } catch (err) {
4628
+ console.error("persist: periodic save failed", err);
4629
+ }
4630
+ };
4631
+ const interval = setInterval(() => {
4632
+ void tick();
4633
+ }, intervalMs);
4634
+ const onSignal = (signal) => {
4635
+ void (async () => {
4636
+ try {
4637
+ await saveGraphToDisk(graph, outPath);
4638
+ } catch (err) {
4639
+ console.error(`persist: ${signal} save failed`, err);
4640
+ } finally {
4641
+ process.exit(0);
4642
+ }
4643
+ })();
4644
+ };
4645
+ process.on("SIGTERM", onSignal);
4646
+ process.on("SIGINT", onSignal);
4647
+ return () => {
4648
+ stopped = true;
4649
+ clearInterval(interval);
4650
+ process.off("SIGTERM", onSignal);
4651
+ process.off("SIGINT", onSignal);
4652
+ };
4653
+ }
4654
+
4655
+ // src/projects.ts
4656
+ init_cjs_shims();
4657
+ var import_node_path32 = __toESM(require("path"), 1);
4382
4658
  function pathsForProject(project, baseDir) {
4383
4659
  if (project === DEFAULT_PROJECT) {
4384
4660
  return {
4385
- snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
4386
- errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
4387
- staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
4388
- embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
4389
- policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
4661
+ snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4662
+ errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4663
+ staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4664
+ embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4665
+ policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
4390
4666
  };
4391
4667
  }
4392
4668
  return {
4393
- snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
4394
- errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
4395
- staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
4396
- embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
4397
- policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
4669
+ snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4670
+ errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4671
+ staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4672
+ embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4673
+ policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
4398
4674
  };
4399
4675
  }
4400
4676
  var Projects = class {
@@ -4434,23 +4710,23 @@ function parseExtraProjects(raw) {
4434
4710
 
4435
4711
  // src/registry.ts
4436
4712
  init_cjs_shims();
4437
- var import_node_fs19 = require("fs");
4713
+ var import_node_fs20 = require("fs");
4438
4714
  var import_node_os2 = __toESM(require("os"), 1);
4439
- var import_node_path32 = __toESM(require("path"), 1);
4440
- var import_types20 = require("@neat.is/types");
4715
+ var import_node_path33 = __toESM(require("path"), 1);
4716
+ var import_types21 = require("@neat.is/types");
4441
4717
  function neatHome() {
4442
4718
  const override = process.env.NEAT_HOME;
4443
- if (override && override.length > 0) return import_node_path32.default.resolve(override);
4444
- return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
4719
+ if (override && override.length > 0) return import_node_path33.default.resolve(override);
4720
+ return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4445
4721
  }
4446
4722
  function registryPath() {
4447
- return import_node_path32.default.join(neatHome(), "projects.json");
4723
+ return import_node_path33.default.join(neatHome(), "projects.json");
4448
4724
  }
4449
4725
  async function readRegistry() {
4450
4726
  const file = registryPath();
4451
4727
  let raw;
4452
4728
  try {
4453
- raw = await import_node_fs19.promises.readFile(file, "utf8");
4729
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4454
4730
  } catch (err) {
4455
4731
  if (err.code === "ENOENT") {
4456
4732
  return { version: 1, projects: [] };
@@ -4458,7 +4734,7 @@ async function readRegistry() {
4458
4734
  throw err;
4459
4735
  }
4460
4736
  const parsed = JSON.parse(raw);
4461
- return import_types20.RegistryFileSchema.parse(parsed);
4737
+ return import_types21.RegistryFileSchema.parse(parsed);
4462
4738
  }
4463
4739
  async function getProject(name) {
4464
4740
  const reg = await readRegistry();
@@ -4525,6 +4801,7 @@ data: ${JSON.stringify(envelope.payload)}
4525
4801
  }
4526
4802
 
4527
4803
  // src/api.ts
4804
+ init_auth();
4528
4805
  function serializeGraph(graph) {
4529
4806
  const nodes = [];
4530
4807
  graph.forEachNode((_id, attrs) => {
@@ -4648,11 +4925,11 @@ function registerRoutes(scope, ctx) {
4648
4925
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4649
4926
  const parsed = [];
4650
4927
  for (const c of candidates) {
4651
- const r = import_types21.DivergenceTypeSchema.safeParse(c);
4928
+ const r = import_types22.DivergenceTypeSchema.safeParse(c);
4652
4929
  if (!r.success) {
4653
4930
  return reply.code(400).send({
4654
4931
  error: `unknown divergence type "${c}"`,
4655
- allowed: import_types21.DivergenceTypeSchema.options
4932
+ allowed: import_types22.DivergenceTypeSchema.options
4656
4933
  });
4657
4934
  }
4658
4935
  parsed.push(r.data);
@@ -4797,6 +5074,35 @@ function registerRoutes(scope, ctx) {
4797
5074
  }
4798
5075
  }
4799
5076
  );
5077
+ scope.post("/snapshot", async (req, reply) => {
5078
+ const proj = resolveProject(registry, req, reply);
5079
+ if (!proj) return;
5080
+ const body = req.body;
5081
+ if (!body || typeof body !== "object" || !body.snapshot) {
5082
+ return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
5083
+ }
5084
+ const snap = body.snapshot;
5085
+ if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
5086
+ return reply.code(400).send({
5087
+ error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
5088
+ });
5089
+ }
5090
+ try {
5091
+ const result = mergeSnapshot(proj.graph, snap);
5092
+ return {
5093
+ project: proj.name,
5094
+ nodesAdded: result.nodesAdded,
5095
+ edgesAdded: result.edgesAdded,
5096
+ nodeCount: proj.graph.order,
5097
+ edgeCount: proj.graph.size
5098
+ };
5099
+ } catch (err) {
5100
+ return reply.code(400).send({
5101
+ error: "snapshot merge failed",
5102
+ details: err.message
5103
+ });
5104
+ }
5105
+ });
4800
5106
  scope.post("/graph/scan", async (req, reply) => {
4801
5107
  const proj = resolveProject(registry, req, reply);
4802
5108
  if (!proj) return;
@@ -4836,7 +5142,7 @@ function registerRoutes(scope, ctx) {
4836
5142
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4837
5143
  let violations = await log.readAll();
4838
5144
  if (req.query.severity) {
4839
- const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
5145
+ const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
4840
5146
  if (!sev.success) {
4841
5147
  return reply.code(400).send({
4842
5148
  error: "invalid severity",
@@ -4853,7 +5159,7 @@ function registerRoutes(scope, ctx) {
4853
5159
  scope.post("/policies/check", async (req, reply) => {
4854
5160
  const proj = resolveProject(registry, req, reply);
4855
5161
  if (!proj) return;
4856
- const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5162
+ const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4857
5163
  if (!parsed.success) {
4858
5164
  return reply.code(400).send({
4859
5165
  error: "invalid /policies/check body",
@@ -4890,6 +5196,15 @@ function registerRoutes(scope, ctx) {
4890
5196
  async function buildApi(opts) {
4891
5197
  const app = (0, import_fastify.default)({ logger: false });
4892
5198
  await app.register(import_cors.default, { origin: true });
5199
+ mountBearerAuth(app, {
5200
+ token: opts.authToken,
5201
+ trustProxy: opts.trustProxy,
5202
+ publicRead: opts.publicRead
5203
+ });
5204
+ app.get("/api/config", async () => ({
5205
+ publicRead: opts.publicRead === true,
5206
+ authProxy: opts.trustProxy === true
5207
+ }));
4893
5208
  const startedAt = opts.startedAt ?? Date.now();
4894
5209
  const registry = buildLegacyRegistry(opts);
4895
5210
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -4951,113 +5266,6 @@ async function buildApi(opts) {
4951
5266
  return app;
4952
5267
  }
4953
5268
 
4954
- // src/persist.ts
4955
- init_cjs_shims();
4956
- var import_node_fs20 = require("fs");
4957
- var import_node_path33 = __toESM(require("path"), 1);
4958
- var import_types22 = require("@neat.is/types");
4959
- var SCHEMA_VERSION = 3;
4960
- function migrateV1ToV2(payload) {
4961
- const nodes = payload.graph.nodes;
4962
- if (Array.isArray(nodes)) {
4963
- for (const node of nodes) {
4964
- if (node.attributes && "pgDriverVersion" in node.attributes) {
4965
- delete node.attributes.pgDriverVersion;
4966
- }
4967
- }
4968
- }
4969
- return { ...payload, schemaVersion: 2 };
4970
- }
4971
- function migrateV2ToV3(payload) {
4972
- const edges = payload.graph.edges;
4973
- if (Array.isArray(edges)) {
4974
- for (const edge of edges) {
4975
- const attrs = edge.attributes;
4976
- if (!attrs || attrs.provenance !== "FRONTIER") continue;
4977
- attrs.provenance = import_types22.Provenance.OBSERVED;
4978
- const type = typeof attrs.type === "string" ? attrs.type : void 0;
4979
- const source = typeof attrs.source === "string" ? attrs.source : void 0;
4980
- const target = typeof attrs.target === "string" ? attrs.target : void 0;
4981
- if (type && source && target) {
4982
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
4983
- attrs.id = newId;
4984
- if (edge.key) edge.key = newId;
4985
- }
4986
- }
4987
- }
4988
- return { ...payload, schemaVersion: 3 };
4989
- }
4990
- async function ensureDir(filePath) {
4991
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
4992
- }
4993
- async function saveGraphToDisk(graph, outPath) {
4994
- await ensureDir(outPath);
4995
- const payload = {
4996
- schemaVersion: SCHEMA_VERSION,
4997
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4998
- graph: graph.export()
4999
- };
5000
- const tmp = `${outPath}.tmp`;
5001
- await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
5002
- await import_node_fs20.promises.rename(tmp, outPath);
5003
- }
5004
- async function loadGraphFromDisk(graph, outPath) {
5005
- let raw;
5006
- try {
5007
- raw = await import_node_fs20.promises.readFile(outPath, "utf8");
5008
- } catch (err) {
5009
- if (err.code === "ENOENT") return;
5010
- throw err;
5011
- }
5012
- let payload = JSON.parse(raw);
5013
- if (payload.schemaVersion === 1) {
5014
- payload = migrateV1ToV2(payload);
5015
- }
5016
- if (payload.schemaVersion === 2) {
5017
- payload = migrateV2ToV3(payload);
5018
- }
5019
- if (payload.schemaVersion !== SCHEMA_VERSION) {
5020
- throw new Error(
5021
- `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
5022
- );
5023
- }
5024
- graph.clear();
5025
- graph.import(payload.graph);
5026
- }
5027
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5028
- let stopped = false;
5029
- const tick = async () => {
5030
- if (stopped) return;
5031
- try {
5032
- await saveGraphToDisk(graph, outPath);
5033
- } catch (err) {
5034
- console.error("persist: periodic save failed", err);
5035
- }
5036
- };
5037
- const interval = setInterval(() => {
5038
- void tick();
5039
- }, intervalMs);
5040
- const onSignal = (signal) => {
5041
- void (async () => {
5042
- try {
5043
- await saveGraphToDisk(graph, outPath);
5044
- } catch (err) {
5045
- console.error(`persist: ${signal} save failed`, err);
5046
- } finally {
5047
- process.exit(0);
5048
- }
5049
- })();
5050
- };
5051
- process.on("SIGTERM", onSignal);
5052
- process.on("SIGINT", onSignal);
5053
- return () => {
5054
- stopped = true;
5055
- clearInterval(interval);
5056
- process.off("SIGTERM", onSignal);
5057
- process.off("SIGINT", onSignal);
5058
- };
5059
- }
5060
-
5061
5269
  // src/server.ts
5062
5270
  init_otel();
5063
5271
  init_otel_grpc();
@@ -5066,7 +5274,7 @@ init_otel_grpc();
5066
5274
  init_cjs_shims();
5067
5275
  var import_node_fs21 = require("fs");
5068
5276
  var import_node_path36 = __toESM(require("path"), 1);
5069
- var import_node_crypto = require("crypto");
5277
+ var import_node_crypto3 = require("crypto");
5070
5278
  var DEFAULT_LIMIT = 10;
5071
5279
  var NOMIC_DIM = 768;
5072
5280
  var MINI_LM_DIM = 384;
@@ -5106,7 +5314,7 @@ function embedText(node) {
5106
5314
  return parts.join(" ");
5107
5315
  }
5108
5316
  function attrsHash(node) {
5109
- return (0, import_node_crypto.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5317
+ return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5110
5318
  }
5111
5319
  function cosine(a, b) {
5112
5320
  if (a.length !== b.length) return 0;
@@ -5350,6 +5558,7 @@ async function buildSearchIndex(graph, options = {}) {
5350
5558
  }
5351
5559
 
5352
5560
  // src/server.ts
5561
+ init_auth();
5353
5562
  async function bootProject(registry, name, scanPath, baseDir) {
5354
5563
  const paths = pathsForProject(name, baseDir);
5355
5564
  const graph = getGraph(name);
@@ -5396,7 +5605,14 @@ async function main() {
5396
5605
  const host = process.env.HOST ?? "0.0.0.0";
5397
5606
  const port = Number(process.env.PORT ?? 8080);
5398
5607
  const otelPort = Number(process.env.OTEL_PORT ?? 4318);
5399
- const app = await buildApi({ projects: registry });
5608
+ const auth = readAuthEnv();
5609
+ assertBindAuthority(host, auth.authToken);
5610
+ const app = await buildApi({
5611
+ projects: registry,
5612
+ authToken: auth.authToken,
5613
+ trustProxy: auth.trustProxy,
5614
+ publicRead: auth.publicRead
5615
+ });
5400
5616
  await app.listen({ port, host });
5401
5617
  console.log(`neat-core listening on http://${host}:${port}`);
5402
5618
  console.log(` base dir: ${baseDir}`);
@@ -5407,12 +5623,22 @@ async function main() {
5407
5623
  graph: defaultCtx.graph,
5408
5624
  errorsPath: defaultCtx.paths.errorsPath
5409
5625
  });
5410
- const otelApp = await buildOtelReceiver({ onSpan });
5626
+ const otelApp = await buildOtelReceiver({
5627
+ onSpan,
5628
+ authToken: auth.otelToken,
5629
+ trustProxy: auth.trustProxy
5630
+ });
5411
5631
  await otelApp.listen({ port: otelPort, host });
5412
5632
  console.log(`neat-core OTLP receiver on http://${host}:${otelPort}/v1/traces`);
5413
5633
  if (process.env.NEAT_OTLP_GRPC === "true") {
5414
5634
  const grpcPort = Number(process.env.NEAT_OTLP_GRPC_PORT ?? 4317);
5415
- const grpcReceiver = await startOtelGrpcReceiver({ onSpan, host, port: grpcPort });
5635
+ const grpcReceiver = await startOtelGrpcReceiver({
5636
+ onSpan,
5637
+ host,
5638
+ port: grpcPort,
5639
+ authToken: auth.otelToken,
5640
+ trustProxy: auth.trustProxy
5641
+ });
5416
5642
  console.log(`neat-core OTLP/gRPC receiver on ${grpcReceiver.address}`);
5417
5643
  }
5418
5644
  }