@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/cli.cjs CHANGED
@@ -51,6 +51,10 @@ function assertBindAuthority(host, token) {
51
51
  if (isLoopbackHost2(host)) return;
52
52
  throw new BindAuthorityError(host);
53
53
  }
54
+ function isBearerDelegated(req) {
55
+ const config = req.routeOptions?.config;
56
+ return config?.bearerDelegated === true;
57
+ }
54
58
  function mountBearerAuth(app, opts) {
55
59
  if (!opts.token || opts.token.length === 0) return;
56
60
  if (opts.trustProxy) return;
@@ -66,6 +70,10 @@ function mountBearerAuth(app, opts) {
66
70
  done();
67
71
  return;
68
72
  }
73
+ if (isBearerDelegated(req)) {
74
+ done();
75
+ return;
76
+ }
69
77
  if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
70
78
  done();
71
79
  return;
@@ -99,7 +107,7 @@ function readAuthEnv(env = process.env) {
99
107
  publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
100
108
  };
101
109
  }
102
- var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
110
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN, BEARER_DELEGATED;
103
111
  var init_auth = __esm({
104
112
  "src/auth.ts"() {
105
113
  "use strict";
@@ -129,6 +137,7 @@ var init_auth = __esm({
129
137
  PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
130
138
  `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
131
139
  );
140
+ BEARER_DELEGATED = { bearerDelegated: true };
132
141
  }
133
142
  });
134
143
 
@@ -3527,6 +3536,10 @@ function languageForPath(relPath) {
3527
3536
  return void 0;
3528
3537
  }
3529
3538
  }
3539
+ var currentSource;
3540
+ function setExtractionSource(source) {
3541
+ currentSource = source;
3542
+ }
3530
3543
  function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3531
3544
  let nodesAdded = 0;
3532
3545
  let edgesAdded = 0;
@@ -3539,10 +3552,16 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3539
3552
  service: serviceName,
3540
3553
  path: relPath,
3541
3554
  ...language ? { language } : {},
3555
+ ...currentSource ? { source: currentSource } : {},
3542
3556
  discoveredVia: "static"
3543
3557
  };
3544
3558
  graph.addNode(fileNodeId, node);
3545
3559
  nodesAdded++;
3560
+ } else if (currentSource) {
3561
+ const existing = graph.getNodeAttributes(fileNodeId);
3562
+ if (existing.source !== currentSource) {
3563
+ graph.replaceNodeAttributes(fileNodeId, { ...existing, source: currentSource });
3564
+ }
3546
3565
  }
3547
3566
  const containsId = (0, import_types5.extractedEdgeId)(serviceNodeId, fileNodeId, import_types5.EdgeType.CONTAINS);
3548
3567
  if (!graph.hasEdge(containsId)) {
@@ -16603,10 +16622,12 @@ init_cjs_shims();
16603
16622
  var import_node_fs34 = require("fs");
16604
16623
  var import_node_path69 = __toESM(require("path"), 1);
16605
16624
  var import_types59 = require("@neat.is/types");
16606
- function dropOrphanedFileNodes(graph) {
16625
+ function dropOrphanedFileNodes(graph, source) {
16607
16626
  const orphans = [];
16608
16627
  graph.forEachNode((id, attrs) => {
16609
- if (attrs.type !== import_types59.NodeType.FileNode) return;
16628
+ const node = attrs;
16629
+ if (node.type !== import_types59.NodeType.FileNode) return;
16630
+ if (source && node.source !== source) return;
16610
16631
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
16611
16632
  orphans.push(id);
16612
16633
  }
@@ -16627,12 +16648,23 @@ function retireEdgesByFile(graph, file) {
16627
16648
  dropOrphanedFileNodes(graph);
16628
16649
  return toDrop.length;
16629
16650
  }
16630
- function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16651
+ function outOfScope(graph, edge, source) {
16652
+ if (!source) return false;
16653
+ for (const endpoint2 of [edge.source, edge.target]) {
16654
+ if (!graph.hasNode(endpoint2)) continue;
16655
+ const node = graph.getNodeAttributes(endpoint2);
16656
+ if (node.type !== import_types59.NodeType.FileNode) continue;
16657
+ return node.source !== source;
16658
+ }
16659
+ return false;
16660
+ }
16661
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], source) {
16631
16662
  const toDrop = [];
16632
16663
  const bases = [scanPath, ...serviceDirs];
16633
16664
  graph.forEachEdge((id, attrs) => {
16634
16665
  const edge = attrs;
16635
16666
  if (edge.provenance !== import_types59.Provenance.EXTRACTED) return;
16667
+ if (outOfScope(graph, edge, source)) return;
16636
16668
  const evidenceFile = edge.evidence?.file;
16637
16669
  if (!evidenceFile) return;
16638
16670
  if (import_node_path69.default.isAbsolute(evidenceFile)) {
@@ -16643,7 +16675,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16643
16675
  if (!found) toDrop.push(id);
16644
16676
  });
16645
16677
  for (const id of toDrop) graph.dropEdge(id);
16646
- dropOrphanedFileNodes(graph);
16678
+ dropOrphanedFileNodes(graph, source);
16647
16679
  return toDrop.length;
16648
16680
  }
16649
16681
 
@@ -16652,6 +16684,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16652
16684
  await ensureCompatLoaded();
16653
16685
  drainExtractionErrors();
16654
16686
  drainSkippedFiles();
16687
+ setExtractionSource(opts.source);
16655
16688
  const services = await discoverServices(scanPath);
16656
16689
  const phase1Nodes = addServiceNodes(graph, services);
16657
16690
  await addServiceAliases(graph, scanPath, services);
@@ -16672,7 +16705,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16672
16705
  const ghostsRetired = retireExtractedEdgesByMissingFile(
16673
16706
  graph,
16674
16707
  scanPath,
16675
- services.map((s) => s.dir)
16708
+ services.map((s) => s.dir),
16709
+ opts.source
16676
16710
  );
16677
16711
  const frontiersPromoted = promoteFrontierNodes(graph);
16678
16712
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
@@ -16707,6 +16741,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16707
16741
  );
16708
16742
  }
16709
16743
  }
16744
+ setExtractionSource(void 0);
16710
16745
  const result = {
16711
16746
  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,
16712
16747
  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,
@@ -24226,6 +24261,19 @@ function registerRoutes(scope, ctx) {
24226
24261
  });
24227
24262
  }
24228
24263
  });
24264
+ scope.post("/repo-sync", async (req, reply) => {
24265
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
24266
+ if (!proj) return;
24267
+ const syncNow = ctx.repoSync?.(proj.name);
24268
+ if (!syncNow) {
24269
+ return reply.code(404).send({
24270
+ error: "repo-sync is not running for this project",
24271
+ project: proj.name,
24272
+ hint: "Only a hosted daemon syncs bound repos. On a local daemon, POST /graph/scan re-extracts the project path."
24273
+ });
24274
+ }
24275
+ return reply.code(202).send({ project: proj.name, ...syncNow() });
24276
+ });
24229
24277
  scope.post("/graph/scan", async (req, reply) => {
24230
24278
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
24231
24279
  if (!proj) return;
@@ -24473,6 +24521,39 @@ function registerRoutes(scope, ctx) {
24473
24521
  }
24474
24522
  });
24475
24523
  }
24524
+ var OTLP_BODY_LIMIT = 16 * 1024 * 1024;
24525
+ function otlpOnRest(getReceiver) {
24526
+ return async function plugin(scope) {
24527
+ scope.removeAllContentTypeParsers();
24528
+ scope.addContentTypeParser(
24529
+ "*",
24530
+ { parseAs: "buffer", bodyLimit: OTLP_BODY_LIMIT },
24531
+ (_req, body, done) => done(null, body)
24532
+ );
24533
+ const relay = async (req, reply) => {
24534
+ const receiver = getReceiver();
24535
+ if (!receiver) {
24536
+ return reply.code(503).header("retry-after", "1").send({ error: "OTLP receiver is starting" });
24537
+ }
24538
+ const headers = { ...req.headers };
24539
+ delete headers["content-length"];
24540
+ delete headers["transfer-encoding"];
24541
+ delete headers.connection;
24542
+ const res = await receiver.inject({
24543
+ method: "POST",
24544
+ url: req.url,
24545
+ headers,
24546
+ payload: req.body
24547
+ });
24548
+ const contentType = res.headers["content-type"];
24549
+ if (contentType !== void 0) void reply.header("content-type", contentType);
24550
+ return reply.code(res.statusCode).send(res.rawPayload);
24551
+ };
24552
+ const route = { config: BEARER_DELEGATED, bodyLimit: OTLP_BODY_LIMIT };
24553
+ scope.post("/v1/traces", route, relay);
24554
+ scope.post("/projects/:project/v1/traces", route, relay);
24555
+ };
24556
+ }
24476
24557
  async function buildApi(opts) {
24477
24558
  const app = (0, import_fastify2.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
24478
24559
  await app.register(import_cors.default, { origin: true });
@@ -24491,6 +24572,7 @@ async function buildApi(opts) {
24491
24572
  authProxy: trustProxy === true,
24492
24573
  requiresAuth
24493
24574
  }));
24575
+ if (opts.otlpReceiver) await app.register(otlpOnRest(opts.otlpReceiver));
24494
24576
  const startedAt = opts.startedAt ?? Date.now();
24495
24577
  const registry = buildLegacyRegistry(opts);
24496
24578
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -24521,6 +24603,7 @@ async function buildApi(opts) {
24521
24603
  bootstrap: opts.bootstrap,
24522
24604
  singleProject: opts.singleProject?.name,
24523
24605
  connectorsHome: opts.connectorsHome,
24606
+ repoSync: opts.repoSync,
24524
24607
  runPoll: opts.runPoll ?? runConnectorPoll
24525
24608
  };
24526
24609
  app.get("/health", async () => {
@@ -30601,6 +30684,52 @@ async function waitForFirstProject(fetchImpl, cpUrl, accessToken, deps) {
30601
30684
  if (deps.signal?.aborted) return { kind: "aborted" };
30602
30685
  }
30603
30686
  }
30687
+ function projectNameForRepo(repo) {
30688
+ const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
30689
+ return slug || "repo";
30690
+ }
30691
+ async function detailOf(res) {
30692
+ const body = await res.text().catch(() => "");
30693
+ try {
30694
+ const parsed = JSON.parse(body);
30695
+ if (parsed.error) return parsed.error;
30696
+ } catch {
30697
+ }
30698
+ return body.trim() || `HTTP ${res.status}`;
30699
+ }
30700
+ async function createProjectForRepo(fetchImpl, cpUrl, accessToken, name) {
30701
+ const headers = { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" };
30702
+ let project;
30703
+ try {
30704
+ const res = await fetchImpl(`${cpUrl}/me/projects`, {
30705
+ method: "POST",
30706
+ headers,
30707
+ body: JSON.stringify({ name }),
30708
+ signal: AbortSignal.timeout(CP_TIMEOUT_MS)
30709
+ });
30710
+ if (!res.ok) return { ok: false, reason: "not-created", detail: await detailOf(res) };
30711
+ project = await res.json();
30712
+ } catch (err) {
30713
+ return { ok: false, reason: "not-created", detail: err instanceof Error ? err.message : String(err) };
30714
+ }
30715
+ try {
30716
+ const res = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/provision`, {
30717
+ method: "POST",
30718
+ headers,
30719
+ signal: AbortSignal.timeout(CP_TIMEOUT_MS)
30720
+ });
30721
+ if (res.status === 402) return { ok: false, reason: "needs-plan", project, detail: await detailOf(res) };
30722
+ if (!res.ok) return { ok: false, reason: "not-provisioned", project, detail: await detailOf(res) };
30723
+ } catch (err) {
30724
+ return {
30725
+ ok: false,
30726
+ reason: "not-provisioned",
30727
+ project,
30728
+ detail: err instanceof Error ? err.message : String(err)
30729
+ };
30730
+ }
30731
+ return { ok: true, project };
30732
+ }
30604
30733
  function bindUrl(webUrl, projectId) {
30605
30734
  return `${webUrl}/config/repos?project=${encodeURIComponent(projectId)}`;
30606
30735
  }
@@ -30626,7 +30755,8 @@ async function runPostLoginConnect(input) {
30626
30755
  const repo = await detect6(cwd);
30627
30756
  out("");
30628
30757
  const cmd = commandPrefix();
30629
- const syncLine = `${cmd} sync --to ${input.project.name}`;
30758
+ const pushCmd = input.endpoint ? `${cmd} sync --to ${input.endpoint}${input.pushToken ? ` --token ${input.pushToken}` : ""}` : null;
30759
+ const pushInto = (label) => pushCmd ? [` ${pushCmd}`] : [` Its daemon URL and token are on ${label} in the console: ${input.webUrl}`];
30630
30760
  if (!repo) {
30631
30761
  out("This directory has no GitHub remote, so it can't be bound to the project.");
30632
30762
  const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
@@ -30636,16 +30766,17 @@ async function runPostLoginConnect(input) {
30636
30766
  out("");
30637
30767
  if (built) {
30638
30768
  out(`Now push it to ${input.project.name}:`);
30639
- out(` ${syncLine}`);
30769
+ for (const line of pushInto(input.project.name)) out(line);
30640
30770
  } else {
30641
30771
  out(`When you're ready, build it and push it to ${input.project.name}:`);
30642
30772
  out(` ${cmd}`);
30643
- out(` ${syncLine}`);
30773
+ for (const line of pushInto(input.project.name)) out(line);
30644
30774
  }
30645
30775
  out(` Manage projects and repos: ${input.webUrl}`);
30646
30776
  return;
30647
30777
  }
30648
- out(` Push its graph instead: ${syncLine}`);
30778
+ out(` Push its graph to ${input.project.name}:`);
30779
+ for (const line of pushInto(input.project.name)) out(line);
30649
30780
  out(` Manage projects and repos: ${input.webUrl}`);
30650
30781
  return;
30651
30782
  }
@@ -30658,39 +30789,87 @@ async function runPostLoginConnect(input) {
30658
30789
  out(` See the graph: ${input.webUrl}`);
30659
30790
  return;
30660
30791
  }
30661
- out(`${slug} isn't part of ${input.project.name} yet \u2014 logging in connected your account, not this repo.`);
30792
+ out(`${slug} isn't in ${input.project.name} \u2014 logging in connected your account, not this repo.`);
30793
+ out("");
30794
+ const suggested = projectNameForRepo(repo);
30795
+ out("Where should this repo go?");
30662
30796
  out("");
30663
- const installed = input.me?.github?.installed;
30664
- const bindLine = installed === false ? "1) Install the GitHub App and bind this repo \u2014 hosted NEAT keeps it in sync" : installed === true ? "1) Bind this repo \u2014 hosted NEAT clones and extracts it, and keeps it in sync" : "1) Bind this repo through the console \u2014 hosted NEAT keeps it in sync";
30665
- out(bindLine);
30666
- out(`2) Push the graph on this machine now \u2014 a snapshot, not a subscription`);
30797
+ out(`1) A project of its own, called ${suggested}`);
30798
+ out(`2) ${input.project.name} \u2014 the project this login connected to`);
30799
+ for (const line of sharedProjectCaveat(bound)) out(` ${line}`);
30667
30800
  out("3) Neither, for now");
30801
+ out("");
30668
30802
  const answer = readLine ? (await readLine("Choose 1, 2 or 3 (Enter skips): "))?.trim() : void 0;
30669
30803
  if (answer === "1") {
30670
- const url = bindUrl(input.webUrl, input.project.id);
30671
- const opened = (d.openBrowser ?? (() => false))(url);
30672
30804
  out("");
30673
- out(opened ? `Opened ${url}` : `Open this to bind the repo:
30674
- ${url}`);
30805
+ const outcome = await createProjectForRepo(fetchImpl, input.cpUrl, input.accessToken, suggested);
30806
+ reportCreate(outcome, input.webUrl, cmd, input.project.name, out);
30675
30807
  return;
30676
30808
  }
30677
30809
  if (answer === "2") {
30678
- const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
30679
30810
  out("");
30680
- if (!hasGraph) {
30681
- out("There is no local graph to push yet. Build one, then push it:");
30682
- out(` ${cmd}`);
30683
- out(` ${syncLine}`);
30684
- return;
30811
+ out(`Two ways into ${input.project.name} \u2014 either is fine, and you can do both:`);
30812
+ out("");
30813
+ const installed = input.me?.github?.installed;
30814
+ out(
30815
+ installed === false ? " Bind it (install the GitHub App first) \u2014 hosted NEAT keeps it in sync:" : installed === true ? " Bind it \u2014 hosted NEAT clones and extracts it, and keeps it in sync:" : " Bind it through the console \u2014 hosted NEAT keeps it in sync:"
30816
+ );
30817
+ out(` ${bindUrl(input.webUrl, input.project.id)}`);
30818
+ out("");
30819
+ const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
30820
+ if (hasGraph) {
30821
+ out(" Or push the graph on this machine now \u2014 a snapshot, not a subscription:");
30822
+ for (const line of pushInto(input.project.name)) out(` ${line}`);
30823
+ } else {
30824
+ out(" Or build a graph here and push it \u2014 a snapshot, not a subscription:");
30825
+ out(` ${cmd}`);
30826
+ for (const line of pushInto(input.project.name)) out(` ${line}`);
30685
30827
  }
30686
- out("Push the local graph with:");
30687
- out(` ${syncLine}`);
30688
30828
  return;
30689
30829
  }
30690
30830
  out("");
30691
30831
  out("No problem \u2014 when you want this repo in the graph:");
30692
- out(` Bind it: ${bindUrl(input.webUrl, input.project.id)}`);
30693
- out(` Or push it: ${syncLine}`);
30832
+ out(` Its own project: ${input.webUrl}`);
30833
+ out(` Or into ${input.project.name}: ${bindUrl(input.webUrl, input.project.id)}`);
30834
+ }
30835
+ function sharedProjectCaveat(bound) {
30836
+ const hazard = "Two repos in one project can currently retire each other's files (#1294),";
30837
+ if (!bound) return [hazard, "so a graph there may end up short of code until that is fixed."];
30838
+ if (bound.length === 0) return ["Nothing else is bound there yet.", `Adding a second repo later would hit #1294 \u2014 ${hazard.toLowerCase()}`];
30839
+ const names = bound.map((r) => `${r.owner}/${r.name}`).join(", ");
30840
+ return [
30841
+ `${bound.length} already there: ${names}.`,
30842
+ hazard,
30843
+ "so both graphs may end up short of code until that is fixed."
30844
+ ];
30845
+ }
30846
+ function reportCreate(outcome, webUrl, cmd, connectedTo, out) {
30847
+ if (outcome.ok) {
30848
+ out(`Created ${outcome.project.name} and started its daemon.`);
30849
+ out("");
30850
+ out(`This CLI is still pointed at ${connectedTo}. Switch it to the new project with:`);
30851
+ out(` ${cmd} login --project ${outcome.project.name}`);
30852
+ out("");
30853
+ out("Then bind this repo to it:");
30854
+ out(` ${bindUrl(webUrl, outcome.project.id)}`);
30855
+ return;
30856
+ }
30857
+ if (outcome.reason === "needs-plan") {
30858
+ out(`Created ${outcome.project.name}, but starting a daemon for it needs a plan.`);
30859
+ out(` Choose one: ${webUrl}/checkout`);
30860
+ out("");
30861
+ out("The project is made and keeps its name, so there's nothing to redo here afterwards.");
30862
+ return;
30863
+ }
30864
+ if (outcome.reason === "not-provisioned") {
30865
+ out(`Created ${outcome.project.name}, but its daemon didn't start: ${outcome.detail}`);
30866
+ out("");
30867
+ out("The project exists \u2014 start it from the console rather than making a second one:");
30868
+ out(` ${webUrl}`);
30869
+ return;
30870
+ }
30871
+ out(`Couldn't create the project: ${outcome.detail}`);
30872
+ out(`Nothing was made, so this is safe to retry, or do it in the console: ${webUrl}`);
30694
30873
  }
30695
30874
 
30696
30875
  // src/login-sso.ts
@@ -30927,6 +31106,10 @@ async function runSsoLogin(opts, deps) {
30927
31106
  webUrl: opts.webUrl,
30928
31107
  accessToken,
30929
31108
  project: { id: project.id, name: project.name },
31109
+ // What a push actually needs: `sync --to` takes the daemon's URL, and its
31110
+ // token comes from the flag rather than the profile (#1302).
31111
+ endpoint: cred.endpoint,
31112
+ pushToken: cred.authToken,
30930
31113
  me,
30931
31114
  deps: {
30932
31115
  fetchImpl,