@neat.is/core 0.7.8 → 0.7.10

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.
@@ -3,7 +3,7 @@ import {
3
3
  mountBearerAuth,
4
4
  readAuthEnv,
5
5
  tableFromSqlStatement
6
- } from "./chunk-Y43UCVZS.js";
6
+ } from "./chunk-UUYCTH2E.js";
7
7
 
8
8
  // src/graph.ts
9
9
  import GraphDefault from "graphology";
@@ -963,6 +963,7 @@ import { parse as parseYaml } from "yaml";
963
963
  import { extractedEdgeId } from "@neat.is/types";
964
964
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
965
965
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
966
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
966
967
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
967
968
  "node_modules",
968
969
  ".git",
@@ -1002,6 +1003,7 @@ async function isPythonVenvDir(dir) {
1002
1003
  function isConfigFile(name) {
1003
1004
  const ext = path3.extname(name);
1004
1005
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1006
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
1005
1007
  if (name === ".env" || name.startsWith(".env.")) {
1006
1008
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1007
1009
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -3599,10 +3601,15 @@ function resolveDistToSrc(absFilepath, line) {
3599
3601
  }
3600
3602
  if (!entry) return null;
3601
3603
  try {
3602
- const pos = entry.consumer.originalPositionFor({
3603
- line: line !== void 0 && Number.isFinite(line) ? line : 1,
3604
- column: 0
3605
- });
3604
+ const queryLine = line !== void 0 && Number.isFinite(line) ? line : 1;
3605
+ let pos = entry.consumer.originalPositionFor({ line: queryLine, column: 0 });
3606
+ if (!pos || !pos.source) {
3607
+ pos = entry.consumer.originalPositionFor({
3608
+ line: queryLine,
3609
+ column: 0,
3610
+ bias: sourceMapJs.SourceMapConsumer.LEAST_UPPER_BOUND
3611
+ });
3612
+ }
3606
3613
  if (!pos || !pos.source) return null;
3607
3614
  const root = entry.consumer.sourceRoot ?? "";
3608
3615
  const resolved = path8.resolve(entry.dir, root, pos.source);
@@ -3611,6 +3618,9 @@ function resolveDistToSrc(absFilepath, line) {
3611
3618
  return null;
3612
3619
  }
3613
3620
  }
3621
+ function hasAdjacentSourceMap(absFilepath) {
3622
+ return sourceMapCache.get(absFilepath) != null;
3623
+ }
3614
3624
  function callSiteFromSpan(span, serviceNode, scanPath) {
3615
3625
  const filepath = codeFilepathOf(span.attributes);
3616
3626
  if (filepath === void 0) return null;
@@ -3626,7 +3636,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
3626
3636
  }
3627
3637
  const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
3628
3638
  if (!relPath) return null;
3629
- if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
3639
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && !hasAdjacentSourceMap(abs) && serviceNode?.name) {
3630
3640
  warnNoSourceMaps(serviceNode.name);
3631
3641
  }
3632
3642
  const fn = codeFunctionOf(span.attributes);
@@ -3902,7 +3912,7 @@ function resolveServiceId(graph, host, env) {
3902
3912
  function frontierIdFor(host) {
3903
3913
  return frontierId(host);
3904
3914
  }
3905
- function ensureServiceNode(graph, serviceName, env) {
3915
+ function resolveFusedServiceId(graph, serviceName, env) {
3906
3916
  const id = serviceId(serviceName, env);
3907
3917
  if (graph.hasNode(id)) return id;
3908
3918
  const wanted = serviceName.toLowerCase();
@@ -3912,17 +3922,21 @@ function ensureServiceNode(graph, serviceName, env) {
3912
3922
  if (svc.discoveredVia === "otel") return false;
3913
3923
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
3914
3924
  });
3915
- if (extractedId) return extractedId;
3925
+ return extractedId ?? id;
3926
+ }
3927
+ function ensureServiceNode(graph, serviceName, env) {
3928
+ const resolved = resolveFusedServiceId(graph, serviceName, env);
3929
+ if (graph.hasNode(resolved)) return resolved;
3916
3930
  const node = {
3917
- id,
3931
+ id: resolved,
3918
3932
  type: NodeType4.ServiceNode,
3919
3933
  name: serviceName,
3920
3934
  language: "unknown",
3921
3935
  discoveredVia: "otel",
3922
3936
  ...env !== "unknown" ? { env } : {}
3923
3937
  };
3924
- graph.addNode(id, node);
3925
- return id;
3938
+ graph.addNode(resolved, node);
3939
+ return resolved;
3926
3940
  }
3927
3941
  function ensureInfraNode(graph, kind, name, provider) {
3928
3942
  const id = infraId(kind, name);
@@ -4115,8 +4129,23 @@ async function appendErrorEvent(ctx, ev) {
4115
4129
  await fs7.mkdir(path8.dirname(ctx.errorsPath), { recursive: true });
4116
4130
  await fs7.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4117
4131
  }
4132
+ async function appendConnectorIncident(errorsPath, input) {
4133
+ const ev = {
4134
+ id: input.id,
4135
+ timestamp: input.timestamp,
4136
+ service: input.service,
4137
+ traceId: input.id,
4138
+ spanId: input.id,
4139
+ errorType: input.errorType,
4140
+ errorMessage: input.errorMessage,
4141
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
4142
+ affectedNode: input.affectedNode
4143
+ };
4144
+ await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
4145
+ await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4146
+ }
4118
4147
  function incidentAffectedNode(span, graph, scanPath) {
4119
- const sid = serviceId(span.service, span.env);
4148
+ const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : serviceId(span.service, span.env);
4120
4149
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
4121
4150
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
4122
4151
  if (callSite) {
@@ -6047,7 +6076,7 @@ function disambiguate(defs) {
6047
6076
  }
6048
6077
  async function addSymbols(graph, services) {
6049
6078
  const parsers = /* @__PURE__ */ new Map();
6050
- const parserForExt4 = (ext) => {
6079
+ const parserForExt5 = (ext) => {
6051
6080
  const grammar = GRAMMAR_BY_EXT[ext];
6052
6081
  if (!grammar) return null;
6053
6082
  let parser = parsers.get(ext);
@@ -6063,7 +6092,7 @@ async function addSymbols(graph, services) {
6063
6092
  for (const service of services) {
6064
6093
  const files = await loadSourceFiles(service.dir);
6065
6094
  for (const file of files) {
6066
- const parser = parserForExt4(path17.extname(file.path));
6095
+ const parser = parserForExt5(path17.extname(file.path));
6067
6096
  if (!parser) continue;
6068
6097
  const relPath = toPosix(path17.relative(service.dir, file.path));
6069
6098
  let defs;
@@ -6221,7 +6250,7 @@ function stringInner(node) {
6221
6250
  }
6222
6251
  async function addSymbolEdges(graph, services) {
6223
6252
  const parsers = /* @__PURE__ */ new Map();
6224
- const parserForExt4 = (ext) => {
6253
+ const parserForExt5 = (ext) => {
6225
6254
  const grammar = GRAMMAR_BY_EXT[ext];
6226
6255
  if (!grammar) return null;
6227
6256
  let parser = parsers.get(ext);
@@ -6238,7 +6267,7 @@ async function addSymbolEdges(graph, services) {
6238
6267
  const tsPaths = await loadTsPathConfig(service.dir);
6239
6268
  const files = await loadSourceFiles(service.dir);
6240
6269
  for (const file of files) {
6241
- const parser = parserForExt4(path18.extname(file.path));
6270
+ const parser = parserForExt5(path18.extname(file.path));
6242
6271
  if (!parser) continue;
6243
6272
  const relPath = toPosix(path18.relative(service.dir, file.path));
6244
6273
  const fileDir = path18.dirname(file.path);
@@ -6506,7 +6535,7 @@ function firstReferenceLines(root, wanted) {
6506
6535
  }
6507
6536
  async function addServerActions(graph, services) {
6508
6537
  const parsers = /* @__PURE__ */ new Map();
6509
- const parserForExt4 = (ext) => {
6538
+ const parserForExt5 = (ext) => {
6510
6539
  const grammar = GRAMMAR_BY_EXT[ext];
6511
6540
  if (!grammar) return null;
6512
6541
  let parser = parsers.get(ext);
@@ -6529,7 +6558,7 @@ async function addServerActions(graph, services) {
6529
6558
  const files = await loadSourceFiles(service.dir);
6530
6559
  for (const file of files) {
6531
6560
  if (isTestPath(file.path)) continue;
6532
- const parser = parserForExt4(path19.extname(file.path));
6561
+ const parser = parserForExt5(path19.extname(file.path));
6533
6562
  if (!parser) continue;
6534
6563
  const relPath = toPosix(path19.relative(service.dir, file.path));
6535
6564
  let root;
@@ -6592,7 +6621,7 @@ async function addServerActions(graph, services) {
6592
6621
  }
6593
6622
  for (const file of files) {
6594
6623
  if (isTestPath(file.path)) continue;
6595
- const parser = parserForExt4(path19.extname(file.path));
6624
+ const parser = parserForExt5(path19.extname(file.path));
6596
6625
  if (!parser) continue;
6597
6626
  const relPath = toPosix(path19.relative(service.dir, file.path));
6598
6627
  const fileDir = path19.dirname(file.path);
@@ -7567,6 +7596,7 @@ import {
7567
7596
  import path30 from "path";
7568
7597
  import Parser6 from "tree-sitter";
7569
7598
  import JavaScript4 from "tree-sitter-javascript";
7599
+ import TypeScript2 from "tree-sitter-typescript";
7570
7600
  import Python3 from "tree-sitter-python";
7571
7601
  import {
7572
7602
  EdgeType as EdgeType13,
@@ -7622,19 +7652,27 @@ function callsFromSource(source, parser, knownHosts) {
7622
7652
  }
7623
7653
  return out;
7624
7654
  }
7625
- function makeJsParser3() {
7626
- const p = new Parser6();
7627
- p.setLanguage(JavaScript4);
7628
- return p;
7629
- }
7630
- function makePyParser3() {
7631
- const p = new Parser6();
7632
- p.setLanguage(Python3);
7633
- return p;
7655
+ var GRAMMAR_BY_EXT2 = {
7656
+ ".ts": TypeScript2.typescript,
7657
+ ".tsx": TypeScript2.tsx,
7658
+ ".js": JavaScript4,
7659
+ ".jsx": JavaScript4,
7660
+ ".mjs": JavaScript4,
7661
+ ".cjs": JavaScript4,
7662
+ ".py": Python3
7663
+ };
7664
+ function parserForExt(ext, cache) {
7665
+ const grammar = GRAMMAR_BY_EXT2[ext] ?? JavaScript4;
7666
+ let parser = cache.get(grammar);
7667
+ if (!parser) {
7668
+ parser = new Parser6();
7669
+ parser.setLanguage(grammar);
7670
+ cache.set(grammar, parser);
7671
+ }
7672
+ return parser;
7634
7673
  }
7635
7674
  async function addHttpCallEdges(graph, services) {
7636
- const jsParser = makeJsParser3();
7637
- const pyParser = makePyParser3();
7675
+ const parserCache = /* @__PURE__ */ new Map();
7638
7676
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
7639
7677
  let nodesAdded = 0;
7640
7678
  let edgesAdded = 0;
@@ -7643,7 +7681,7 @@ async function addHttpCallEdges(graph, services) {
7643
7681
  const seen = /* @__PURE__ */ new Set();
7644
7682
  for (const file of files) {
7645
7683
  if (isTestPath(file.path)) continue;
7646
- const parser = path30.extname(file.path) === ".py" ? pyParser : jsParser;
7684
+ const parser = parserForExt(path30.extname(file.path), parserCache);
7647
7685
  let sites;
7648
7686
  try {
7649
7687
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -7722,7 +7760,7 @@ function parseSource5(parser, source) {
7722
7760
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
7723
7761
  );
7724
7762
  }
7725
- function makeJsParser4() {
7763
+ function makeJsParser3() {
7726
7764
  const p = new Parser7();
7727
7765
  p.setLanguage(JavaScript5);
7728
7766
  return p;
@@ -7900,7 +7938,7 @@ function findRoute(entries, method, normalizedPath) {
7900
7938
  );
7901
7939
  }
7902
7940
  async function addRouteCallEdges(graph, services) {
7903
- const jsParser = makeJsParser4();
7941
+ const jsParser = makeJsParser3();
7904
7942
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
7905
7943
  const routeIndex = buildRouteIndex(graph);
7906
7944
  if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
@@ -8286,7 +8324,7 @@ import JavaScript6 from "tree-sitter-javascript";
8286
8324
  import { infraId as infraId7 } from "@neat.is/types";
8287
8325
  var FIRESTORE_CLIENT_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase\/firestore['"`]/;
8288
8326
  var FIRESTORE_ADMIN_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase-admin(?:\/firestore)?['"`]/;
8289
- function parserForExt(ext) {
8327
+ function parserForExt2(ext) {
8290
8328
  const p = new Parser8();
8291
8329
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? JavaScript6);
8292
8330
  return p;
@@ -8451,7 +8489,7 @@ function firestoreEndpointsFromFile(file, serviceDir) {
8451
8489
  const hasAdmin = FIRESTORE_ADMIN_IMPORT_RE.test(file.content);
8452
8490
  if (!hasClient && !hasAdmin) return [];
8453
8491
  const fileSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
8454
- const tree = parseSource3(parserForExt(path37.extname(file.path)), file.content);
8492
+ const tree = parseSource3(parserForExt2(path37.extname(file.path)), file.content);
8455
8493
  const clientVars = firestoreClientVars(tree.rootNode);
8456
8494
  const collLine = /* @__PURE__ */ new Map();
8457
8495
  const writes = /* @__PURE__ */ new Map();
@@ -8866,7 +8904,7 @@ import Python4 from "tree-sitter-python";
8866
8904
  import { infraId as infraId9 } from "@neat.is/types";
8867
8905
  var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
8868
8906
  var PARSE_CHUNK6 = 16384;
8869
- function makePyParser4() {
8907
+ function makePyParser3() {
8870
8908
  const p = new Parser9();
8871
8909
  p.setLanguage(Python4);
8872
8910
  return p;
@@ -8973,7 +9011,7 @@ function foreignKeyParentTable(call) {
8973
9011
  }
8974
9012
  function sqlalchemyForeignKeys(file, serviceDir) {
8975
9013
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
8976
- const tree = parseSource6(makePyParser4(), file.content);
9014
+ const tree = parseSource6(makePyParser3(), file.content);
8977
9015
  const out = [];
8978
9016
  const seen = /* @__PURE__ */ new Set();
8979
9017
  walk3(tree.rootNode, (node) => {
@@ -9010,7 +9048,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
9010
9048
  }
9011
9049
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
9012
9050
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9013
- const tree = parseSource6(makePyParser4(), file.content);
9051
+ const tree = parseSource6(makePyParser3(), file.content);
9014
9052
  const out = [];
9015
9053
  const seen = /* @__PURE__ */ new Set();
9016
9054
  const push = (name, line, columns) => {
@@ -9062,7 +9100,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
9062
9100
  function buildSqlalchemyModelRegistry(files) {
9063
9101
  const table = /* @__PURE__ */ new Map();
9064
9102
  const ambiguous = /* @__PURE__ */ new Set();
9065
- const parser = makePyParser4();
9103
+ const parser = makePyParser3();
9066
9104
  for (const file of files) {
9067
9105
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) continue;
9068
9106
  const tree = parseSource6(parser, file.content);
@@ -9111,7 +9149,7 @@ function importsModelName(content, name) {
9111
9149
  function pythonOrmCrossFileEndpoints(files, serviceDir) {
9112
9150
  const registry = buildSqlalchemyModelRegistry(files);
9113
9151
  if (registry.size === 0) return [];
9114
- const parser = makePyParser4();
9152
+ const parser = makePyParser3();
9115
9153
  const out = [];
9116
9154
  const seen = /* @__PURE__ */ new Set();
9117
9155
  for (const file of files) {
@@ -9148,7 +9186,7 @@ import Python5 from "tree-sitter-python";
9148
9186
  import { infraId as infraId10 } from "@neat.is/types";
9149
9187
  var DJANGO_IMPORT_RE = /(?:from|import)\s+django\b/;
9150
9188
  var PARSE_CHUNK7 = 16384;
9151
- function makePyParser5() {
9189
+ function makePyParser4() {
9152
9190
  const p = new Parser10();
9153
9191
  p.setLanguage(Python5);
9154
9192
  return p;
@@ -9209,7 +9247,7 @@ function readMeta(body) {
9209
9247
  }
9210
9248
  function djangoOrmEndpointsFromFile(file, serviceDir) {
9211
9249
  if (!DJANGO_IMPORT_RE.test(file.content)) return [];
9212
- const tree = parseSource7(makePyParser5(), file.content);
9250
+ const tree = parseSource7(makePyParser4(), file.content);
9213
9251
  const out = [];
9214
9252
  const seen = /* @__PURE__ */ new Set();
9215
9253
  const defaultAppLabel = path40.basename(path40.dirname(file.path));
@@ -9243,7 +9281,7 @@ import JavaScript7 from "tree-sitter-javascript";
9243
9281
  import { infraId as infraId11 } from "@neat.is/types";
9244
9282
  var DRIZZLE_IMPORT_RE = /drizzle-orm/;
9245
9283
  var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
9246
- function parserForExt2(ext) {
9284
+ function parserForExt3(ext) {
9247
9285
  const p = new Parser11();
9248
9286
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? JavaScript7);
9249
9287
  return p;
@@ -9315,7 +9353,7 @@ function columnsFromObject(obj) {
9315
9353
  }
9316
9354
  function drizzleEndpointsFromFile(file, serviceDir) {
9317
9355
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
9318
- const tree = parseSource3(parserForExt2(path41.extname(file.path)), file.content);
9356
+ const tree = parseSource3(parserForExt3(path41.extname(file.path)), file.content);
9319
9357
  const out = [];
9320
9358
  const seen = /* @__PURE__ */ new Set();
9321
9359
  const walk9 = (node) => {
@@ -9404,7 +9442,7 @@ function referencesTargetVar(call) {
9404
9442
  }
9405
9443
  function drizzleForeignKeys(file, serviceDir) {
9406
9444
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
9407
- const tree = parseSource3(parserForExt2(path41.extname(file.path)), file.content);
9445
+ const tree = parseSource3(parserForExt3(path41.extname(file.path)), file.content);
9408
9446
  const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
9409
9447
  const out = [];
9410
9448
  const seen = /* @__PURE__ */ new Set();
@@ -10467,8 +10505,41 @@ import path45 from "path";
10467
10505
  import Parser14 from "tree-sitter";
10468
10506
  import Go3 from "tree-sitter-go";
10469
10507
  import { infraId as infraId15 } from "@neat.is/types";
10470
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
10471
10508
  var PARSE_CHUNK10 = 16384;
10509
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
10510
+ "Query",
10511
+ "QueryContext",
10512
+ "QueryRow",
10513
+ "QueryRowContext",
10514
+ "Exec",
10515
+ "ExecContext",
10516
+ "Prepare",
10517
+ "PrepareContext"
10518
+ ]);
10519
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
10520
+ "Get",
10521
+ "Select",
10522
+ "Queryx",
10523
+ "QueryRowx",
10524
+ "NamedExec",
10525
+ "NamedQuery",
10526
+ "MustExec",
10527
+ "Preparex",
10528
+ "GetContext",
10529
+ "SelectContext"
10530
+ ]);
10531
+ var DATABASE_SQL_IMPORT = "database/sql";
10532
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
10533
+ function makeGoParser3() {
10534
+ const p = new Parser14();
10535
+ p.setLanguage(Go3);
10536
+ return p;
10537
+ }
10538
+ function parseSource10(parser, source) {
10539
+ return parser.parse(
10540
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
10541
+ );
10542
+ }
10472
10543
  function walk7(node, visit) {
10473
10544
  visit(node);
10474
10545
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -10476,25 +10547,54 @@ function walk7(node, visit) {
10476
10547
  if (child) walk7(child, visit);
10477
10548
  }
10478
10549
  }
10550
+ function goStringLiteralValue(node) {
10551
+ if (!node) return null;
10552
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
10553
+ const t = node.text;
10554
+ return t.length >= 2 ? t.slice(1, -1) : "";
10555
+ }
10556
+ return null;
10557
+ }
10558
+ function goImportsAny(root, names) {
10559
+ let found = false;
10560
+ walk7(root, (node) => {
10561
+ if (found || node.type !== "import_spec") return;
10562
+ for (let i = 0; i < node.namedChildCount; i++) {
10563
+ const value = goStringLiteralValue(node.namedChild(i));
10564
+ if (value !== null && names.has(value)) found = true;
10565
+ }
10566
+ });
10567
+ return found;
10568
+ }
10569
+ function firstStringLiteralArg(argsNode) {
10570
+ if (!argsNode) return null;
10571
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
10572
+ const value = goStringLiteralValue(argsNode.namedChild(i));
10573
+ if (value !== null) return value;
10574
+ }
10575
+ return null;
10576
+ }
10479
10577
  function goSqlEndpointsFromFile(file, serviceDir) {
10480
10578
  if (path45.extname(file.path) !== ".go") return [];
10481
- const parser = new Parser14();
10482
- parser.setLanguage(Go3);
10483
- const tree = parser.parse(
10484
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
10485
- );
10579
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
10580
+ const tree = parseSource10(makeGoParser3(), file.content);
10581
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
10582
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
10583
+ if (!importsDatabaseSql && !importsSqlx) return [];
10486
10584
  const out = [];
10487
10585
  walk7(tree.rootNode, (node) => {
10488
10586
  if (node.type !== "call_expression") return;
10489
10587
  const fn = node.childForFieldName("function");
10490
10588
  if (fn?.type !== "selector_expression") return;
10491
10589
  const method = fn.childForFieldName("field")?.text;
10492
- if (!method || !SQL_METHODS.has(method)) return;
10493
- const arg = node.childForFieldName("arguments")?.namedChild(0);
10494
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
10495
- const sql = arg.text.slice(1, -1);
10590
+ if (!method) return;
10591
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
10592
+ if (!recognized) return;
10593
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
10594
+ if (sql === null) return;
10496
10595
  const table = tableFromSqlStatement(sql);
10497
10596
  if (!table) return;
10597
+ const columns = columnsFromSqlStatement(sql);
10498
10598
  const line = node.startPosition.row + 1;
10499
10599
  out.push({
10500
10600
  infraId: infraId15("sql-table", table),
@@ -10502,7 +10602,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
10502
10602
  kind: "sql-table",
10503
10603
  edgeType: "CALLS",
10504
10604
  confidenceKind: "verified-call-site",
10505
- evidence: { file: toPosix(path45.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
10605
+ ...columns.length > 0 ? { columns } : {},
10606
+ evidence: {
10607
+ file: toPosix(path45.relative(serviceDir, file.path)),
10608
+ line,
10609
+ snippet: snippet(file.content, line)
10610
+ }
10506
10611
  });
10507
10612
  });
10508
10613
  return out;
@@ -10515,12 +10620,12 @@ import Go4 from "tree-sitter-go";
10515
10620
  import { infraId as infraId16 } from "@neat.is/types";
10516
10621
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
10517
10622
  var PARSE_CHUNK11 = 16384;
10518
- function makeGoParser3() {
10623
+ function makeGoParser4() {
10519
10624
  const p = new Parser15();
10520
10625
  p.setLanguage(Go4);
10521
10626
  return p;
10522
10627
  }
10523
- function parseSource10(parser, source) {
10628
+ function parseSource11(parser, source) {
10524
10629
  return parser.parse(
10525
10630
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
10526
10631
  );
@@ -10944,7 +11049,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
10944
11049
  function gormEndpointsFromFile(file, serviceDir) {
10945
11050
  if (path46.extname(file.path) !== ".go") return [];
10946
11051
  if (!GORM_IMPORT_RE.test(file.content)) return [];
10947
- const tree = parseSource10(makeGoParser3(), file.content);
11052
+ const tree = parseSource11(makeGoParser4(), file.content);
10948
11053
  const { structs, models, tableFor } = analyze(tree);
10949
11054
  const out = [];
10950
11055
  const seenTables = /* @__PURE__ */ new Set();
@@ -10975,7 +11080,7 @@ function gormEndpointsFromFile(file, serviceDir) {
10975
11080
  function gormForeignKeys(file, serviceDir) {
10976
11081
  if (path46.extname(file.path) !== ".go") return [];
10977
11082
  if (!GORM_IMPORT_RE.test(file.content)) return [];
10978
- const tree = parseSource10(makeGoParser3(), file.content);
11083
+ const tree = parseSource11(makeGoParser4(), file.content);
10979
11084
  const { structs, models, tableFor } = analyze(tree);
10980
11085
  const out = [];
10981
11086
  const seen = /* @__PURE__ */ new Set();
@@ -12105,7 +12210,7 @@ import {
12105
12210
  } from "@neat.is/types";
12106
12211
  var ZOD_IMPORT_RE = /\bzod\b/;
12107
12212
  var ZOD_OBJECTS = /* @__PURE__ */ new Set(["z", "zod"]);
12108
- function parserForExt3(ext) {
12213
+ function parserForExt4(ext) {
12109
12214
  const p = new Parser16();
12110
12215
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? JavaScript8);
12111
12216
  return p;
@@ -12194,7 +12299,7 @@ function topLevelSchemas(root) {
12194
12299
  }
12195
12300
  function zodShapesFromFile(file, serviceDir) {
12196
12301
  if (!ZOD_IMPORT_RE.test(file.content)) return [];
12197
- const tree = parseSource3(parserForExt3(path55.extname(file.path)), file.content);
12302
+ const tree = parseSource3(parserForExt4(path55.extname(file.path)), file.content);
12198
12303
  const out = [];
12199
12304
  const seen = /* @__PURE__ */ new Set();
12200
12305
  for (const { name, call } of topLevelSchemas(tree.rootNode)) {
@@ -14399,6 +14504,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
14399
14504
  unresolved++;
14400
14505
  continue;
14401
14506
  }
14507
+ if (signal.incident) {
14508
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
14509
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
14510
+ unresolved++;
14511
+ continue;
14512
+ }
14513
+ await appendConnectorIncident(ctx.errorsPath, {
14514
+ id: signal.incident.id,
14515
+ timestamp: signal.incident.timestamp,
14516
+ service: signal.incident.service,
14517
+ errorType: signal.incident.errorType,
14518
+ errorMessage: signal.incident.errorMessage,
14519
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
14520
+ affectedNode: resolved.targetNodeId
14521
+ });
14522
+ continue;
14523
+ }
14402
14524
  if (resolved.ensureInfraNode) {
14403
14525
  const { kind, name, provider } = resolved.ensureInfraNode;
14404
14526
  ensureInfraNode(graph, kind, name, provider);
@@ -16566,6 +16688,329 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
16566
16688
  };
16567
16689
  }
16568
16690
 
16691
+ // src/connectors/eas/types.ts
16692
+ function readEasCredentials(raw) {
16693
+ const token = raw["token"];
16694
+ if (typeof token !== "string" || token.length === 0) {
16695
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
16696
+ }
16697
+ return { token };
16698
+ }
16699
+ var EAS_STATUS_ERRORED = "ERRORED";
16700
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
16701
+ "SPIN_UP_BUILDER",
16702
+ "PREPARE_CREDENTIALS",
16703
+ "RESTORE_CACHE",
16704
+ "UPLOAD_APPLICATION_ARCHIVE"
16705
+ ]);
16706
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
16707
+ function isTransientFailure(err) {
16708
+ if (!err) return false;
16709
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
16710
+ if (phase) {
16711
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
16712
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
16713
+ }
16714
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
16715
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
16716
+ return false;
16717
+ }
16718
+ var FIELD_SEP3 = "\0";
16719
+ var EAS_TARGET_KIND = "eas-build";
16720
+ function packEasTargetName(identity) {
16721
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
16722
+ }
16723
+ function parseEasTargetName(targetName) {
16724
+ const sep = targetName.indexOf(FIELD_SEP3);
16725
+ if (sep === -1) return null;
16726
+ const serviceName = targetName.slice(0, sep);
16727
+ const phase = targetName.slice(sep + 1);
16728
+ if (!serviceName) return null;
16729
+ return { serviceName, phase };
16730
+ }
16731
+
16732
+ // src/connectors/eas/client.ts
16733
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
16734
+ var DEFAULT_PAGE_SIZE = 50;
16735
+ var DEFAULT_MAX_PAGES = 10;
16736
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
16737
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
16738
+ var BUILDS_QUERY = `
16739
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
16740
+ app {
16741
+ byId(appId: $appId) {
16742
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
16743
+ id
16744
+ status
16745
+ platform
16746
+ buildProfile
16747
+ gitCommitHash
16748
+ gitCommitMessage
16749
+ gitRef
16750
+ isGitWorkingTreeDirty
16751
+ createdAt
16752
+ completedAt
16753
+ error {
16754
+ buildPhase
16755
+ errorCode
16756
+ message
16757
+ docsUrl
16758
+ }
16759
+ logFileUrls
16760
+ }
16761
+ }
16762
+ }
16763
+ }
16764
+ `;
16765
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
16766
+ const res = await junctionFetch(
16767
+ apiUrl,
16768
+ {
16769
+ method: "POST",
16770
+ headers: {
16771
+ "Content-Type": "application/json",
16772
+ ...bearerAuthHeader(token)
16773
+ },
16774
+ body: JSON.stringify({ query, variables })
16775
+ },
16776
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
16777
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
16778
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
16779
+ );
16780
+ if (!res.ok) {
16781
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
16782
+ }
16783
+ const body = await res.json();
16784
+ if (body.errors && body.errors.length > 0) {
16785
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
16786
+ }
16787
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
16788
+ return body.data;
16789
+ }
16790
+ async function fetchErroredBuilds(token, config, fetchImpl) {
16791
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
16792
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
16793
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
16794
+ const out = [];
16795
+ const seen = /* @__PURE__ */ new Set();
16796
+ for (let page = 0; page < maxPages; page++) {
16797
+ const data = await easGraphQL(
16798
+ apiUrl,
16799
+ token,
16800
+ BUILDS_QUERY,
16801
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
16802
+ config.appId,
16803
+ fetchImpl
16804
+ );
16805
+ const builds = data.app?.byId?.builds;
16806
+ if (!Array.isArray(builds)) break;
16807
+ let added = 0;
16808
+ for (const b of builds) {
16809
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
16810
+ if (b.status !== EAS_STATUS_ERRORED) continue;
16811
+ if (seen.has(b.id)) continue;
16812
+ seen.add(b.id);
16813
+ out.push(b);
16814
+ added++;
16815
+ }
16816
+ if (builds.length < pageSize) break;
16817
+ if (added === 0) break;
16818
+ }
16819
+ return out;
16820
+ }
16821
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
16822
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
16823
+ const doFetch = fetchImpl ?? fetch;
16824
+ const chunks = [];
16825
+ for (const url of logFileUrls) {
16826
+ if (typeof url !== "string" || url.length === 0) continue;
16827
+ try {
16828
+ const res = await doFetch(url);
16829
+ if (!res.ok) continue;
16830
+ chunks.push(await res.text());
16831
+ } catch {
16832
+ }
16833
+ }
16834
+ const joined = chunks.join("\n");
16835
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
16836
+ }
16837
+
16838
+ // src/connectors/eas/map.ts
16839
+ function buildEventTime(build) {
16840
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
16841
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
16842
+ return (/* @__PURE__ */ new Date()).toISOString();
16843
+ }
16844
+ function incidentMessage2(build) {
16845
+ const err = build.error ?? {};
16846
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
16847
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
16848
+ let msg = `EAS build failed${phase}: ${detail}`;
16849
+ if (build.isGitWorkingTreeDirty === true) {
16850
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
16851
+ }
16852
+ return msg;
16853
+ }
16854
+ function incidentAttributes(build) {
16855
+ const attrs = {};
16856
+ const err = build.error ?? {};
16857
+ const put = (k, v) => {
16858
+ if (typeof v === "string" && v.length === 0) return;
16859
+ if (v !== void 0 && v !== null) attrs[k] = v;
16860
+ };
16861
+ put("eas.buildId", build.id);
16862
+ put("eas.platform", build.platform ?? void 0);
16863
+ put("eas.buildProfile", build.buildProfile ?? void 0);
16864
+ put("eas.buildPhase", err.buildPhase ?? void 0);
16865
+ put("eas.errorCode", err.errorCode ?? void 0);
16866
+ put("eas.docsUrl", err.docsUrl ?? void 0);
16867
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
16868
+ put("eas.gitRef", build.gitRef ?? void 0);
16869
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
16870
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
16871
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
16872
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
16873
+ }
16874
+ put("eas.createdAt", build.createdAt ?? void 0);
16875
+ put("eas.completedAt", build.completedAt ?? void 0);
16876
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
16877
+ attrs["eas.logs"] = build.logsText;
16878
+ }
16879
+ return attrs;
16880
+ }
16881
+ function mapBuildToSignal(build, serviceName) {
16882
+ if (!build || typeof build !== "object") return null;
16883
+ if (build.status !== EAS_STATUS_ERRORED) return null;
16884
+ if (!build.error) return null;
16885
+ if (isTransientFailure(build.error)) return null;
16886
+ const timestamp = buildEventTime(build);
16887
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
16888
+ return {
16889
+ targetKind: EAS_TARGET_KIND,
16890
+ targetName: packEasTargetName({ serviceName, phase }),
16891
+ // Incident-only — no edge, so no call/error count to replay.
16892
+ callCount: 0,
16893
+ errorCount: 0,
16894
+ lastObservedIso: timestamp,
16895
+ incident: {
16896
+ id: `eas:build:${build.id}`,
16897
+ timestamp,
16898
+ service: serviceName,
16899
+ errorType: "eas-build-failure",
16900
+ errorMessage: incidentMessage2(build),
16901
+ attributes: incidentAttributes(build)
16902
+ }
16903
+ };
16904
+ }
16905
+ function mapBuildsToSignals(builds, serviceName) {
16906
+ const out = [];
16907
+ for (const build of builds) {
16908
+ const signal = mapBuildToSignal(build, serviceName);
16909
+ if (signal) out.push(signal);
16910
+ }
16911
+ return out;
16912
+ }
16913
+
16914
+ // src/connectors/eas/resolve.ts
16915
+ import { EdgeType as EdgeType34, NodeType as NodeType32, parseFileId as parseFileId3 } from "@neat.is/types";
16916
+ var NO_ENV2 = "unknown";
16917
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
16918
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
16919
+ "READ_APP_CONFIG",
16920
+ "CONFIGURE_EXPO_UPDATES",
16921
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
16922
+ ]);
16923
+ function configBasenamesForPhase(phase) {
16924
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
16925
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
16926
+ return [];
16927
+ }
16928
+ function configNodeService(graph, configNodeId) {
16929
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
16930
+ const edge = graph.getEdgeAttributes(edgeId);
16931
+ if (edge.type !== EdgeType34.CONFIGURED_BY) continue;
16932
+ const parsed = parseFileId3(edge.source);
16933
+ if (parsed) return parsed.service;
16934
+ }
16935
+ return null;
16936
+ }
16937
+ function findConfigNode(graph, basenames, serviceName) {
16938
+ let scoped = null;
16939
+ let anyMatch = null;
16940
+ graph.forEachNode((id, attrs) => {
16941
+ if (scoped) return;
16942
+ const node = attrs;
16943
+ if (node.type !== NodeType32.ConfigNode) return;
16944
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
16945
+ if (anyMatch === null) anyMatch = id;
16946
+ if (configNodeService(graph, id) === serviceName) scoped = id;
16947
+ });
16948
+ return scoped ?? anyMatch;
16949
+ }
16950
+ function createEasResolveTarget(graph) {
16951
+ return (signal) => {
16952
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
16953
+ const identity = parseEasTargetName(signal.targetName);
16954
+ if (!identity) return null;
16955
+ const { serviceName, phase } = identity;
16956
+ const basenames = configBasenamesForPhase(phase);
16957
+ if (basenames.length > 0) {
16958
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
16959
+ if (configNodeId) {
16960
+ return { targetNodeId: configNodeId, serviceName, edgeType: EdgeType34.CALLS };
16961
+ }
16962
+ }
16963
+ return {
16964
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
16965
+ serviceName,
16966
+ edgeType: EdgeType34.CALLS
16967
+ };
16968
+ };
16969
+ }
16970
+
16971
+ // src/connectors/eas/index.ts
16972
+ function isBuildSince(build, sinceIso) {
16973
+ const t = Date.parse(buildEventTime(build));
16974
+ const s = Date.parse(sinceIso);
16975
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
16976
+ return t > s;
16977
+ }
16978
+ function boundedSinceIso2(since, now, maxLookbackMs) {
16979
+ const floor = new Date(now.getTime() - maxLookbackMs);
16980
+ if (!since) return floor.toISOString();
16981
+ const sinceMs = new Date(since).getTime();
16982
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
16983
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
16984
+ }
16985
+ var EasConnector = class {
16986
+ constructor(config, fetchImpl) {
16987
+ this.config = config;
16988
+ this.fetchImpl = fetchImpl;
16989
+ }
16990
+ config;
16991
+ fetchImpl;
16992
+ provider = "eas";
16993
+ async poll(ctx) {
16994
+ const creds = readEasCredentials(ctx.credentials);
16995
+ const serviceName = this.config.serviceName ?? this.config.appId;
16996
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
16997
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
16998
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
16999
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17000
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17001
+ for (const build of fresh) {
17002
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17003
+ }
17004
+ return mapBuildsToSignals(fresh, serviceName);
17005
+ }
17006
+ };
17007
+ function createEasConnector(graph, config, fetchImpl) {
17008
+ return {
17009
+ connector: new EasConnector(config, fetchImpl),
17010
+ resolveTarget: createEasResolveTarget(graph)
17011
+ };
17012
+ }
17013
+
16569
17014
  // src/connectors/registry.ts
16570
17015
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
16571
17016
  async function authProbe(input) {
@@ -16853,6 +17298,41 @@ var PROVIDER_DISPATCH = {
16853
17298
  ...fetchImpl ? { fetchImpl } : {}
16854
17299
  });
16855
17300
  }
17301
+ },
17302
+ eas: {
17303
+ provider: "eas",
17304
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
17305
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
17306
+ primaryCredentialKey: "token",
17307
+ requiredCredentialFields: ["token"],
17308
+ requiredOptionFields: ["appId"],
17309
+ build(graph, options) {
17310
+ return createEasConnector(graph, options);
17311
+ },
17312
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
17313
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
17314
+ // authenticates and that this app id is reachable, the same probe-the-real-
17315
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
17316
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
17317
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
17318
+ // silently at the first poll.
17319
+ async validate({ credentials, options, fetchImpl }) {
17320
+ const cfg = options;
17321
+ const appId = String(cfg.appId ?? "");
17322
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
17323
+ const probeConfig = {
17324
+ appId,
17325
+ pageSize: 1,
17326
+ maxPages: 1,
17327
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
17328
+ };
17329
+ try {
17330
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
17331
+ return { ok: true };
17332
+ } catch (err) {
17333
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
17334
+ }
17335
+ }
16856
17336
  }
16857
17337
  };
16858
17338
  function vercelCredsFrom(credentials) {
@@ -17065,7 +17545,11 @@ async function startConnectorPolling(input) {
17065
17545
  const stopFns = all.map(
17066
17546
  (registration) => startConnectorPollLoop(
17067
17547
  registration.connector,
17068
- { projectDir: input.projectDir, credentials: registration.credentials },
17548
+ {
17549
+ projectDir: input.projectDir,
17550
+ credentials: registration.credentials,
17551
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
17552
+ },
17069
17553
  input.graph,
17070
17554
  registration.resolveTarget,
17071
17555
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -17394,10 +17878,15 @@ function registerRoutes(scope, ctx) {
17394
17878
  }
17395
17879
  const reg = built.registration;
17396
17880
  const at = (/* @__PURE__ */ new Date()).toISOString();
17881
+ const incidentsPath = errorsPathFor(proj);
17397
17882
  try {
17398
17883
  const result = await ctx.runPoll(
17399
17884
  reg.connector,
17400
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
17885
+ {
17886
+ projectDir: proj.scanPath ?? "",
17887
+ credentials: reg.credentials,
17888
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
17889
+ },
17401
17890
  proj.graph,
17402
17891
  reg.resolveTarget
17403
17892
  );
@@ -18035,4 +18524,4 @@ export {
18035
18524
  deprovisionConnector,
18036
18525
  buildApi
18037
18526
  };
18038
- //# sourceMappingURL=chunk-LN75Z624.js.map
18527
+ //# sourceMappingURL=chunk-6T7ZHODF.js.map