@neat.is/core 0.10.3-dev.20260930 → 0.10.4

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
 
@@ -23616,6 +23625,19 @@ function registerRoutes(scope, ctx) {
23616
23625
  });
23617
23626
  }
23618
23627
  });
23628
+ scope.post("/repo-sync", async (req, reply) => {
23629
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
23630
+ if (!proj) return;
23631
+ const syncNow = ctx.repoSync?.(proj.name);
23632
+ if (!syncNow) {
23633
+ return reply.code(404).send({
23634
+ error: "repo-sync is not running for this project",
23635
+ project: proj.name,
23636
+ hint: "Only a hosted daemon syncs bound repos. On a local daemon, POST /graph/scan re-extracts the project path."
23637
+ });
23638
+ }
23639
+ return reply.code(202).send({ project: proj.name, ...syncNow() });
23640
+ });
23619
23641
  scope.post("/graph/scan", async (req, reply) => {
23620
23642
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
23621
23643
  if (!proj) return;
@@ -23863,6 +23885,39 @@ function registerRoutes(scope, ctx) {
23863
23885
  }
23864
23886
  });
23865
23887
  }
23888
+ var OTLP_BODY_LIMIT = 16 * 1024 * 1024;
23889
+ function otlpOnRest(getReceiver) {
23890
+ return async function plugin(scope) {
23891
+ scope.removeAllContentTypeParsers();
23892
+ scope.addContentTypeParser(
23893
+ "*",
23894
+ { parseAs: "buffer", bodyLimit: OTLP_BODY_LIMIT },
23895
+ (_req, body, done) => done(null, body)
23896
+ );
23897
+ const relay = async (req, reply) => {
23898
+ const receiver = getReceiver();
23899
+ if (!receiver) {
23900
+ return reply.code(503).header("retry-after", "1").send({ error: "OTLP receiver is starting" });
23901
+ }
23902
+ const headers = { ...req.headers };
23903
+ delete headers["content-length"];
23904
+ delete headers["transfer-encoding"];
23905
+ delete headers.connection;
23906
+ const res = await receiver.inject({
23907
+ method: "POST",
23908
+ url: req.url,
23909
+ headers,
23910
+ payload: req.body
23911
+ });
23912
+ const contentType = res.headers["content-type"];
23913
+ if (contentType !== void 0) void reply.header("content-type", contentType);
23914
+ return reply.code(res.statusCode).send(res.rawPayload);
23915
+ };
23916
+ const route = { config: BEARER_DELEGATED, bodyLimit: OTLP_BODY_LIMIT };
23917
+ scope.post("/v1/traces", route, relay);
23918
+ scope.post("/projects/:project/v1/traces", route, relay);
23919
+ };
23920
+ }
23866
23921
  async function buildApi(opts) {
23867
23922
  const app = (0, import_fastify2.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
23868
23923
  await app.register(import_cors.default, { origin: true });
@@ -23881,6 +23936,7 @@ async function buildApi(opts) {
23881
23936
  authProxy: trustProxy === true,
23882
23937
  requiresAuth
23883
23938
  }));
23939
+ if (opts.otlpReceiver) await app.register(otlpOnRest(opts.otlpReceiver));
23884
23940
  const startedAt = opts.startedAt ?? Date.now();
23885
23941
  const registry = buildLegacyRegistry(opts);
23886
23942
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -23911,6 +23967,7 @@ async function buildApi(opts) {
23911
23967
  bootstrap: opts.bootstrap,
23912
23968
  singleProject: opts.singleProject?.name,
23913
23969
  connectorsHome: opts.connectorsHome,
23970
+ repoSync: opts.repoSync,
23914
23971
  runPoll: opts.runPoll ?? runConnectorPoll
23915
23972
  };
23916
23973
  app.get("/health", async () => {
@@ -24477,6 +24534,7 @@ async function syncOneRepo(r, input) {
24477
24534
  detail: `extracted ${nodes} node${nodes === 1 ? "" : "s"}, ${edges} edge${edges === 1 ? "" : "s"}`,
24478
24535
  lastSyncAt: new Date(input.now?.() ?? Date.now()).toISOString()
24479
24536
  });
24537
+ return true;
24480
24538
  } catch (err) {
24481
24539
  input.onError?.(label, err);
24482
24540
  await cpPostStatus(deps, r.owner, r.name, {
@@ -24484,50 +24542,76 @@ async function syncOneRepo(r, input) {
24484
24542
  detail: scrubToken(err.message).slice(0, 300)
24485
24543
  }).catch(() => {
24486
24544
  });
24545
+ return false;
24487
24546
  } finally {
24488
24547
  if (dir) await (0, import_promises2.rm)(dir, { recursive: true, force: true }).catch(() => {
24489
24548
  });
24490
24549
  }
24491
24550
  }
