@neat.is/core 0.10.3 → 0.10.5-dev.20261001

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/index.cjs CHANGED
@@ -50,6 +50,10 @@ function assertBindAuthority(host, token) {
50
50
  if (isLoopbackHost2(host)) return;
51
51
  throw new BindAuthorityError(host);
52
52
  }
53
+ function isBearerDelegated(req) {
54
+ const config = req.routeOptions?.config;
55
+ return config?.bearerDelegated === true;
56
+ }
53
57
  function mountBearerAuth(app, opts) {
54
58
  if (!opts.token || opts.token.length === 0) return;
55
59
  if (opts.trustProxy) return;
@@ -65,6 +69,10 @@ function mountBearerAuth(app, opts) {
65
69
  done();
66
70
  return;
67
71
  }
72
+ if (isBearerDelegated(req)) {
73
+ done();
74
+ return;
75
+ }
68
76
  if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
69
77
  done();
70
78
  return;
@@ -98,7 +106,7 @@ function readAuthEnv(env = process.env) {
98
106
  publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
99
107
  };
100
108
  }
101
- var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
109
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN, BEARER_DELEGATED;
102
110
  var init_auth = __esm({
103
111
  "src/auth.ts"() {
104
112
  "use strict";
@@ -128,6 +136,7 @@ var init_auth = __esm({
128
136
  PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
129
137
  `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
130
138
  );
139
+ BEARER_DELEGATED = { bearerDelegated: true };
131
140
  }
132
141
  });
133
142
 
@@ -3390,6 +3399,10 @@ function languageForPath(relPath) {
3390
3399
  return void 0;
3391
3400
  }
3392
3401
  }
3402
+ var currentSource;
3403
+ function setExtractionSource(source) {
3404
+ currentSource = source;
3405
+ }
3393
3406
  function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3394
3407
  let nodesAdded = 0;
3395
3408
  let edgesAdded = 0;
@@ -3402,10 +3415,16 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3402
3415
  service: serviceName,
3403
3416
  path: relPath,
3404
3417
  ...language ? { language } : {},
3418
+ ...currentSource ? { source: currentSource } : {},
3405
3419
  discoveredVia: "static"
3406
3420
  };
3407
3421
  graph.addNode(fileNodeId, node);
3408
3422
  nodesAdded++;
3423
+ } else if (currentSource) {
3424
+ const existing = graph.getNodeAttributes(fileNodeId);
3425
+ if (existing.source !== currentSource) {
3426
+ graph.replaceNodeAttributes(fileNodeId, { ...existing, source: currentSource });
3427
+ }
3409
3428
  }
3410
3429
  const containsId = (0, import_types4.extractedEdgeId)(serviceNodeId, fileNodeId, import_types4.EdgeType.CONTAINS);
3411
3430
  if (!graph.hasEdge(containsId)) {
@@ -16466,10 +16485,12 @@ init_cjs_shims();
16466
16485
  var import_node_fs33 = require("fs");
16467
16486
  var import_node_path68 = __toESM(require("path"), 1);
16468
16487
  var import_types58 = require("@neat.is/types");
16469
- function dropOrphanedFileNodes(graph) {
16488
+ function dropOrphanedFileNodes(graph, source) {
16470
16489
  const orphans = [];
16471
16490
  graph.forEachNode((id, attrs) => {
16472
- if (attrs.type !== import_types58.NodeType.FileNode) return;
16491
+ const node = attrs;
16492
+ if (node.type !== import_types58.NodeType.FileNode) return;
16493
+ if (source && node.source !== source) return;
16473
16494
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
16474
16495
  orphans.push(id);
16475
16496
  }
@@ -16477,12 +16498,23 @@ function dropOrphanedFileNodes(graph) {
16477
16498
  for (const id of orphans) graph.dropNode(id);
16478
16499
  return orphans.length;
16479
16500
  }
16480
- function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16501
+ function outOfScope(graph, edge, source) {
16502
+ if (!source) return false;
16503
+ for (const endpoint2 of [edge.source, edge.target]) {
16504
+ if (!graph.hasNode(endpoint2)) continue;
16505
+ const node = graph.getNodeAttributes(endpoint2);
16506
+ if (node.type !== import_types58.NodeType.FileNode) continue;
16507
+ return node.source !== source;
16508
+ }
16509
+ return false;
16510
+ }
16511
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], source) {
16481
16512
  const toDrop = [];
16482
16513
  const bases = [scanPath, ...serviceDirs];
16483
16514
  graph.forEachEdge((id, attrs) => {
16484
16515
  const edge = attrs;
16485
16516
  if (edge.provenance !== import_types58.Provenance.EXTRACTED) return;
16517
+ if (outOfScope(graph, edge, source)) return;
16486
16518
  const evidenceFile = edge.evidence?.file;
16487
16519
  if (!evidenceFile) return;
16488
16520
  if (import_node_path68.default.isAbsolute(evidenceFile)) {
@@ -16493,7 +16525,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16493
16525
  if (!found) toDrop.push(id);
16494
16526
  });
16495
16527
  for (const id of toDrop) graph.dropEdge(id);
16496
- dropOrphanedFileNodes(graph);
16528
+ dropOrphanedFileNodes(graph, source);
16497
16529
  return toDrop.length;
16498
16530
  }
16499
16531
 
@@ -16502,6 +16534,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16502
16534
  await ensureCompatLoaded();
16503
16535
  drainExtractionErrors();
16504
16536
  drainSkippedFiles();
16537
+ setExtractionSource(opts.source);
16505
16538
  const services = await discoverServices(scanPath);
16506
16539
  const phase1Nodes = addServiceNodes(graph, services);
16507
16540
  await addServiceAliases(graph, scanPath, services);
@@ -16522,7 +16555,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16522
16555
  const ghostsRetired = retireExtractedEdgesByMissingFile(
16523
16556
  graph,
16524
16557
  scanPath,
16525
- services.map((s) => s.dir)
16558
+ services.map((s) => s.dir),
16559
+ opts.source
16526
16560
  );
16527
16561
  const frontiersPromoted = promoteFrontierNodes(graph);
16528
16562
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
@@ -16557,6 +16591,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16557
16591
  );
16558
16592
  }
16559
16593
  }
16594
+ setExtractionSource(void 0);
16560
16595
  const result = {
16561
16596
  nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + actionPhase.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded + zodPhase.nodesAdded + firestoreRulesPhase.nodesAdded,
16562
16597
  edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + actionPhase.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded + zodPhase.edgesAdded + firestoreRulesPhase.edgesAdded,
@@ -23616,6 +23651,19 @@ function registerRoutes(scope, ctx) {
23616
23651
  });
23617
23652
  }
23618
23653
  });
23654
+ scope.post("/repo-sync", async (req, reply) => {
23655
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
23656
+ if (!proj) return;
23657
+ const syncNow = ctx.repoSync?.(proj.name);
23658
+ if (!syncNow) {
23659
+ return reply.code(404).send({
23660
+ error: "repo-sync is not running for this project",
23661
+ project: proj.name,
23662
+ hint: "Only a hosted daemon syncs bound repos. On a local daemon, POST /graph/scan re-extracts the project path."
23663
+ });
23664
+ }
23665
+ return reply.code(202).send({ project: proj.name, ...syncNow() });
23666
+ });
23619
23667
  scope.post("/graph/scan", async (req, reply) => {
23620
23668
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
23621
23669
  if (!proj) return;
@@ -23863,6 +23911,39 @@ function registerRoutes(scope, ctx) {
23863
23911
  }
23864
23912
  });
23865
23913
  }
23914
+ var OTLP_BODY_LIMIT = 16 * 1024 * 1024;
23915
+ function otlpOnRest(getReceiver) {
23916
+ return async function plugin(scope) {
23917
+ scope.removeAllContentTypeParsers();
23918
+ scope.addContentTypeParser(
23919
+ "*",
23920
+ { parseAs: "buffer", bodyLimit: OTLP_BODY_LIMIT },
23921
+ (_req, body, done) => done(null, body)
23922
+ );
23923
+ const relay = async (req, reply) => {
23924
+ const receiver = getReceiver();
23925
+ if (!receiver) {
23926
+ return reply.code(503).header("retry-after", "1").send({ error: "OTLP receiver is starting" });
23927
+ }
23928
+ const headers = { ...req.headers };
23929
+ delete headers["content-length"];
23930
+ delete headers["transfer-encoding"];
23931
+ delete headers.connection;
23932
+ const res = await receiver.inject({
23933
+ method: "POST",
23934
+ url: req.url,
23935
+ headers,
23936
+ payload: req.body
23937
+ });
23938
+ const contentType = res.headers["content-type"];
23939
+ if (contentType !== void 0) void reply.header("content-type", contentType);
23940
+ return reply.code(res.statusCode).send(res.rawPayload);
23941
+ };
23942
+ const route = { config: BEARER_DELEGATED, bodyLimit: OTLP_BODY_LIMIT };
23943
+ scope.post("/v1/traces", route, relay);
23944
+ scope.post("/projects/:project/v1/traces", route, relay);
23945
+ };
23946
+ }
23866
23947
  async function buildApi(opts) {
23867
23948
  const app = (0, import_fastify2.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
23868
23949
  await app.register(import_cors.default, { origin: true });
@@ -23881,6 +23962,7 @@ async function buildApi(opts) {
23881
23962
  authProxy: trustProxy === true,
23882
23963
  requiresAuth
23883
23964
  }));
23965
+ if (opts.otlpReceiver) await app.register(otlpOnRest(opts.otlpReceiver));
23884
23966
  const startedAt = opts.startedAt ?? Date.now();
23885
23967
  const registry = buildLegacyRegistry(opts);
23886
23968
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -23911,6 +23993,7 @@ async function buildApi(opts) {
23911
23993
  bootstrap: opts.bootstrap,
23912
23994
  singleProject: opts.singleProject?.name,
23913
23995
  connectorsHome: opts.connectorsHome,
23996
+ repoSync: opts.repoSync,
23914
23997
  runPoll: opts.runPoll ?? runConnectorPoll
23915
23998
  };
23916
23999
  app.get("/health", async () => {
@@ -24469,7 +24552,7 @@ async function syncOneRepo(r, input) {
24469
24552
  await cpPostStatus(deps, r.owner, r.name, { syncStatus: "syncing", detail: "cloning" }).catch(() => {
24470
24553
  });
24471
24554
  await cloneRepo(r.cloneUrl, r.defaultBranch, dir);
24472
- const extracted = await extract(graph, dir);
24555
+ const extracted = await extract(graph, dir, { source: label });
24473
24556
  const nodes = extracted?.nodesAdded ?? 0;
24474
24557
  const edges = extracted?.edgesAdded ?? 0;
24475
24558
  await cpPostStatus(deps, r.owner, r.name, {
@@ -24477,6 +24560,7 @@ async function syncOneRepo(r, input) {
24477
24560
  detail: `extracted ${nodes} node${nodes === 1 ? "" : "s"}, ${edges} edge${edges === 1 ? "" : "s"}`,
24478
24561
  lastSyncAt: new Date(input.now?.() ?? Date.now()).toISOString()
24479
24562
  });
24563
+ return true;
24480
24564
  } catch (err) {
24481
24565
  input.onError?.(label, err);
24482
24566
  await cpPostStatus(deps, r.owner, r.name, {
@@ -24484,50 +24568,76 @@ async function syncOneRepo(r, input) {
24484
24568
  detail: scrubToken(err.message).slice(0, 300)
24485
24569
  }).catch(() => {
24486
24570
  });
24571
+ return false;
24487
24572
  } finally {
24488
24573
  if (dir) await (0, import_promises2.rm)(dir, { recursive: true, force: true }).catch(() => {
24489
24574
  });
24490
24575
  }
24491
24576
  }
24492
- async function runRepoSyncPass(input) {
24577
+ async function runRepoSyncPassCounted(input) {
24578
+ const counts = { listed: false, synced: 0, failed: 0 };
24493
24579
  let repos;
24494
24580
  try {
24495
24581
  repos = await cpGet2(`/internal/projects/${input.deps.projectId}/repos`, input.deps);
24496
24582
  } catch (err) {
24497
24583
  input.onSkip?.("(all)", `control plane repo list unreadable \u2014 ${err.message}`);
24498
- return false;
24584
+ return counts;
24499
24585
  }
24500
- if (!Array.isArray(repos)) return false;
24586
+ if (!Array.isArray(repos)) return counts;
24587
+ counts.listed = true;
24501
24588
  for (const r of repos) {
24502
24589
  if (!input.forceResync && !needsSync(r)) continue;
24503
- await syncOneRepo(r, input);
24590
+ if (await syncOneRepo(r, input)) counts.synced++;
24591
+ else counts.failed++;
24504
24592
  }
24505
- return true;
24593
+ return counts;
24506
24594
  }
24507
24595
  async function startRepoSync(input) {
24508
24596
  const intervalMs = input.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
24597
+ const now = () => input.now?.() ?? Date.now();
24509
24598
  let stopped = false;
24510
- let running = false;
24599
+ let inFlight = null;
24600
+ let again = false;
24601
+ let lastPass;
24511
24602
  let bootResyncDone = false;
24512
- const tick = async () => {
24513
- if (stopped || running) return;
24514
- running = true;
24603
+ const run = async () => {
24515
24604
  try {
24516
- const listed = await runRepoSyncPass({ ...input, forceResync: !bootResyncDone });
24517
- if (listed) bootResyncDone = true;
24605
+ do {
24606
+ again = false;
24607
+ const startedAt = new Date(now()).toISOString();
24608
+ const counts = await runRepoSyncPassCounted({ ...input, forceResync: !bootResyncDone });
24609
+ if (counts.listed) bootResyncDone = true;
24610
+ lastPass = { ...counts, startedAt, finishedAt: new Date(now()).toISOString() };
24611
+ } while (again && !stopped);
24518
24612
  } finally {
24519
- running = false;
24613
+ inFlight = null;
24520
24614
  }
24521
24615
  };
24522
- void tick();
24523
- const timer = setInterval(() => {
24524
- void tick();
24525
- }, intervalMs);
24616
+ const tick = () => {
24617
+ if (stopped || inFlight) return;
24618
+ inFlight = run();
24619
+ };
24620
+ tick();
24621
+ const timer = setInterval(tick, intervalMs);
24526
24622
  if (typeof timer.unref === "function") timer.unref();
24527
- return () => {
24623
+ const stop = () => {
24528
24624
  stopped = true;
24529
24625
  clearInterval(timer);
24530
24626
  };
24627
+ return Object.assign(stop, {
24628
+ syncNow: () => {
24629
+ const summary = lastPass ? { lastPass } : {};
24630
+ if (inFlight) {
24631
+ again = true;
24632
+ return { status: "queued", ...summary };
24633
+ }
24634
+ inFlight = run();
24635
+ return { status: "started", ...summary };
24636
+ },
24637
+ settled: async () => {
24638
+ while (inFlight) await inFlight;
24639
+ }
24640
+ });
24531
24641
  }
24532
24642
  async function maybeStartRepoSync(input) {
24533
24643
  const env = input.env ?? process.env;
@@ -25225,7 +25335,7 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
25225
25335
  await loadGraphFromDisk(graph, outPath);
25226
25336
  const detachEvents = attachGraphToEventBus(graph, { project: entry.name });
25227
25337
  try {
25228
- await extractFromDirectory(graph, entry.path);
25338
+ await extractFromDirectory(graph, entry.path, { source: entry.name });
25229
25339
  const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
25230
25340
  const stopStaleness = startStalenessLoop(graph, {
25231
25341
  staleEventsPath: paths.staleEventsPath,
@@ -25283,6 +25393,7 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
25283
25393
  stopHostedConnectors,
25284
25394
  stopK8sSubstrate,
25285
25395
  stopRepoSync,
25396
+ ...stopRepoSync.syncNow ? { syncRepos: stopRepoSync.syncNow } : {},
25286
25397
  detachEvents,
25287
25398
  status: "active"
25288
25399
  };
@@ -25554,7 +25665,14 @@ async function startDaemon(opts = {}) {
25554
25665
  // through the same resolved home the slot bootstrap read it from, so a
25555
25666
  // daemon given an explicit NEAT_HOME serves status for the same file it
25556
25667
  // polls.
25557
- connectorsHome: home
25668
+ connectorsHome: home,
25669
+ // #1293 — "sync this project's repos now". Looked up per request: the
25670
+ // slot, and with it the trigger, appears once bootstrap has run.
25671
+ repoSync: (name) => slots.get(name)?.syncRepos,
25672
+ // OTLP answers on this listener too, so a host that routes a single
25673
+ // port still delivers a tenant app's spans. Bound late: the receiver
25674
+ // is built below, once the REST bind has succeeded.
25675
+ otlpReceiver: () => otlpApp ?? void 0
25558
25676
  });
25559
25677
  restAddress = await restApp.listen({ port: restPort, host });
25560
25678
  console.log(
@@ -25735,7 +25853,9 @@ async function startDaemon(opts = {}) {
25735
25853
  }
25736
25854
  });
25737
25855
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
25738
- console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
25856
+ console.log(
25857
+ `neatd: OTLP listening on http://${host}:${portFromListenAddress(otlpAddress, otlpPort)}/v1/traces (also at /v1/traces on the REST port)`
25858
+ );
25739
25859
  } catch (err) {
25740
25860
  for (const slot of slots.values()) {
25741
25861
  teardownSlot(slot);