@neat.is/core 0.7.9 → 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.
package/dist/neatd.cjs CHANGED
@@ -2229,6 +2229,7 @@ var import_yaml = require("yaml");
2229
2229
  var import_types3 = require("@neat.is/types");
2230
2230
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2231
2231
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2232
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2232
2233
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2233
2234
  "node_modules",
2234
2235
  ".git",
@@ -2268,6 +2269,7 @@ async function isPythonVenvDir(dir) {
2268
2269
  function isConfigFile(name) {
2269
2270
  const ext = import_node_path3.default.extname(name);
2270
2271
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2272
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2271
2273
  if (name === ".env" || name.startsWith(".env.")) {
2272
2274
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2273
2275
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -5372,6 +5374,21 @@ async function appendErrorEvent(ctx, ev) {
5372
5374
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
5373
5375
  await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5374
5376
  }
5377
+ async function appendConnectorIncident(errorsPath, input) {
5378
+ const ev = {
5379
+ id: input.id,
5380
+ timestamp: input.timestamp,
5381
+ service: input.service,
5382
+ traceId: input.id,
5383
+ spanId: input.id,
5384
+ errorType: input.errorType,
5385
+ errorMessage: input.errorMessage,
5386
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5387
+ affectedNode: input.affectedNode
5388
+ };
5389
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
5390
+ await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5391
+ }
5375
5392
  function incidentAffectedNode(span, graph, scanPath) {
5376
5393
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5377
5394
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
@@ -11182,8 +11199,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11182
11199
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11183
11200
  var import_types35 = require("@neat.is/types");
11184
11201
  init_otel();
11185
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11186
11202
  var PARSE_CHUNK10 = 16384;
11203
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11204
+ "Query",
11205
+ "QueryContext",
11206
+ "QueryRow",
11207
+ "QueryRowContext",
11208
+ "Exec",
11209
+ "ExecContext",
11210
+ "Prepare",
11211
+ "PrepareContext"
11212
+ ]);
11213
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11214
+ "Get",
11215
+ "Select",
11216
+ "Queryx",
11217
+ "QueryRowx",
11218
+ "NamedExec",
11219
+ "NamedQuery",
11220
+ "MustExec",
11221
+ "Preparex",
11222
+ "GetContext",
11223
+ "SelectContext"
11224
+ ]);
11225
+ var DATABASE_SQL_IMPORT = "database/sql";
11226
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11227
+ function makeGoParser3() {
11228
+ const p = new import_tree_sitter14.default();
11229
+ p.setLanguage(import_tree_sitter_go3.default);
11230
+ return p;
11231
+ }
11232
+ function parseSource10(parser, source) {
11233
+ return parser.parse(
11234
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11235
+ );
11236
+ }
11187
11237
  function walk7(node, visit) {
11188
11238
  visit(node);
11189
11239
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11191,25 +11241,54 @@ function walk7(node, visit) {
11191
11241
  if (child) walk7(child, visit);
11192
11242
  }
11193
11243
  }
