@neat.is/core 0.5.4-dev.20260721 → 0.6.1

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-VR73QNJD.js";
4
+ } from "./chunk-3YUNROLT.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,15 +37,15 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-PEFX3DBR.js";
40
+ } from "./chunk-7C6ZULNU.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
- } from "./chunk-4RU3AAOI.js";
43
+ } from "./chunk-MVINCLQM.js";
44
44
  import {
45
45
  buildOtelReceiver,
46
46
  logSpanHandler,
47
47
  parseOtlpRequest
48
- } from "./chunk-I4NZ7PSN.js";
48
+ } from "./chunk-BZ3AJVAC.js";
49
49
  export {
50
50
  ProjectNameCollisionError,
51
51
  addProject,
package/dist/neatd.cjs CHANGED
@@ -342,6 +342,11 @@ function pickEnv(spanAttrs, resourceAttrs) {
342
342
  }
343
343
  return ENV_FALLBACK;
344
344
  }
345
+ function normalizeDbSystem(attrs) {
346
+ const raw = attrs["db.system"];
347
+ if (typeof raw !== "string") return void 0;
348
+ return raw === "mongoose" ? "mongodb" : raw;
349
+ }
345
350
  function messagingDestinationOf(attrs) {
346
351
  for (const key of ["messaging.destination.name", "messaging.destination"]) {
347
352
  const v = attrs[key];
@@ -396,7 +401,7 @@ function parseOtlpRequest(body) {
396
401
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
397
402
  env: pickEnv(attrs, resourceAttrs),
398
403
  attributes: attrs,
399
- dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
404
+ dbSystem: normalizeDbSystem(attrs),
400
405
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
401
406
  dbCollection: typeof attrs["db.collection.name"] === "string" ? attrs["db.collection.name"] : typeof attrs["db.mongodb.collection"] === "string" ? attrs["db.mongodb.collection"] : void 0,
402
407
  messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
@@ -5300,6 +5305,7 @@ init_cjs_shims();
5300
5305
  var import_node_path22 = __toESM(require("path"), 1);
5301
5306
  var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5302
5307
  var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5308
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5303
5309
  var import_types11 = require("@neat.is/types");
5304
5310
  var PARSE_CHUNK2 = 16384;
5305
5311
  function parseSource2(parser, source) {
@@ -5312,6 +5318,11 @@ function makeJsParser2() {
5312
5318
  p.setLanguage(import_tree_sitter_javascript2.default);
5313
5319
  return p;
5314
5320
  }
5321
+ function makePyParser2() {
5322
+ const p = new import_tree_sitter2.default();
5323
+ p.setLanguage(import_tree_sitter_python2.default);
5324
+ return p;
5325
+ }
5315
5326
  var ROUTER_METHODS = /* @__PURE__ */ new Set([
5316
5327
  "get",
5317
5328
  "post",
@@ -5324,6 +5335,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5324
5335
  ]);
5325
5336
  var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5326
5337
  var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5338
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
5327
5339
  function canonicalizeTemplate(raw) {
5328
5340
  let p = raw.split("?")[0].split("#")[0];
5329
5341
  if (!p.startsWith("/")) p = "/" + p;
@@ -5551,8 +5563,102 @@ function nextRoutesFromFile(source, relFile, parser) {
5551
5563
  }
5552
5564
  return [];
5553
5565
  }
5566
+ function pyStaticStringText(node) {
5567
+ if (node.type !== "string") return null;
5568
+ for (let i = 0; i < node.namedChildCount; i++) {
5569
+ const child = node.namedChild(i);
5570
+ if (child?.type === "interpolation") return null;
5571
+ if (child?.type === "string_content") return child.text;
5572
+ }
5573
+ return "";
5574
+ }
5575
+ function keywordArrayStrings(argsNode, key) {
5576
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
5577
+ const arg = argsNode.namedChild(i);
5578
+ if (arg?.type !== "keyword_argument") continue;
5579
+ if (arg.childForFieldName("name")?.text !== key) continue;
5580
+ const val = arg.childForFieldName("value");
5581
+ if (!val || val.type !== "list") return [];
5582
+ const out = [];
5583
+ for (let j = 0; j < val.namedChildCount; j++) {
5584
+ const el = val.namedChild(j);
5585
+ if (el?.type === "string") {
5586
+ const s = pyStaticStringText(el);
5587
+ if (s) out.push(s);
5588
+ }
5589
+ }
5590
+ return out;
5591
+ }
5592
+ return [];
5593
+ }
5594
+ function collectApiRouterPrefixes(root) {
5595
+ const prefixes = /* @__PURE__ */ new Map();
5596
+ walk(root, (node) => {
5597
+ if (node.type !== "assignment") return;
5598
+ const right = node.childForFieldName("right");
5599
+ if (!right || right.type !== "call") return;
5600
+ const fn = right.childForFieldName("function");
5601
+ if (!fn) return;
5602
+ const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
5603
+ if (ctor !== "APIRouter") return;
5604
+ const left = node.childForFieldName("left");
5605
+ if (!left || left.type !== "identifier") return;
5606
+ const args = right.childForFieldName("arguments");
5607
+ if (!args) return;
5608
+ for (let i = 0; i < args.namedChildCount; i++) {
5609
+ const arg = args.namedChild(i);
5610
+ if (arg?.type !== "keyword_argument") continue;
5611
+ if (arg.childForFieldName("name")?.text !== "prefix") continue;
5612
+ const val = arg.childForFieldName("value");
5613
+ const p = val ? pyStaticStringText(val) : null;
5614
+ if (p !== null) prefixes.set(left.text, p);
5615
+ }
5616
+ });
5617
+ return prefixes;
5618
+ }
5619
+ function fastapiRoutesFromSource(source, parser) {
5620
+ const tree = parseSource2(parser, source);
5621
+ const prefixes = collectApiRouterPrefixes(tree.rootNode);
5622
+ const out = [];
5623
+ walk(tree.rootNode, (node) => {
5624
+ if (node.type !== "decorator") return;
5625
+ const call = node.namedChild(0);
5626
+ if (!call || call.type !== "call") return;
5627
+ const fn = call.childForFieldName("function");
5628
+ if (!fn || fn.type !== "attribute") return;
5629
+ const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5630
+ if (!method) return;
5631
+ const isVerb = FASTAPI_METHODS.has(method);
5632
+ if (!isVerb && method !== "api_route") return;
5633
+ const args = call.childForFieldName("arguments");
5634
+ const first = args?.namedChild(0);
5635
+ if (!first || first.type !== "string") return;
5636
+ const rawPath = pyStaticStringText(first);
5637
+ if (rawPath === null || !rawPath.startsWith("/")) return;
5638
+ const obj = fn.childForFieldName("object")?.text;
5639
+ const prefix = obj ? prefixes.get(obj) ?? "" : "";
5640
+ const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5641
+ const line = node.startPosition.row + 1;
5642
+ if (isVerb) {
5643
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5644
+ return;
5645
+ }
5646
+ const methods = keywordArrayStrings(args, "methods");
5647
+ const list = methods.length > 0 ? methods : ["ALL"];
5648
+ for (const m of list) {
5649
+ out.push({
5650
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5651
+ pathTemplate,
5652
+ line,
5653
+ framework: "fastapi"
5654
+ });
5655
+ }
5656
+ });
5657
+ return out;
5658
+ }
5554
5659
  async function addRoutes(graph, services) {
5555
5660
  const jsParser = makeJsParser2();
5661
+ const pyParser = makePyParser2();
5556
5662
  let nodesAdded = 0;
5557
5663
  let edgesAdded = 0;
5558
5664
  for (const service of services) {
@@ -5564,15 +5670,20 @@ async function addRoutes(graph, services) {
5564
5670
  const hasFastify = deps["fastify"] !== void 0;
5565
5671
  const hasHono = deps["hono"] !== void 0;
5566
5672
  const hasNext = deps["next"] !== void 0;
5567
- if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
5673
+ const hasFastapi = deps["fastapi"] !== void 0;
5674
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
5568
5675
  const files = await loadSourceFiles(service.dir);
5569
5676
  for (const file of files) {
5570
5677
  if (isTestPath(file.path)) continue;
5571
- if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
5678
+ const ext = import_node_path22.default.extname(file.path);
5679
+ const isPy = ext === ".py";
5680
+ if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy) continue;
5572
5681
  const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5573
5682
  let routes;
5574
5683
  try {
5575
- if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5684
+ if (isPy) {
5685
+ routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5686
+ } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5576
5687
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5577
5688
  } else if (hasExpress || hasFastify || hasHono) {
5578
5689
  routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
@@ -5756,7 +5867,7 @@ init_cjs_shims();
5756
5867
  var import_node_path24 = __toESM(require("path"), 1);
5757
5868
  var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5758
5869
  var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5759
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5870
+ var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
5760
5871
  var import_types13 = require("@neat.is/types");
5761
5872
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
5762
5873
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
@@ -5811,14 +5922,14 @@ function makeJsParser3() {
5811
5922
  p.setLanguage(import_tree_sitter_javascript3.default);
5812
5923
  return p;
5813
5924
  }
5814
- function makePyParser2() {
5925
+ function makePyParser3() {
5815
5926
  const p = new import_tree_sitter3.default();
5816
- p.setLanguage(import_tree_sitter_python2.default);
5927
+ p.setLanguage(import_tree_sitter_python3.default);
5817
5928
  return p;
5818
5929
  }
5819
5930
  async function addHttpCallEdges(graph, services) {
5820
5931
  const jsParser = makeJsParser3();
5821
- const pyParser = makePyParser2();
5932
+ const pyParser = makePyParser3();
5822
5933
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5823
5934
  let nodesAdded = 0;
5824
5935
  let edgesAdded = 0;
@@ -9774,6 +9885,42 @@ function registerRoutes(scope, ctx) {
9774
9885
  return reply.code(500).send({ error: err.message });
9775
9886
  }
9776
9887
  });
9888
+ scope.get("/instrumentation", async (req2, reply) => {
9889
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
9890
+ if (!proj) return;
9891
+ if (!proj.scanPath) {
9892
+ return { engaged: null };
9893
+ }
9894
+ try {
9895
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
9896
+ const uninstrumented = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
9897
+ if (state.hookFiles.length === 0) {
9898
+ return {
9899
+ engaged: false,
9900
+ diagnosis: {
9901
+ reason: "No instrumented entry point found \u2014 NEAT hasn't written an OTel init hook for this project.",
9902
+ fixCommand: "neat init",
9903
+ detail: "Run `neat init` so NEAT writes the instrumentation hook next to your entry point, then run your app."
9904
+ }
9905
+ };
9906
+ }
9907
+ if (uninstrumented.length > 0) {
9908
+ const names = uninstrumented.map((u) => u.library);
9909
+ const more = names.length > 1 ? ` (and ${names.length - 1} more)` : "";
9910
+ return {
9911
+ engaged: false,
9912
+ diagnosis: {
9913
+ reason: `\`${names[0]}\`${more} isn't in the auto-instrumentation set, so spans from it won't reach the graph.`,
9914
+ fixCommand: "neat extend",
9915
+ detail: `Uninstrumented on your hot path: ${names.join(", ")}. Run \`neat extend\` to wire the missing instrumentation.`
9916
+ }
9917
+ };
9918
+ }
9919
+ return { engaged: true };
9920
+ } catch {
9921
+ return { engaged: null };
9922
+ }
9923
+ });
9777
9924
  scope.post("/extend/apply", async (req2, reply) => {
9778
9925
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
9779
9926
  if (!proj) return;
@@ -11636,15 +11783,21 @@ var PROVIDER_DISPATCH = {
11636
11783
  resolveTarget: createCloudflareResolveTarget(config, graph)
11637
11784
  };
11638
11785
  },
11639
- // GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
11640
- // token live" endpoint. 200 on a valid token, 401 on an invalid one.
11786
+ // GET /accounts/{accountId}/tokens/verify — the *account-scoped* token-verify
11787
+ // endpoint. A Workers connector token is scoped to the account, and the
11788
+ // user-level `GET /user/tokens/verify` returns 401 "Invalid API Token" for
11789
+ // such a token even though it authenticates fine against the account's own
11790
+ // resources (confirmed live). Probing the account-scoped verify endpoint —
11791
+ // `accountId` is already required for this provider — returns 200
11792
+ // `{status:"active"}` for a working token and 401 for a bad one, so a valid
11793
+ // Workers token is no longer falsely rejected at `neat connector add`.
11641
11794
  validate({ credentials, options, fetchImpl }) {
11642
11795
  const cfg = options;
11643
11796
  const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
11644
11797
  return authProbe({
11645
11798
  provider: "cloudflare",
11646
11799
  accountKey: cfg.accountId ?? "validate",
11647
- url: `${baseUrl}/user/tokens/verify`,
11800
+ url: `${baseUrl}/accounts/${cfg.accountId ?? ""}/tokens/verify`,
11648
11801
  token: String(credentials.apiToken ?? ""),
11649
11802
  ...fetchImpl ? { fetchImpl } : {}
11650
11803
  });