24492
- async function runRepoSyncPass(input) {
24551
+ async function runRepoSyncPassCounted(input) {
24552
+ const counts = { listed: false, synced: 0, failed: 0 };
24493
24553
  let repos;
24494
24554
  try {
24495
24555
  repos = await cpGet2(`/internal/projects/${input.deps.projectId}/repos`, input.deps);
24496
24556
  } catch (err) {
24497
24557
  input.onSkip?.("(all)", `control plane repo list unreadable \u2014 ${err.message}`);
24498
- return false;
24558
+ return counts;
24499
24559
  }
24500
- if (!Array.isArray(repos)) return false;
24560
+ if (!Array.isArray(repos)) return counts;
24561
+ counts.listed = true;
24501
24562
  for (const r of repos) {
24502
24563
  if (!input.forceResync && !needsSync(r)) continue;
24503
- await syncOneRepo(r, input);
24564
+ if (await syncOneRepo(r, input)) counts.synced++;
24565
+ else counts.failed++;
24504
24566
  }
24505
- return true;
24567
+ return counts;
24506
24568
  }
24507
24569
  async function startRepoSync(input) {
24508
24570
  const intervalMs = input.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
24571
+ const now = () => input.now?.() ?? Date.now();
24509
24572
  let stopped = false;
24510
- let running = false;
24573
+ let inFlight = null;
24574
+ let again = false;
24575
+ let lastPass;
24511
24576
  let bootResyncDone = false;
24512
- const tick = async () => {
24513
- if (stopped || running) return;
24514
- running = true;
24577
+ const run = async () => {
24515
24578
  try {
24516
- const listed = await runRepoSyncPass({ ...input, forceResync: !bootResyncDone });
24517
- if (listed) bootResyncDone = true;
24579
+ do {
24580
+ again = false;
24581
+ const startedAt = new Date(now()).toISOString();
24582
+ const counts = await runRepoSyncPassCounted({ ...input, forceResync: !bootResyncDone });
24583
+ if (counts.listed) bootResyncDone = true;
24584
+ lastPass = { ...counts, startedAt, finishedAt: new Date(now()).toISOString() };
24585
+ } while (again && !stopped);
24518
24586
  } finally {
24519
- running = false;
24587
+ inFlight = null;
24520
24588
  }
24521
24589
  };
24522
- void tick();
24523
- const timer = setInterval(() => {
24524
- void tick();
24525
- }, intervalMs);
24590
+ const tick = () => {
24591
+ if (stopped || inFlight) return;
24592
+ inFlight = run();
24593
+ };
24594
+ tick();
24595
+ const timer = setInterval(tick, intervalMs);
24526
24596
  if (typeof timer.unref === "function") timer.unref();
24527
- return () => {
24597
+ const stop = () => {
24528
24598
  stopped = true;
24529
24599
  clearInterval(timer);
24530
24600
  };
24601
+ return Object.assign(stop, {
24602
+ syncNow: () => {
24603
+ const summary = lastPass ? { lastPass } : {};
24604
+ if (inFlight) {
24605
+ again = true;
24606
+ return { status: "queued", ...summary };
24607
+ }
24608
+ inFlight = run();
24609
+ return { status: "started", ...summary };
24610
+ },
24611
+ settled: async () => {
24612
+ while (inFlight) await inFlight;
24613
+ }
24614
+ });
24531
24615
  }
24532
24616
  async function maybeStartRepoSync(input) {
24533
24617
  const env = input.env ?? process.env;
@@ -25283,6 +25367,7 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
25283
25367
  stopHostedConnectors,
25284
25368
  stopK8sSubstrate,
25285
25369
  stopRepoSync,
25370
+ ...stopRepoSync.syncNow ? { syncRepos: stopRepoSync.syncNow } : {},
25286
25371
  detachEvents,
25287
25372
  status: "active"
25288
25373
  };
@@ -25554,7 +25639,14 @@ async function startDaemon(opts = {}) {
25554
25639
  // through the same resolved home the slot bootstrap read it from, so a
25555
25640
  // daemon given an explicit NEAT_HOME serves status for the same file it
25556
25641
  // polls.
25557
- connectorsHome: home
25642
+ connectorsHome: home,
25643
+ // #1293 — "sync this project's repos now". Looked up per request: the
25644
+ // slot, and with it the trigger, appears once bootstrap has run.
25645
+ repoSync: (name) => slots.get(name)?.syncRepos,
25646
+ // OTLP answers on this listener too, so a host that routes a single
25647
+ // port still delivers a tenant app's spans. Bound late: the receiver
25648
+ // is built below, once the REST bind has succeeded.
25649
+ otlpReceiver: () => otlpApp ?? void 0
25558
25650
  });
25559
25651
  restAddress = await restApp.listen({ port: restPort, host });
25560
25652
  console.log(
@@ -25735,7 +25827,9 @@ async function startDaemon(opts = {}) {
25735
25827
  }
25736
25828
  });
25737
25829
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
25738
- console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
25830
+ console.log(
25831
+ `neatd: OTLP listening on http://${host}:${portFromListenAddress(otlpAddress, otlpPort)}/v1/traces (also at /v1/traces on the REST port)`
25832
+ );
25739
25833
  } catch (err) {
25740
25834
  for (const slot of slots.values()) {
25741
25835
  teardownSlot(slot);