11244
+ function goStringLiteralValue(node) {
11245
+ if (!node) return null;
11246
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11247
+ const t = node.text;
11248
+ return t.length >= 2 ? t.slice(1, -1) : "";
11249
+ }
11250
+ return null;
11251
+ }
11252
+ function goImportsAny(root, names) {
11253
+ let found = false;
11254
+ walk7(root, (node) => {
11255
+ if (found || node.type !== "import_spec") return;
11256
+ for (let i = 0; i < node.namedChildCount; i++) {
11257
+ const value = goStringLiteralValue(node.namedChild(i));
11258
+ if (value !== null && names.has(value)) found = true;
11259
+ }
11260
+ });
11261
+ return found;
11262
+ }
11263
+ function firstStringLiteralArg(argsNode) {
11264
+ if (!argsNode) return null;
11265
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11266
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11267
+ if (value !== null) return value;
11268
+ }
11269
+ return null;
11270
+ }
11194
11271
  function goSqlEndpointsFromFile(file, serviceDir) {
11195
11272
  if (import_node_path47.default.extname(file.path) !== ".go") return [];
11196
- const parser = new import_tree_sitter14.default();
11197
- parser.setLanguage(import_tree_sitter_go3.default);
11198
- const tree = parser.parse(
11199
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11200
- );
11273
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11274
+ const tree = parseSource10(makeGoParser3(), file.content);
11275
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11276
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11277
+ if (!importsDatabaseSql && !importsSqlx) return [];
11201
11278
  const out = [];
11202
11279
  walk7(tree.rootNode, (node) => {
11203
11280
  if (node.type !== "call_expression") return;
11204
11281
  const fn = node.childForFieldName("function");
11205
11282
  if (fn?.type !== "selector_expression") return;
11206
11283
  const method = fn.childForFieldName("field")?.text;
11207
- if (!method || !SQL_METHODS.has(method)) return;
11208
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11209
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11210
- const sql = arg.text.slice(1, -1);
11284
+ if (!method) return;
11285
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11286
+ if (!recognized) return;
11287
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11288
+ if (sql === null) return;
11211
11289
  const table = tableFromSqlStatement(sql);
11212
11290
  if (!table) return;
11291
+ const columns = columnsFromSqlStatement(sql);
11213
11292
  const line = node.startPosition.row + 1;
11214
11293
  out.push({
11215
11294
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11217,7 +11296,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11217
11296
  kind: "sql-table",
11218
11297
  edgeType: "CALLS",
11219
11298
  confidenceKind: "verified-call-site",
11220
- evidence: { file: toPosix(import_node_path47.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11299
+ ...columns.length > 0 ? { columns } : {},
11300
+ evidence: {
11301
+ file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
11302
+ line,
11303
+ snippet: snippet(file.content, line)
11304
+ }
11221
11305
  });
11222
11306
  });
11223
11307
  return out;
@@ -11231,12 +11315,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11231
11315
  var import_types36 = require("@neat.is/types");
11232
11316
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11233
11317
  var PARSE_CHUNK11 = 16384;
11234
- function makeGoParser3() {
11318
+ function makeGoParser4() {
11235
11319
  const p = new import_tree_sitter15.default();
11236
11320
  p.setLanguage(import_tree_sitter_go4.default);
11237
11321
  return p;
11238
11322
  }
11239
- function parseSource10(parser, source) {
11323
+ function parseSource11(parser, source) {
11240
11324
  return parser.parse(
11241
11325
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11242
11326
  );
@@ -11660,7 +11744,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11660
11744
  function gormEndpointsFromFile(file, serviceDir) {
11661
11745
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11662
11746
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11663
- const tree = parseSource10(makeGoParser3(), file.content);
11747
+ const tree = parseSource11(makeGoParser4(), file.content);
11664
11748
  const { structs, models, tableFor } = analyze(tree);
11665
11749
  const out = [];
11666
11750
  const seenTables = /* @__PURE__ */ new Set();
@@ -11691,7 +11775,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11691
11775
  function gormForeignKeys(file, serviceDir) {
11692
11776
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11693
11777
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11694
- const tree = parseSource10(makeGoParser3(), file.content);
11778
+ const tree = parseSource11(makeGoParser4(), file.content);
11695
11779
  const { structs, models, tableFor } = analyze(tree);
11696
11780
  const out = [];
11697
11781
  const seen = /* @__PURE__ */ new Set();
@@ -13486,7 +13570,7 @@ var Projects = class {
13486
13570
  init_cjs_shims();
13487
13571
  var import_fastify2 = __toESM(require("fastify"), 1);
13488
13572
  var import_cors = __toESM(require("@fastify/cors"), 1);
13489
- var import_types80 = require("@neat.is/types");
13573
+ var import_types85 = require("@neat.is/types");
13490
13574
 
13491
13575
  // src/extend/index.ts
13492
13576
  init_cjs_shims();
@@ -14882,6 +14966,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
14882
14966
  unresolved++;
14883
14967
  continue;
14884
14968
  }
14969
+ if (signal.incident) {
14970
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
14971
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
14972
+ unresolved++;
14973
+ continue;
14974
+ }
14975
+ await appendConnectorIncident(ctx.errorsPath, {
14976
+ id: signal.incident.id,
14977
+ timestamp: signal.incident.timestamp,
14978
+ service: signal.incident.service,
14979
+ errorType: signal.incident.errorType,
14980
+ errorMessage: signal.incident.errorMessage,
14981
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
14982
+ affectedNode: resolved.targetNodeId
14983
+ });
14984
+ continue;
14985
+ }
14885
14986
  if (resolved.ensureInfraNode) {
14886
14987
  const { kind, name, provider } = resolved.ensureInfraNode;
14887
14988
  ensureInfraNode(graph, kind, name, provider);
@@ -17116,6 +17217,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17116
17217
  };
17117
17218
  }
17118
17219
 
17220
+ // src/connectors/eas/index.ts
17221
+ init_cjs_shims();
17222
+
17223
+ // src/connectors/eas/client.ts
17224
+ init_cjs_shims();
17225
+
17226
+ // src/connectors/eas/types.ts
17227
+ init_cjs_shims();
17228
+ function readEasCredentials(raw) {
17229
+ const token = raw["token"];
17230
+ if (typeof token !== "string" || token.length === 0) {
17231
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17232
+ }
17233
+ return { token };
17234
+ }
17235
+ var EAS_STATUS_ERRORED = "ERRORED";
17236
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17237
+ "SPIN_UP_BUILDER",
17238
+ "PREPARE_CREDENTIALS",
17239
+ "RESTORE_CACHE",
17240
+ "UPLOAD_APPLICATION_ARCHIVE"
17241
+ ]);
17242
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17243
+ function isTransientFailure(err) {
17244
+ if (!err) return false;
17245
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17246
+ if (phase) {
17247
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17248
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17249
+ }
17250
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17251
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17252
+ return false;
17253
+ }
17254
+ var FIELD_SEP3 = "\0";
17255
+ var EAS_TARGET_KIND = "eas-build";
17256
+ function packEasTargetName(identity) {
17257
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17258
+ }
17259
+ function parseEasTargetName(targetName) {
17260
+ const sep = targetName.indexOf(FIELD_SEP3);
17261
+ if (sep === -1) return null;
17262
+ const serviceName = targetName.slice(0, sep);
17263
+ const phase = targetName.slice(sep + 1);
17264
+ if (!serviceName) return null;
17265
+ return { serviceName, phase };
17266
+ }
17267
+
17268
+ // src/connectors/eas/client.ts
17269
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17270
+ var DEFAULT_PAGE_SIZE = 50;
17271
+ var DEFAULT_MAX_PAGES = 10;
17272
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17273
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17274
+ var BUILDS_QUERY = `
17275
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17276
+ app {
17277
+ byId(appId: $appId) {
17278
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17279
+ id
17280
+ status
17281
+ platform
17282
+ buildProfile
17283
+ gitCommitHash
17284
+ gitCommitMessage
17285
+ gitRef
17286
+ isGitWorkingTreeDirty
17287
+ createdAt
17288
+ completedAt
17289
+ error {
17290
+ buildPhase
17291
+ errorCode
17292
+ message
17293
+ docsUrl
17294
+ }
17295
+ logFileUrls
17296
+ }
17297
+ }
17298
+ }
17299
+ }
17300
+ `;
17301
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17302
+ const res = await junctionFetch(
17303
+ apiUrl,
17304
+ {
17305
+ method: "POST",
17306
+ headers: {
17307
+ "Content-Type": "application/json",
17308
+ ...bearerAuthHeader(token)
17309
+ },
17310
+ body: JSON.stringify({ query, variables })
17311
+ },
17312
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17313
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17314
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17315
+ );
17316
+ if (!res.ok) {
17317
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17318
+ }
17319
+ const body = await res.json();
17320
+ if (body.errors && body.errors.length > 0) {
17321
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17322
+ }
17323
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17324
+ return body.data;
17325
+ }
17326
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17327
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17328
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17329
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17330
+ const out = [];
17331
+ const seen = /* @__PURE__ */ new Set();
17332
+ for (let page = 0; page < maxPages; page++) {
17333
+ const data = await easGraphQL(
17334
+ apiUrl,
17335
+ token,
17336
+ BUILDS_QUERY,
17337
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17338
+ config.appId,
17339
+ fetchImpl
17340
+ );
17341
+ const builds = data.app?.byId?.builds;
17342
+ if (!Array.isArray(builds)) break;
17343
+ let added = 0;
17344
+ for (const b of builds) {
17345
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17346
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17347
+ if (seen.has(b.id)) continue;
17348
+ seen.add(b.id);
17349
+ out.push(b);
17350
+ added++;
17351
+ }
17352
+ if (builds.length < pageSize) break;
17353
+ if (added === 0) break;
17354
+ }
17355
+ return out;
17356
+ }
17357
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17358
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17359
+ const doFetch = fetchImpl ?? fetch;
17360
+ const chunks = [];
17361
+ for (const url of logFileUrls) {
17362
+ if (typeof url !== "string" || url.length === 0) continue;
17363
+ try {
17364
+ const res = await doFetch(url);
17365
+ if (!res.ok) continue;
17366
+ chunks.push(await res.text());
17367
+ } catch {
17368
+ }
17369
+ }
17370
+ const joined = chunks.join("\n");
17371
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17372
+ }
17373
+
17374
+ // src/connectors/eas/map.ts
17375
+ init_cjs_shims();
17376
+ function buildEventTime(build) {
17377
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17378
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17379
+ return (/* @__PURE__ */ new Date()).toISOString();
17380
+ }
17381
+ function incidentMessage2(build) {
17382
+ const err = build.error ?? {};
17383
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17384
+ 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";
17385
+ let msg = `EAS build failed${phase}: ${detail}`;
17386
+ if (build.isGitWorkingTreeDirty === true) {
17387
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17388
+ }
17389
+ return msg;
17390
+ }
17391
+ function incidentAttributes(build) {
17392
+ const attrs = {};
17393
+ const err = build.error ?? {};
17394
+ const put = (k, v) => {
17395
+ if (typeof v === "string" && v.length === 0) return;
17396
+ if (v !== void 0 && v !== null) attrs[k] = v;
17397
+ };
17398
+ put("eas.buildId", build.id);
17399
+ put("eas.platform", build.platform ?? void 0);
17400
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17401
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17402
+ put("eas.errorCode", err.errorCode ?? void 0);
17403
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17404
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17405
+ put("eas.gitRef", build.gitRef ?? void 0);
17406
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17407
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17408
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17409
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17410
+ }
17411
+ put("eas.createdAt", build.createdAt ?? void 0);
17412
+ put("eas.completedAt", build.completedAt ?? void 0);
17413
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17414
+ attrs["eas.logs"] = build.logsText;
17415
+ }
17416
+ return attrs;
17417
+ }
17418
+ function mapBuildToSignal(build, serviceName) {
17419
+ if (!build || typeof build !== "object") return null;
17420
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17421
+ if (!build.error) return null;
17422
+ if (isTransientFailure(build.error)) return null;
17423
+ const timestamp = buildEventTime(build);
17424
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17425
+ return {
17426
+ targetKind: EAS_TARGET_KIND,
17427
+ targetName: packEasTargetName({ serviceName, phase }),
17428
+ // Incident-only — no edge, so no call/error count to replay.
17429
+ callCount: 0,
17430
+ errorCount: 0,
17431
+ lastObservedIso: timestamp,
17432
+ incident: {
17433
+ id: `eas:build:${build.id}`,
17434
+ timestamp,
17435
+ service: serviceName,
17436
+ errorType: "eas-build-failure",
17437
+ errorMessage: incidentMessage2(build),
17438
+ attributes: incidentAttributes(build)
17439
+ }
17440
+ };
17441
+ }
17442
+ function mapBuildsToSignals(builds, serviceName) {
17443
+ const out = [];
17444
+ for (const build of builds) {
17445
+ const signal = mapBuildToSignal(build, serviceName);
17446
+ if (signal) out.push(signal);
17447
+ }
17448
+ return out;
17449
+ }
17450
+
17451
+ // src/connectors/eas/resolve.ts
17452
+ init_cjs_shims();
17453
+ var import_types82 = require("@neat.is/types");
17454
+ var NO_ENV2 = "unknown";
17455
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17456
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17457
+ "READ_APP_CONFIG",
17458
+ "CONFIGURE_EXPO_UPDATES",
17459
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17460
+ ]);
17461
+ function configBasenamesForPhase(phase) {
17462
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17463
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17464
+ return [];
17465
+ }
17466
+ function configNodeService(graph, configNodeId) {
17467
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17468
+ const edge = graph.getEdgeAttributes(edgeId);
17469
+ if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
17470
+ const parsed = (0, import_types82.parseFileId)(edge.source);
17471
+ if (parsed) return parsed.service;
17472
+ }
17473
+ return null;
17474
+ }
17475
+ function findConfigNode(graph, basenames, serviceName) {
17476
+ let scoped = null;
17477
+ let anyMatch = null;
17478
+ graph.forEachNode((id, attrs) => {
17479
+ if (scoped) return;
17480
+ const node = attrs;
17481
+ if (node.type !== import_types82.NodeType.ConfigNode) return;
17482
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17483
+ if (anyMatch === null) anyMatch = id;
17484
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17485
+ });
17486
+ return scoped ?? anyMatch;
17487
+ }
17488
+ function createEasResolveTarget(graph) {
17489
+ return (signal) => {
17490
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17491
+ const identity = parseEasTargetName(signal.targetName);
17492
+ if (!identity) return null;
17493
+ const { serviceName, phase } = identity;
17494
+ const basenames = configBasenamesForPhase(phase);
17495
+ if (basenames.length > 0) {
17496
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17497
+ if (configNodeId) {
17498
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
17499
+ }
17500
+ }
17501
+ return {
17502
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17503
+ serviceName,
17504
+ edgeType: import_types82.EdgeType.CALLS
17505
+ };
17506
+ };
17507
+ }
17508
+
17509
+ // src/connectors/eas/index.ts
17510
+ function isBuildSince(build, sinceIso) {
17511
+ const t = Date.parse(buildEventTime(build));
17512
+ const s = Date.parse(sinceIso);
17513
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17514
+ return t > s;
17515
+ }
17516
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17517
+ const floor = new Date(now.getTime() - maxLookbackMs);
17518
+ if (!since) return floor.toISOString();
17519
+ const sinceMs = new Date(since).getTime();
17520
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17521
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17522
+ }
17523
+ var EasConnector = class {
17524
+ constructor(config, fetchImpl) {
17525
+ this.config = config;
17526
+ this.fetchImpl = fetchImpl;
17527
+ }
17528
+ config;
17529
+ fetchImpl;
17530
+ provider = "eas";
17531
+ async poll(ctx) {
17532
+ const creds = readEasCredentials(ctx.credentials);
17533
+ const serviceName = this.config.serviceName ?? this.config.appId;
17534
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17535
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17536
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17537
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17538
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17539
+ for (const build of fresh) {
17540
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17541
+ }
17542
+ return mapBuildsToSignals(fresh, serviceName);
17543
+ }
17544
+ };
17545
+ function createEasConnector(graph, config, fetchImpl) {
17546
+ return {
17547
+ connector: new EasConnector(config, fetchImpl),
17548
+ resolveTarget: createEasResolveTarget(graph)
17549
+ };
17550
+ }
17551
+
17119
17552
  // src/connectors/registry.ts
