@neat.is/core 0.4.28-dev.20260712 → 0.4.28-dev.20260713

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.
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  DEFAULT_PROJECT,
3
+ EnvRefUnsetError,
3
4
  Projects,
4
5
  attachGraphToEventBus,
5
6
  buildApi,
7
+ connectorMatchesProject,
6
8
  ensureInfraNode,
7
9
  ensureObservedFileNode,
8
10
  ensureServiceNode,
@@ -15,9 +17,13 @@ import {
15
17
  normalizePathTemplate,
16
18
  pathsForProject,
17
19
  pruneRegistry,
20
+ readConnectorsConfig,
18
21
  reconcileObservedRelPath,
22
+ recordConnectorPoll,
19
23
  registryPath,
20
24
  resetGraph,
25
+ resolveCredential,
26
+ sanitizePollError,
21
27
  saveGraphToDisk,
22
28
  setStatus,
23
29
  startPersistLoop,
@@ -25,7 +31,7 @@ import {
25
31
  touchLastSeen,
26
32
  upsertObservedEdge,
27
33
  writeAtomically
28
- } from "./chunk-5T6J3WKC.js";
34
+ } from "./chunk-N7HEYLOF.js";
29
35
  import {
30
36
  assertBindAuthority,
31
37
  buildOtelReceiver,
@@ -35,13 +41,13 @@ import {
35
41
 
36
42
  // src/daemon.ts
37
43
  import {
38
- promises as fs3,
44
+ promises as fs2,
39
45
  watch,
40
46
  renameSync,
41
47
  unlinkSync,
42
48
  writeFileSync
43
49
  } from "fs";
44
- import path3 from "path";
50
+ import path2 from "path";
45
51
  import { createRequire } from "module";
46
52
 
47
53
  // src/connectors/index.ts
@@ -103,16 +109,31 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
103
109
  let stopped = false;
104
110
  let since = ctx.since;
105
111
  const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
112
+ const connectorId = options.connectorId;
106
113
  const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
107
114
  const tick = () => {
108
115
  if (stopped) return;
109
116
  void (async () => {
110
117
  const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
111
118
  try {
112
- await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
119
+ const result = await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
113
120
  since = tickStartedAt;
121
+ if (connectorId) {
122
+ recordConnectorPoll(connectorId, {
123
+ outcome: "ok",
124
+ at: tickStartedAt,
125
+ signalsLastPoll: result.signalCount
126
+ });
127
+ }
114
128
  } catch (err) {
115
129
  onError(err);
130
+ if (connectorId) {
131
+ recordConnectorPoll(connectorId, {
132
+ outcome: "error",
133
+ at: tickStartedAt,
134
+ error: sanitizePollError(err)
135
+ });
136
+ }
116
137
  }
117
138
  })();
118
139
  };
@@ -403,6 +424,30 @@ function buildEdgeLogsQuery(limit) {
403
424
  `limit ${safeLimit}`
404
425
  ].join("\n");
405
426
  }
427
+ function logsAllHttpFailureMessage(status) {
428
+ if (status === 401 || status === 403) {
429
+ return `supabase connector: logs.all request rejected (HTTP ${status}). Check the Management API token, its analytics read scope, and --api-project-ref.`;
430
+ }
431
+ if (status === 404) {
432
+ return "supabase connector: logs.all project not found (HTTP 404). Check --api-project-ref.";
433
+ }
434
+ if (status === 429) {
435
+ return "supabase connector: logs.all rate-limited (HTTP 429). The connector will retry on the next poll; reduce poll cadence if this persists.";
436
+ }
437
+ if (status === 400) {
438
+ return "supabase connector: logs.all query rejected (HTTP 400). Provider details redacted; confirm the live log-query dialect before shipping this connector.";
439
+ }
440
+ return `supabase connector: logs.all request failed (HTTP ${status}); provider details redacted.`;
441
+ }
442
+ function providerErrorDetails(error) {
443
+ if (!error || typeof error === "string") return "";
444
+ const parts = [];
445
+ if (typeof error.code === "number") parts.push(`code ${error.code}`);
446
+ if (typeof error.status === "string" && /^[A-Z0-9_.-]+$/.test(error.status)) {
447
+ parts.push(`status ${error.status}`);
448
+ }
449
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
450
+ }
406
451
  async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
407
452
  const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
408
453
  const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
@@ -417,12 +462,18 @@ async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl
417
462
  { provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
418
463
  );
419
464
  if (!res.ok) {
420
- throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
465
+ throw new Error(logsAllHttpFailureMessage(res.status));
466
+ }
467
+ let body;
468
+ try {
469
+ body = await res.json();
470
+ } catch {
471
+ throw new Error("supabase connector: logs.all returned invalid JSON; provider details redacted.");
421
472
  }
422
- const body = await res.json();
423
473
  if (body.error) {
424
- const message = typeof body.error === "string" ? body.error : body.error.message;
425
- throw new Error(`supabase connector: logs.all returned an error (${message})`);
474
+ throw new Error(
475
+ `supabase connector: logs.all returned a provider error${providerErrorDetails(body.error)}; provider message redacted.`
476
+ );
426
477
  }
427
478
  return body.result ?? [];
428
479
  }