17120
17553
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17121
17554
  async function authProbe(input) {
@@ -17403,6 +17836,41 @@ var PROVIDER_DISPATCH = {
17403
17836
  ...fetchImpl ? { fetchImpl } : {}
17404
17837
  });
17405
17838
  }
17839
+ },
17840
+ eas: {
17841
+ provider: "eas",
17842
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
17843
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
17844
+ primaryCredentialKey: "token",
17845
+ requiredCredentialFields: ["token"],
17846
+ requiredOptionFields: ["appId"],
17847
+ build(graph, options) {
17848
+ return createEasConnector(graph, options);
17849
+ },
17850
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
17851
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
17852
+ // authenticates and that this app id is reachable, the same probe-the-real-
17853
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
17854
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
17855
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
17856
+ // silently at the first poll.
17857
+ async validate({ credentials, options, fetchImpl }) {
17858
+ const cfg = options;
17859
+ const appId = String(cfg.appId ?? "");
17860
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
17861
+ const probeConfig = {
17862
+ appId,
17863
+ pageSize: 1,
17864
+ maxPages: 1,
17865
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
17866
+ };
17867
+ try {
17868
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
17869
+ return { ok: true };
17870
+ } catch (err) {
17871
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
17872
+ }
17873
+ }
17406
17874
  }
17407
17875
  };
17408
17876
  function vercelCredsFrom(credentials) {
@@ -17584,7 +18052,11 @@ async function startConnectorPolling(input) {
17584
18052
  const stopFns = all.map(
17585
18053
  (registration) => startConnectorPollLoop(
17586
18054
  registration.connector,
17587
- { projectDir: input.projectDir, credentials: registration.credentials },
18055
+ {
18056
+ projectDir: input.projectDir,
18057
+ credentials: registration.credentials,
18058
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18059
+ },
17588
18060
  input.graph,
17589
18061
  registration.resolveTarget,
17590
18062
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -17754,11 +18226,11 @@ function registerRoutes(scope, ctx) {
17754
18226
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
17755
18227
  const parsed = [];
17756
18228
  for (const c of candidates) {
17757
- const r = import_types80.DivergenceTypeSchema.safeParse(c);
18229
+ const r = import_types85.DivergenceTypeSchema.safeParse(c);
17758
18230
  if (!r.success) {
17759
18231
  return reply.code(400).send({
17760
18232
  error: `unknown divergence type "${c}"`,
17761
- allowed: import_types80.DivergenceTypeSchema.options
18233
+ allowed: import_types85.DivergenceTypeSchema.options
17762
18234
  });
17763
18235
  }
17764
18236
  parsed.push(r.data);
@@ -17865,10 +18337,15 @@ function registerRoutes(scope, ctx) {
17865
18337
  }
17866
18338
  const reg = built.registration;
17867
18339
  const at = (/* @__PURE__ */ new Date()).toISOString();
18340
+ const incidentsPath = errorsPathFor(proj);
17868
18341
  try {
17869
18342
  const result = await ctx.runPoll(
17870
18343
  reg.connector,
17871
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18344
+ {
18345
+ projectDir: proj.scanPath ?? "",
18346
+ credentials: reg.credentials,
18347
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18348
+ },
17872
18349
  proj.graph,
17873
18350
  reg.resolveTarget
17874
18351
  );
@@ -18067,7 +18544,7 @@ function registerRoutes(scope, ctx) {
18067
18544
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18068
18545
  let violations = await log.readAll();
18069
18546
  if (req2.query.severity) {
18070
- const sev = import_types80.PolicySeveritySchema.safeParse(req2.query.severity);
18547
+ const sev = import_types85.PolicySeveritySchema.safeParse(req2.query.severity);
18071
18548
  if (!sev.success) {
18072
18549
  return reply.code(400).send({
18073
18550
  error: "invalid severity",
@@ -18106,7 +18583,7 @@ function registerRoutes(scope, ctx) {
18106
18583
  scope.post("/policies/check", async (req2, reply) => {
18107
18584
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
18108
18585
  if (!proj) return;
18109
- const parsed = import_types80.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
18586
+ const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
18110
18587
  if (!parsed.success) {
18111
18588
  return reply.code(400).send({
18112
18589
  error: "invalid /policies/check body",
@@ -18447,7 +18924,7 @@ function unroutedErrorsPath(neatHome4) {
18447
18924
  }
18448
18925
 
18449
18926
  // src/daemon.ts
18450
- var import_types81 = require("@neat.is/types");
18927
+ var import_types86 = require("@neat.is/types");
18451
18928
  function daemonJsonPath(scanPath) {
18452
18929
  return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
18453
18930
  }
@@ -18586,7 +19063,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
18586
19063
  if (!serviceName) return true;
18587
19064
  if (serviceNameMatchesProject(serviceName, project)) return true;
18588
19065
  return graph.someNode(
18589
- (_id, attrs) => attrs.type === import_types81.NodeType.ServiceNode && attrs.name === serviceName
19066
+ (_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
18590
19067
  );
18591
19068
  }
18592
19069
  async function bootstrapProject(entry2, connectors = [], neatHome4) {
@@ -18634,6 +19111,10 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
18634
19111
  project: entry2.name,
18635
19112
  graph,
18636
19113
  projectDir: entry2.path,
19114
+ // The slot's incident ledger, so an incident-emitting connector (ADR-185)
19115
+ // writes a build-failure incident onto the same errors.ndjson OTLP-derived
19116
+ // incidents land in.
19117
+ errorsPath: paths.errorsPath,
18637
19118
  ...neatHome4 ? { home: neatHome4 } : {},
18638
19119
  extra: connectors,
18639
19120
  onSkip: (skipped, reason) => console.warn(