@@ -450,10 +501,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
450
501
  // src/connectors/supabase/map.ts
451
502
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
452
503
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
453
- function targetFromRestPath(path4) {
454
- const rpcMatch = REST_RPC_PATH_RE.exec(path4);
504
+ function targetFromRestPath(path3) {
505
+ const rpcMatch = REST_RPC_PATH_RE.exec(path3);
455
506
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
456
- const tableMatch = REST_TABLE_PATH_RE.exec(path4);
507
+ const tableMatch = REST_TABLE_PATH_RE.exec(path3);
457
508
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
458
509
  return null;
459
510
  }
@@ -574,6 +625,35 @@ function createSupabaseResolveTarget(graph, config) {
574
625
 
575
626
  // src/connectors/supabase/index.ts
576
627
  var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
628
+ function errorCode(err) {
629
+ const code = err?.code;
630
+ return typeof code === "string" && code.length > 0 ? code : void 0;
631
+ }
632
+ function describeSupabasePostgresSurfaceFailure(projectRef, err) {
633
+ const code = errorCode(err);
634
+ let reason;
635
+ switch (code) {
636
+ case "42501":
637
+ reason = "permission denied; grant pg_read_all_stats to the configured Postgres role";
638
+ break;
639
+ case "42P01":
640
+ case "42704":
641
+ reason = "pg_stat_statements is not enabled or visible to the configured Postgres role";
642
+ break;
643
+ case "28P01":
644
+ case "28000":
645
+ reason = "Postgres credential rejected";
646
+ break;
647
+ case "3D000":
648
+ reason = "database not found";
649
+ break;
650
+ default: {
651
+ const name = err instanceof Error && err.name ? err.name : "Error";
652
+ reason = code ? `${name} ${code}` : name;
653
+ }
654
+ }
655
+ return `supabase connector: pg_stat_statements surface unavailable for project ${projectRef} (${reason}); continuing with Management API log surface.`;
656
+ }
577
657
  var SupabaseConnector = class {
578
658
  // `deps.fetchPgStatStatements` defaults to the real Postgres-backed
579
659
  // implementation; tests override it to exercise the "both surfaces
@@ -605,12 +685,18 @@ var SupabaseConnector = class {
605
685
  const signals = mapEdgeLogRowsToSignals(logRows);
606
686
  if (creds.postgresConnectionString) {
607
687
  const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
608
- const statementRows = await fetchStatements(
609
- creds.postgresConnectionString,
610
- this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
611
- this.config.apiProjectRef
612
- );
613
- signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
688
+ try {
689
+ const statementRows = await fetchStatements(
690
+ creds.postgresConnectionString,
691
+ this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
692
+ this.config.apiProjectRef
693
+ );
694
+ signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
695
+ } catch (err) {
696
+ const summary = describeSupabasePostgresSurfaceFailure(this.config.apiProjectRef, err);
697
+ if (this.deps.onPostgresSurfaceError) this.deps.onPostgresSurfaceError(err, summary);
698
+ else console.warn(summary);
699
+ }
614
700
  }
615
701
  return signals;
616
702
  }
@@ -978,9 +1064,9 @@ function parseFirebaseTargetName(targetName) {
978
1064
  const secondSep = rest.indexOf(FIELD_SEP);
979
1065
  if (secondSep === -1) return null;
980
1066
  const method = rest.slice(0, secondSep);
981
- const path4 = rest.slice(secondSep + 1);
982
- if (!resourceName || !method || !path4) return null;
983
- return { resourceName, method, path: path4 };
1067
+ const path3 = rest.slice(secondSep + 1);
1068
+ if (!resourceName || !method || !path3) return null;
1069
+ return { resourceName, method, path: path3 };
984
1070
  }
985
1071
  function resourceNameFor(type, labels) {
986
1072
  if (!labels) return null;
@@ -1017,14 +1103,14 @@ function mapLogEntryToSignal(entry) {
1017
1103
  if (!req) return null;
1018
1104
  if (!req.requestMethod) return null;
1019
1105
  const method = req.requestMethod.toUpperCase();
1020
- const path4 = pathFromRequestUrl(req.requestUrl);
1021
- if (path4 === null) return null;
1106
+ const path3 = pathFromRequestUrl(req.requestUrl);
1107
+ if (path3 === null) return null;
1022
1108
  const timestamp = entry.timestamp;
1023
1109
  if (!timestamp) return null;
1024
1110
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
1025
1111
  return {
1026
1112
  targetKind: resourceType,
1027
- targetName: packFirebaseTargetName({ resourceName, method, path: path4 }),
1113
+ targetName: packFirebaseTargetName({ resourceName, method, path: path3 }),
1028
1114
  callCount: 1,
1029
1115
  errorCount: isError ? 1 : 0,
1030
1116
  lastObservedIso: timestamp
@@ -1212,7 +1298,7 @@ function mapEventToSignal(event) {
1212
1298
  if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
1213
1299
  const statusCode = metadata?.statusCode;
1214
1300
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
1215
- const path4 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
1301
+ const path3 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
1216
1302
  return {
1217
1303
  targetKind: CLOUDFLARE_TARGET_KIND,
1218
1304
  targetName: scriptName,
@@ -1220,7 +1306,7 @@ function mapEventToSignal(event) {
1220
1306
  errorCount: isError ? 1 : 0,
1221
1307
  lastObservedIso: new Date(timestampMs).toISOString(),
1222
1308
  method,
1223
- ...path4 ? { path: path4 } : {},
1309
+ ...path3 ? { path: path3 } : {},
1224
1310
  ...typeof statusCode === "number" ? { statusCode } : {},
1225
1311
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
1226
1312
  };
@@ -1266,8 +1352,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
1266
1352
  });
1267
1353
  return found;
1268
1354
  }
1269
- function findMatchingRouteNode(graph, serviceName, method, path4) {
1270
- const normalizedPath = normalizePathTemplate(path4);
1355
+ function findMatchingRouteNode(graph, serviceName, method, path3) {
1356
+ const normalizedPath = normalizePathTemplate(path3);
1271
1357
  let found = null;
1272
1358
  graph.forEachNode((id, attrs) => {
1273
1359
  if (found) return;
@@ -1284,10 +1370,10 @@ function createCloudflareResolveTarget(config, graph) {
1284
1370
  return (signal) => {
1285
1371
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
1286
1372
  const scriptName = signal.targetName;
1287
- const { method, path: path4 } = signal;
1373
+ const { method, path: path3 } = signal;
1288
1374
  const resolveRouteGrain = (serviceName, wholeFileId) => {
1289
- if (!method || !path4) return wholeFileId;
1290
- return findMatchingRouteNode(graph, serviceName, method, path4) ?? wholeFileId;
1375
+ if (!method || !path3) return wholeFileId;
1376
+ return findMatchingRouteNode(graph, serviceName, method, path3) ?? wholeFileId;
1291
1377
  };
1292
1378
  const mapping = config.workers?.[scriptName];
1293
1379
  if (mapping) {
@@ -1316,280 +1402,6 @@ function createCloudflareResolveTarget(config, graph) {
1316
1402
  };
1317
1403
  }
1318
1404
 
1319
- // src/connectors-config.ts
1320
- import os from "os";
1321
- import path from "path";
1322
- import { promises as fs } from "fs";
1323
- var CONNECTORS_CONFIG_VERSION = 1;
1324
- var EnvRefUnsetError = class extends Error {
1325
- ref;
1326
- varName;
1327
- constructor(ref, varName) {
1328
- super(`${ref} is unset`);
1329
- this.name = "EnvRefUnsetError";
1330
- this.ref = ref;
1331
- this.varName = varName;
1332
- }
1333
- };
1334
- function neatHome() {
1335
- const override = process.env.NEAT_HOME;
1336
- if (override && override.length > 0) return path.resolve(override);
1337
- return path.join(os.homedir(), ".neat");
1338
- }
1339
- function connectorsConfigPath(home = neatHome()) {
1340
- return path.join(home, "connectors.json");
1341
- }
1342
- var MODE_MASK_LOOSER_THAN_0600 = 63;
1343
- async function warnIfModeLooserThan0600(file) {
1344
- if (process.platform === "win32") return;
1345
- try {
1346
- const stat = await fs.stat(file);
1347
- if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
1348
- const mode = (stat.mode & 511).toString(8).padStart(3, "0");
1349
- console.warn(
1350
- `[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
1351
- );
1352
- }
1353
- } catch {
1354
- }
1355
- }
1356
- async function readConnectorsConfig(home = neatHome()) {
1357
- const file = connectorsConfigPath(home);
1358
- let raw;
1359
- try {
1360
- raw = await fs.readFile(file, "utf8");
1361
- } catch (err) {
1362
- if (err.code === "ENOENT") {
1363
- return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
1364
- }
1365
- throw err;
1366
- }
1367
- await warnIfModeLooserThan0600(file);
1368
- let parsed;
1369
- try {
1370
- parsed = JSON.parse(raw);
1371
- } catch (err) {
1372
- throw new Error(`${file} is not valid JSON: ${err.message}`);
1373
- }
1374
- return validateConfig(parsed, file);
1375
- }
1376
- function validateConfig(parsed, file) {
1377
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1378
- throw new Error(`${file} must be a JSON object with a "connectors" array`);
1379
- }
1380
- const obj = parsed;
1381
- const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
1382
- if (typeof version !== "number" || !Number.isInteger(version)) {
1383
- throw new Error(`${file}: "version" must be an integer`);
1384
- }
1385
- const rawConnectors = obj.connectors;
1386
- if (!Array.isArray(rawConnectors)) {
1387
- throw new Error(`${file}: "connectors" must be an array`);
1388
- }
1389
- const connectors = rawConnectors.map((entry, i) => validateEntry(entry, i, file));
1390
- return { version, connectors };
1391
- }
1392
- function validateEntry(entry, index, file) {
1393
- const where = `${file}: connectors[${index}]`;
1394
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1395
- throw new Error(`${where} must be an object`);
1396
- }
1397
- const e = entry;
1398
- const id = e.id;
1399
- if (typeof id !== "string" || id.length === 0) {
1400
- throw new Error(`${where}.id must be a non-empty string`);
1401
- }
1402
- const provider = e.provider;
1403
- if (typeof provider !== "string" || provider.length === 0) {
1404
- throw new Error(`${where}.provider must be a non-empty string`);
1405
- }
1406
- if (e.project !== void 0 && typeof e.project !== "string") {
1407
- throw new Error(`${where}.project must be a string when present`);
1408
- }
1409
- const credential = validateCredentialRef(e.credential, `${where}.credential`);
1410
- let options;
1411
- if (e.options !== void 0) {
1412
- if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
1413
- throw new Error(`${where}.options must be an object when present`);
1414
- }
1415
- options = e.options;
1416
- }
1417
- return {
1418
- id,
1419
- provider,
1420
- ...typeof e.project === "string" ? { project: e.project } : {},
1421
- credential,
1422
- ...options ? { options } : {}
1423
- };
1424
- }
1425
- function validateCredentialRef(raw, where) {
1426
- if (typeof raw === "string") {
1427
- if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
1428
- return raw;
1429
- }
1430
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
1431
- const fields = raw;
1432
- const keys = Object.keys(fields);
1433
- if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
1434
- for (const key of keys) {
1435
- const v = fields[key];
1436
- if (typeof v !== "string" || v.length === 0) {
1437
- throw new Error(`${where}.${key} must be a non-empty string`);
1438
- }
1439
- }
1440
- return fields;
1441
- }
1442
- throw new Error(`${where} must be a string or an object of strings`);
1443
- }
1444
- function resolveCredential(ref, env = process.env) {
1445
- if (typeof ref === "string") {
1446
- return { kind: "single", value: resolveRef(ref, env) };
1447
- }
1448
- const fields = {};
1449
- for (const [key, value] of Object.entries(ref)) {
1450
- fields[key] = resolveRef(value, env);
1451
- }
1452
- return { kind: "fields", fields };
1453
- }
1454
- function resolveRef(value, env) {
1455
- if (value.length > 1 && value.startsWith("$")) {
1456
- const varName = value.slice(1);
1457
- const resolved = env[varName];
1458
- if (resolved === void 0 || resolved.length === 0) {
1459
- throw new EnvRefUnsetError(value, varName);
1460
- }
1461
- return resolved;
1462
- }
1463
- return value;
1464
- }
1465
- function connectorMatchesProject(entry, project) {
1466
- return entry.project === void 0 || entry.project === project;
1467
- }
1468
- var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
1469
- var CONNECTORS_LOCK_RETRY_MS = 50;
1470
- function connectorsConfigLockPath(home = neatHome()) {
1471
- return path.join(home, "connectors.json.lock");
1472
- }
1473
- function isEnvRef(value) {
1474
- return value.length > 1 && value.startsWith("$");
1475
- }
1476
- async function writeConfigAtomically0600(file, contents) {
1477
- await fs.mkdir(path.dirname(file), { recursive: true });
1478
- const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
1479
- const fd = await fs.open(tmp, "w", 384);
1480
- try {
1481
- await fd.writeFile(contents, "utf8");
1482
- await fd.chmod(384);
1483
- await fd.sync();
1484
- } finally {
1485
- await fd.close();
1486
- }
1487
- await fs.rename(tmp, file);
1488
- }
1489
- async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
1490
- const deadline = Date.now() + timeoutMs;
1491
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
1492
- for (; ; ) {
1493
- try {
1494
- const fd = await fs.open(lockPath, "wx");
1495
- try {
1496
- await fd.writeFile(`${process.pid}
1497
- `, "utf8");
1498
- } finally {
1499
- await fd.close();
1500
- }
1501
- return;
1502
- } catch (err) {
1503
- if (err.code !== "EEXIST") throw err;
1504
- if (Date.now() >= deadline) {
1505
- throw new Error(
1506
- `neat connector: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process may be writing the connector config; if none is, remove that file by hand.`
1507
- );
1508
- }
1509
- await new Promise((r) => setTimeout(r, CONNECTORS_LOCK_RETRY_MS));
1510
- }
1511
- }
1512
- }
1513
- async function releaseConnectorsLock(lockPath) {
1514
- await fs.unlink(lockPath).catch(() => {
1515
- });
1516
- }
1517
- async function withConnectorsLock(home, fn) {
1518
- const lock = connectorsConfigLockPath(home);
1519
- await acquireConnectorsLock(lock);
1520
- try {
1521
- return await fn();
1522
- } finally {
1523
- await releaseConnectorsLock(lock);
1524
- }
1525
- }
1526
- function serializeConfig(config) {
1527
- return JSON.stringify(
1528
- { version: config.version ?? CONNECTORS_CONFIG_VERSION, connectors: config.connectors },
1529
- null,
1530
- 2
1531
- ) + "\n";
1532
- }
1533
- async function upsertConnectorEntry(entry, home = neatHome()) {
1534
- const file = connectorsConfigPath(home);
1535
- return withConnectorsLock(home, async () => {
1536
- const config = await readConnectorsConfig(home);
1537
- validateEntry(entry, config.connectors.length, file);
1538
- const idx = config.connectors.findIndex((c) => c.id === entry.id);
1539
- const replaced = idx >= 0;
1540
- if (replaced) config.connectors[idx] = entry;
1541
- else config.connectors.push(entry);
1542
- await writeConfigAtomically0600(file, serializeConfig(config));
1543
- return { replaced };
1544
- });
1545
- }
1546
- async function removeConnectorEntry(id, home = neatHome()) {
1547
- const file = connectorsConfigPath(home);
1548
- return withConnectorsLock(home, async () => {
1549
- const config = await readConnectorsConfig(home);
1550
- const idx = config.connectors.findIndex((c) => c.id === id);
1551
- if (idx < 0) return void 0;
1552
- const [removed] = config.connectors.splice(idx, 1);
1553
- await writeConfigAtomically0600(file, serializeConfig(config));
1554
- return removed;
1555
- });
1556
- }
1557
- function slugFragment(value) {
1558
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1559
- }
1560
- function autoSlugConnectorId(provider, project, existingIds) {
1561
- const base = slugFragment(provider) || "connector";
1562
- if (!existingIds.has(base)) return base;
1563
- const projectFrag = project ? slugFragment(project) : "";
1564
- if (projectFrag) {
1565
- const withProject = `${base}-${projectFrag}`;
1566
- if (!existingIds.has(withProject)) return withProject;
1567
- }
1568
- const stem = projectFrag ? `${base}-${projectFrag}` : base;
1569
- for (let n = 2; ; n++) {
1570
- const candidate = `${stem}-${n}`;
1571
- if (!existingIds.has(candidate)) return candidate;
1572
- }
1573
- }
1574
- function describeCredential(ref, env = process.env) {
1575
- const one = (value, field) => {
1576
- if (isEnvRef(value)) {
1577
- const varName = value.slice(1);
1578
- const resolved = env[varName];
1579
- const set = typeof resolved === "string" && resolved.length > 0;
1580
- return {
1581
- ...field ? { field } : {},
1582
- kind: "env-ref",
1583
- display: value,
1584
- status: set ? "set" : "unset"
1585
- };
1586
- }
1587
- return { ...field ? { field } : {}, kind: "plaintext", display: "****" };
1588
- };
1589
- if (typeof ref === "string") return [one(ref)];
1590
- return Object.entries(ref).map(([k, v]) => one(v, k));
1591
- }
1592
-
1593
1405
  // src/connectors/registry.ts
1594
1406
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
1595
1407
  async function authProbe(input) {
@@ -1778,6 +1590,9 @@ function buildRegistration(entry, graph, env = process.env) {
1778
1590
  return {
1779
1591
  ok: true,
1780
1592
  registration: {
1593
+ // Carry the entry id so the daemon can key this connector's poll-status
1594
+ // records to it (ADR-136).
1595
+ id: entry.id,
1781
1596
  connector: built.connector,
1782
1597
  credentials,
1783
1598
  resolveTarget: built.resolveTarget,
@@ -1833,8 +1648,8 @@ async function loadConnectorRegistrations(input) {
1833
1648
  }
1834
1649
 
1835
1650
  // src/unrouted.ts
1836
- import { promises as fs2 } from "fs";
1837
- import path2 from "path";
1651
+ import { promises as fs } from "fs";
1652
+ import path from "path";
1838
1653
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
1839
1654
  return {
1840
1655
  timestamp: now.toISOString(),
@@ -1843,39 +1658,39 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
1843
1658
  traceId: traceId ?? null
1844
1659
  };
1845
1660
  }
1846
- async function appendUnroutedSpan(neatHome2, record) {
1847
- const target = path2.join(neatHome2, "errors.ndjson");
1848
- await fs2.mkdir(neatHome2, { recursive: true });
1849
- await fs2.appendFile(target, JSON.stringify(record) + "\n", "utf8");
1661
+ async function appendUnroutedSpan(neatHome, record) {
1662
+ const target = path.join(neatHome, "errors.ndjson");
1663
+ await fs.mkdir(neatHome, { recursive: true });
1664
+ await fs.appendFile(target, JSON.stringify(record) + "\n", "utf8");
1850
1665
  }
1851
- function unroutedErrorsPath(neatHome2) {
1852
- return path2.join(neatHome2, "errors.ndjson");
1666
+ function unroutedErrorsPath(neatHome) {
1667
+ return path.join(neatHome, "errors.ndjson");
1853
1668
  }
1854
1669
 
1855
1670
  // src/daemon.ts
1856
1671
  import { NodeType as NodeType4 } from "@neat.is/types";
1857
1672
  function daemonJsonPath(scanPath) {
1858
- return path3.join(scanPath, "neat-out", "daemon.json");
1673
+ return path2.join(scanPath, "neat-out", "daemon.json");
1859
1674
  }
1860
1675
  function daemonsDiscoveryDir(home) {
1861
1676
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
1862
- return path3.join(base, "daemons");
1677
+ return path2.join(base, "daemons");
1863
1678
  }
1864
1679
  function daemonDiscoveryPath(project, home) {
1865
- return path3.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
1680
+ return path2.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
1866
1681
  }
1867
1682
  function sanitizeDiscoveryName(project) {
1868
1683
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
1869
1684
  }
1870
1685
  function neatHomeFromEnv() {
1871
1686
  const env = process.env.NEAT_HOME;
1872
- if (env && env.length > 0) return path3.resolve(env);
1687
+ if (env && env.length > 0) return path2.resolve(env);
1873
1688
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1874
- return path3.join(home, ".neat");
1689
+ return path2.join(home, ".neat");
1875
1690
  }
1876
1691
  async function readDaemonRecord(scanPath) {
1877
1692
  try {
1878
- const raw = await fs3.readFile(daemonJsonPath(scanPath), "utf8");
1693
+ const raw = await fs2.readFile(daemonJsonPath(scanPath), "utf8");
1879
1694
  const parsed = JSON.parse(raw);
1880
1695
  if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
1881
1696
  return parsed;
@@ -1915,7 +1730,7 @@ async function clearDaemonRecord(record, home) {
1915
1730
  } catch {
1916
1731
  }
1917
1732
  try {
1918
- await fs3.unlink(daemonDiscoveryPath(record.project, home));
1733
+ await fs2.unlink(daemonDiscoveryPath(record.project, home));
1919
1734
  } catch {
1920
1735
  }
1921
1736
  }
@@ -1952,11 +1767,11 @@ function teardownSlot(slot) {
1952
1767
  }
1953
1768
  }
1954
1769
  function neatHomeFor(opts) {
1955
- if (opts.neatHome && opts.neatHome.length > 0) return path3.resolve(opts.neatHome);
1770
+ if (opts.neatHome && opts.neatHome.length > 0) return path2.resolve(opts.neatHome);
1956
1771
  const env = process.env.NEAT_HOME;
1957
- if (env && env.length > 0) return path3.resolve(env);
1772
+ if (env && env.length > 0) return path2.resolve(env);
1958
1773
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1959
- return path3.join(home, ".neat");
1774
+ return path2.join(home, ".neat");
1960
1775
  }
1961
1776
  function routeSpanToProject(serviceName, projects) {
1962
1777
  if (!serviceName) return DEFAULT_PROJECT;
@@ -2003,10 +1818,10 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
2003
1818
  (_id, attrs) => attrs.type === NodeType4.ServiceNode && attrs.name === serviceName
2004
1819
  );
2005
1820
  }
2006
- async function bootstrapProject(entry, connectors = [], neatHome2) {
2007
- const paths = pathsForProject(entry.name, path3.join(entry.path, "neat-out"));
1821
+ async function bootstrapProject(entry, connectors = [], neatHome) {
1822
+ const paths = pathsForProject(entry.name, path2.join(entry.path, "neat-out"));
2008
1823
  try {
2009
- const stat = await fs3.stat(entry.path);
1824
+ const stat = await fs2.stat(entry.path);
2010
1825
  if (!stat.isDirectory()) {
2011
1826
  throw new Error(`registered path ${entry.path} is not a directory`);
2012
1827
  }
@@ -2044,10 +1859,10 @@ async function bootstrapProject(entry, connectors = [], neatHome2) {
2044
1859
  staleEventsPath: paths.staleEventsPath,
2045
1860
  project: entry.name
2046
1861
  });
2047
- const fileConnectors = neatHome2 ? await loadConnectorRegistrations({
1862
+ const fileConnectors = neatHome ? await loadConnectorRegistrations({
2048
1863
  project: entry.name,
2049
1864
  graph,
2050
- home: neatHome2,
1865
+ home: neatHome,
2051
1866
  onSkip: (skipped, reason) => console.warn(
2052
1867
  `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
2053
1868
  )
@@ -2059,7 +1874,11 @@ async function bootstrapProject(entry, connectors = [], neatHome2) {
2059
1874
  { projectDir: entry.path, credentials: registration.credentials },
2060
1875
  graph,
2061
1876
  registration.resolveTarget,
2062
- { intervalMs: registration.intervalMs }
1877
+ // `connectorId` is threaded through so every tick lands in the
1878
+ // in-process status tracker the connector-status endpoint reads
1879
+ // (ADR-136). Undefined for a programmatic registration, which records
1880
+ // nothing.
1881
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
2063
1882
  )
2064
1883
  );
2065
1884
  const stopConnectors = () => {
@@ -2131,7 +1950,7 @@ async function startDaemon(opts = {}) {
2131
1950
  const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
2132
1951
  const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
2133
1952
  const singleProject = projectArg;
2134
- const singleProjectPath = singleProject && projectPathArg ? path3.resolve(projectPathArg) : null;
1953
+ const singleProjectPath = singleProject && projectPathArg ? path2.resolve(projectPathArg) : null;
2135
1954
  if (singleProject && !singleProjectPath) {
2136
1955
  throw new Error(
2137
1956
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -2139,14 +1958,14 @@ async function startDaemon(opts = {}) {
2139
1958
  }
2140
1959
  if (!singleProject) {
2141
1960
  try {
2142
- await fs3.access(regPath);
1961
+ await fs2.access(regPath);
2143
1962
  } catch {
2144
1963
  throw new Error(
2145
1964
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
2146
1965
  );
2147
1966
  }
2148
1967
  }
2149
- const pidPath = path3.join(home, "neatd.pid");
1968
+ const pidPath = path2.join(home, "neatd.pid");
2150
1969
  await writeAtomically(pidPath, `${process.pid}
2151
1970
  `);
2152
1971
  const slots = /* @__PURE__ */ new Map();
@@ -2328,7 +2147,12 @@ async function startDaemon(opts = {}) {
2328
2147
  // only this project (the dashboard pins to it), and the daemon-wide
2329
2148
  // `/health` carries it at the top level for the spawn-reuse identity
2330
2149
  // check. Absent for the legacy multi-project daemon.
2331
- singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
2150
+ singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0,
2151
+ // ADR-136 — the connector-status endpoint reads ~/.neat/connectors.json
2152
+ // through the same resolved home the slot bootstrap read it from, so a
2153
+ // daemon given an explicit NEAT_HOME serves status for the same file it
2154
+ // polls.
2155
+ connectorsHome: home
2332
2156
  });
2333
2157
  restAddress = await restApp.listen({ port: restPort, host });
2334
2158
  console.log(
@@ -2340,7 +2164,7 @@ async function startDaemon(opts = {}) {
2340
2164
  }
2341
2165
  if (restApp) await restApp.close().catch(() => {
2342
2166
  });
2343
- await fs3.unlink(pidPath).catch(() => {
2167
+ await fs2.unlink(pidPath).catch(() => {
2344
2168
  });
2345
2169
  throw new Error(
2346
2170
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -2466,7 +2290,7 @@ async function startDaemon(opts = {}) {
2466
2290
  });
2467
2291
  if (otlpApp) await otlpApp.close().catch(() => {
2468
2292
  });
2469
- await fs3.unlink(pidPath).catch(() => {
2293
+ await fs2.unlink(pidPath).catch(() => {
2470
2294
  });
2471
2295
  throw new Error(
2472
2296
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -2501,7 +2325,7 @@ async function startDaemon(opts = {}) {
2501
2325
  });
2502
2326
  if (otlpApp) await otlpApp.close().catch(() => {
2503
2327
  });
2504
- await fs3.unlink(pidPath).catch(() => {
2328
+ await fs2.unlink(pidPath).catch(() => {
2505
2329
  });
2506
2330
  throw new Error(
2507
2331
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -2548,8 +2372,8 @@ async function startDaemon(opts = {}) {
2548
2372
  let registryWatcher = null;
2549
2373
  let reloadTimer = null;
2550
2374
  if (!singleProject) try {
2551
- const regDir = path3.dirname(regPath);
2552
- const regBase = path3.basename(regPath);
2375
+ const regDir = path2.dirname(regPath);
2376
+ const regBase = path2.basename(regPath);
2553
2377
  registryWatcher = watch(regDir, (_eventType, filename) => {
2554
2378
  if (filename !== null && filename !== regBase) return;
2555
2379
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -2599,7 +2423,7 @@ async function startDaemon(opts = {}) {
2599
2423
  if (daemonRecord) {
2600
2424
  await clearDaemonRecord(daemonRecord, home);
2601
2425
  }
2602
- await fs3.unlink(pidPath).catch(() => {
2426
+ await fs2.unlink(pidPath).catch(() => {
2603
2427
  });
2604
2428
  };
2605
2429
  return {
@@ -2617,12 +2441,6 @@ async function startDaemon(opts = {}) {
2617
2441
  }
2618
2442
 
2619
2443
  export {
2620
- readConnectorsConfig,
2621
- isEnvRef,
2622
- upsertConnectorEntry,
2623
- removeConnectorEntry,
2624
- autoSlugConnectorId,
2625
- describeCredential,
2626
2444
  PROVIDER_DISPATCH,
2627
2445
  getProviderDispatch,
2628
2446
  validateConnectorEntry,
@@ -2636,4 +2454,4 @@ export {
2636
2454
  resolveHost,
2637
2455
  startDaemon
2638
2456
  };
2639
- //# sourceMappingURL=chunk-UJ6WBLIE.js.map
2457
+ //# sourceMappingURL=chunk-V2HUBS5J.js.map