@neat.is/core 0.5.3 → 0.6.0

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.
@@ -31,13 +31,13 @@ import {
31
31
  touchLastSeen,
32
32
  upsertObservedEdge,
33
33
  writeAtomically
34
- } from "./chunk-GJHEZC5K.js";
34
+ } from "./chunk-CS4GHQO3.js";
35
35
  import {
36
36
  assertBindAuthority,
37
37
  buildOtelReceiver,
38
38
  listenSteppingOtlp,
39
39
  readAuthEnv
40
- } from "./chunk-CFDPIMRP.js";
40
+ } from "./chunk-BZ3AJVAC.js";
41
41
 
42
42
  // src/daemon.ts
43
43
  import {
@@ -240,7 +240,12 @@ var JUNCTION_DEFAULT_RATE_LIMITS = {
240
240
  supabase: { capacity: 30, refillMs: 1e4 },
241
241
  // Not a documented API limit at all — a self-imposed ceiling on the raw
242
242
  // pg_stat_statements connection (see module header above).
243
- "supabase-postgres": { capacity: 20, refillMs: 3e3 }
243
+ "supabase-postgres": { capacity: 20, refillMs: 3e3 },
244
+ // Push provider (ADR-146): the Drains REST API is touched only by `neat
245
+ // connector add/remove/test` (provision/deprovision/validate), never a poll
246
+ // loop, so this bucket is exercised a handful of times per command. Kept
247
+ // conservative pending a documented Drains-API rate limit.
248
+ vercel: { capacity: 20, refillMs: 5e3 }
244
249
  };
245
250
  var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
246
251
  function defaultRateLimitFor(provider) {
@@ -1443,6 +1448,108 @@ function createCloudflareResolveTarget(config, graph) {
1443
1448
  };
1444
1449
  }
1445
1450
 
1451
+ // src/connectors/vercel/client.ts
1452
+ var DEFAULT_API_BASE_URL = "https://api.vercel.com";
1453
+ var DEFAULT_DRAIN_NAME = "neat-otlp";
1454
+ var TRACE_SCHEMAS = { trace: { version: "v1" } };
1455
+ function apiBase(config) {
1456
+ return config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
1457
+ }
1458
+ function teamQuery(config) {
1459
+ return `?teamId=${encodeURIComponent(config.teamId)}`;
1460
+ }
1461
+ function drainDelivery(config, otelToken) {
1462
+ return {
1463
+ type: "http",
1464
+ endpoint: config.endpoint,
1465
+ encoding: "json",
1466
+ headers: bearerAuthHeader(otelToken),
1467
+ ...config.secret ? { secret: config.secret } : {}
1468
+ };
1469
+ }
1470
+ async function describeError(res) {
1471
+ try {
1472
+ const data = await res.json();
1473
+ const message = data?.error?.message;
1474
+ return typeof message === "string" && message.length > 0 ? ` \u2014 ${message}` : "";
1475
+ } catch {
1476
+ return "";
1477
+ }
1478
+ }
1479
+ async function createVercelDrain(config, credentials, fetchImpl = fetch) {
1480
+ const projectIds = config.projectIds ?? [];
1481
+ const body = {
1482
+ name: config.drainName ?? DEFAULT_DRAIN_NAME,
1483
+ projects: projectIds.length > 0 ? "some" : "all",
1484
+ ...projectIds.length > 0 ? { projectIds } : {},
1485
+ schemas: TRACE_SCHEMAS,
1486
+ delivery: drainDelivery(config, credentials.otelToken),
1487
+ source: { kind: "self-served" }
1488
+ };
1489
+ const res = await junctionFetch(
1490
+ `${apiBase(config)}/v1/drains${teamQuery(config)}`,
1491
+ {
1492
+ method: "POST",
1493
+ headers: { "Content-Type": "application/json", ...bearerAuthHeader(credentials.token) },
1494
+ body: JSON.stringify(body)
1495
+ },
1496
+ { provider: "vercel", accountKey: config.teamId, fetchImpl }
1497
+ );
1498
+ if (!res.ok) {
1499
+ throw new Error(
1500
+ `vercel connector: create drain failed (${res.status} ${res.statusText}${await describeError(res)})`
1501
+ );
1502
+ }
1503
+ const payload = await res.json().catch(() => null);
1504
+ if (!payload || typeof payload.id !== "string" || payload.id.length === 0) {
1505
+ throw new Error(
1506
+ "vercel connector: create drain returned no drain id \u2014 the Drains API response shape may have changed"
1507
+ );
1508
+ }
1509
+ return {
1510
+ id: payload.id,
1511
+ ...payload.status ? { status: payload.status } : {},
1512
+ ...payload.disabledReason ? { disabledReason: payload.disabledReason } : {}
1513
+ };
1514
+ }
1515
+ async function deleteVercelDrain(config, drainId, credentials, fetchImpl = fetch) {
1516
+ const res = await junctionFetch(
1517
+ `${apiBase(config)}/v1/drains/${encodeURIComponent(drainId)}${teamQuery(config)}`,
1518
+ { method: "DELETE", headers: { ...bearerAuthHeader(credentials.token) } },
1519
+ { provider: "vercel", accountKey: config.teamId, fetchImpl }
1520
+ );
1521
+ if (res.ok || res.status === 404) return;
1522
+ throw new Error(
1523
+ `vercel connector: delete drain failed (${res.status} ${res.statusText}${await describeError(res)})`
1524
+ );
1525
+ }
1526
+ async function testVercelDrainDelivery(config, credentials, fetchImpl = fetch) {
1527
+ const res = await junctionFetch(
1528
+ `${apiBase(config)}/v1/drains/test${teamQuery(config)}`,
1529
+ {
1530
+ method: "POST",
1531
+ headers: { "Content-Type": "application/json", ...bearerAuthHeader(credentials.token) },
1532
+ body: JSON.stringify({ schemas: TRACE_SCHEMAS, delivery: drainDelivery(config, credentials.otelToken) })
1533
+ },
1534
+ { provider: "vercel", accountKey: config.teamId, fetchImpl }
1535
+ );
1536
+ if (res.status === 401 || res.status === 403) {
1537
+ return { status: "failure", error: `vercel rejected the API token (HTTP ${res.status})` };
1538
+ }
1539
+ if (!res.ok) {
1540
+ return {
1541
+ status: "failure",
1542
+ error: `vercel drain validation failed (${res.status} ${res.statusText}${await describeError(res)})`
1543
+ };
1544
+ }
1545
+ const payload = await res.json().catch(() => null);
1546
+ return {
1547
+ ...payload?.status ? { status: payload.status } : {},
1548
+ ...payload?.error ? { error: payload.error } : {},
1549
+ ...payload?.endpoint ? { endpoint: payload.endpoint } : {}
1550
+ };
1551
+ }
1552
+
1446
1553
  // src/connectors/registry.ts
1447
1554
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
1448
1555
  async function authProbe(input) {
@@ -1579,23 +1686,117 @@ var PROVIDER_DISPATCH = {
1579
1686
  resolveTarget: createCloudflareResolveTarget(config, graph)
1580
1687
  };
1581
1688
  },
1582
- // GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
1583
- // token live" endpoint. 200 on a valid token, 401 on an invalid one.
1689
+ // GET /accounts/{accountId}/tokens/verify — the *account-scoped* token-verify
1690
+ // endpoint. A Workers connector token is scoped to the account, and the
1691
+ // user-level `GET /user/tokens/verify` returns 401 "Invalid API Token" for
1692
+ // such a token even though it authenticates fine against the account's own
1693
+ // resources (confirmed live). Probing the account-scoped verify endpoint —
1694
+ // `accountId` is already required for this provider — returns 200
1695
+ // `{status:"active"}` for a working token and 401 for a bad one, so a valid
1696
+ // Workers token is no longer falsely rejected at `neat connector add`.
1584
1697
  validate({ credentials, options, fetchImpl }) {
1585
1698
  const cfg = options;
1586
1699
  const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
1587
1700
  return authProbe({
1588
1701
  provider: "cloudflare",
1589
1702
  accountKey: cfg.accountId ?? "validate",
1590
- url: `${baseUrl}/user/tokens/verify`,
1703
+ url: `${baseUrl}/accounts/${cfg.accountId ?? ""}/tokens/verify`,
1591
1704
  token: String(credentials.apiToken ?? ""),
1592
1705
  ...fetchImpl ? { fetchImpl } : {}
1593
1706
  });
1594
1707
  }
1595
1708
  }
1596
1709
  };
1597
- function getProviderDispatch(provider) {
1598
- return PROVIDER_DISPATCH[provider];
1710
+ function vercelCredsFrom(credentials) {
1711
+ return { token: String(credentials.token ?? ""), otelToken: String(credentials.otelToken ?? "") };
1712
+ }
1713
+ function vercelConfigFromOptions(options) {
1714
+ const raw = options.projectIds;
1715
+ let projectIds;
1716
+ if (Array.isArray(raw)) projectIds = raw.filter((p) => typeof p === "string");
1717
+ else if (typeof raw === "string" && raw.trim().length > 0) {
1718
+ projectIds = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1719
+ }
1720
+ return {
1721
+ teamId: String(options.teamId ?? ""),
1722
+ endpoint: String(options.endpoint ?? ""),
1723
+ ...projectIds && projectIds.length > 0 ? { projectIds } : {},
1724
+ ...typeof options.drainId === "string" ? { drainId: options.drainId } : {},
1725
+ ...typeof options.drainName === "string" ? { drainName: options.drainName } : {},
1726
+ ...typeof options.apiBaseUrl === "string" ? { apiBaseUrl: options.apiBaseUrl } : {},
1727
+ ...typeof options.secret === "string" ? { secret: options.secret } : {}
1728
+ };
1729
+ }
1730
+ var PUSH_PROVIDER_DISPATCH = {
1731
+ vercel: {
1732
+ provider: "vercel",
1733
+ // The Vercel access token is the "primary" secret a single `--token`
1734
+ // populates; `otelToken` (the daemon's OTLP bearer) is the second field.
1735
+ primaryCredentialKey: "token",
1736
+ requiredCredentialFields: ["token", "otelToken"],
1737
+ // teamId scopes every Drains call; endpoint is where the drain delivers.
1738
+ // projectIds is optional (absent → the drain covers the whole team).
1739
+ requiredOptionFields: ["teamId", "endpoint"],
1740
+ // POST /v1/drains/test — authenticates the token and pings the endpoint
1741
+ // with a sample event, so `success` means the credential is live *and* the
1742
+ // daemon's OTLP endpoint is reachable and accepted the drain's bearer.
1743
+ async validate({ credentials, options, fetchImpl }) {
1744
+ const result = await testVercelDrainDelivery(
1745
+ vercelConfigFromOptions(options),
1746
+ vercelCredsFrom(credentials),
1747
+ fetchImpl
1748
+ );
1749
+ if (result.status === "success") return { ok: true };
1750
+ return {
1751
+ ok: false,
1752
+ reason: result.error ?? `vercel drain delivery test returned "${result.status ?? "no status"}"`
1753
+ };
1754
+ },
1755
+ // POST /v1/drains — creates the trace drain, returns its id to store in
1756
+ // `options.drainId`. A created-but-not-enabled drain is surfaced as a note,
1757
+ // not a failure (the entry still points at a real drain).
1758
+ async provision({ credentials, options, fetchImpl }) {
1759
+ try {
1760
+ const created = await createVercelDrain(
1761
+ vercelConfigFromOptions(options),
1762
+ vercelCredsFrom(credentials),
1763
+ fetchImpl
1764
+ );
1765
+ const note = created.status && created.status !== "enabled" ? `the drain was created but its status is "${created.status}"${created.disabledReason ? ` (${created.disabledReason})` : ""} \u2014 check the Vercel dashboard` : void 0;
1766
+ return { ok: true, options: { drainId: created.id }, ...note ? { note } : {} };
1767
+ } catch (err) {
1768
+ return { ok: false, reason: err.message };
1769
+ }
1770
+ },
1771
+ // DELETE /v1/drains/{id} — idempotent (deleteVercelDrain treats 404 as
1772
+ // success). No recorded drainId → nothing to delete, still a success.
1773
+ async deprovision({ credentials, options, fetchImpl }) {
1774
+ const drainId = typeof options.drainId === "string" ? options.drainId : "";
1775
+ if (!drainId) {
1776
+ return { ok: true, note: "no drain id was recorded \u2014 nothing to delete on the Vercel side" };
1777
+ }
1778
+ try {
1779
+ await deleteVercelDrain(
1780
+ vercelConfigFromOptions(options),
1781
+ drainId,
1782
+ vercelCredsFrom(credentials),
1783
+ fetchImpl
1784
+ );
1785
+ return { ok: true };
1786
+ } catch (err) {
1787
+ return { ok: false, reason: err.message };
1788
+ }
1789
+ }
1790
+ }
1791
+ };
1792
+ function isPushProvider(provider) {
1793
+ return provider in PUSH_PROVIDER_DISPATCH;
1794
+ }
1795
+ function getProviderFieldSchema(provider) {
1796
+ return PROVIDER_DISPATCH[provider] ?? PUSH_PROVIDER_DISPATCH[provider];
1797
+ }
1798
+ function knownProviderNames() {
1799
+ return [...Object.keys(PROVIDER_DISPATCH), ...Object.keys(PUSH_PROVIDER_DISPATCH)].sort();
1599
1800
  }
1600
1801
  function resolveEntryCredentials(dispatch, entry, env) {
1601
1802
  let credentials;
@@ -1619,6 +1820,13 @@ function resolveEntryCredentials(dispatch, entry, env) {
1619
1820
  function buildRegistration(entry, graph, env = process.env) {
1620
1821
  const dispatch = PROVIDER_DISPATCH[entry.provider];
1621
1822
  if (!dispatch) {
1823
+ if (isPushProvider(entry.provider)) {
1824
+ return {
1825
+ ok: false,
1826
+ push: true,
1827
+ reason: `push provider "${entry.provider}" ingests via the OTLP receiver \u2014 nothing to poll`
1828
+ };
1829
+ }
1622
1830
  return { ok: false, reason: `unknown provider "${entry.provider}"` };
1623
1831
  }
1624
1832
  const creds = resolveEntryCredentials(dispatch, entry, env);
@@ -1653,7 +1861,7 @@ function buildRegistration(entry, graph, env = process.env) {
1653
1861
  };
1654
1862
  }
1655
1863
  async function validateConnectorEntry(entry, env = process.env, fetchImpl) {
1656
- const dispatch = PROVIDER_DISPATCH[entry.provider];
1864
+ const dispatch = PROVIDER_DISPATCH[entry.provider] ?? PUSH_PROVIDER_DISPATCH[entry.provider];
1657
1865
  if (!dispatch) {
1658
1866
  return { status: "unknown-provider", reason: `unknown provider "${entry.provider}"` };
1659
1867
  }
@@ -1694,10 +1902,58 @@ async function loadConnectorRegistrations(input) {
1694
1902
  if (!connectorMatchesProject(entry, project)) continue;
1695
1903
  const result = buildRegistration(entry, graph, env);
1696
1904
  if (result.ok) registrations.push(result.registration);
1697
- else onSkip?.(entry, result.reason);
1905
+ else if (!result.push) onSkip?.(entry, result.reason);
1698
1906
  }
1699
1907
  return registrations;
1700
1908
  }
1909
+ function resolvePushEntry(entry, env) {
1910
+ const dispatch = PUSH_PROVIDER_DISPATCH[entry.provider];
1911
+ if (!dispatch) {
1912
+ return PROVIDER_DISPATCH[entry.provider] ? {
1913
+ ok: false,
1914
+ outcome: {
1915
+ status: "not-push",
1916
+ reason: `provider "${entry.provider}" is polled, not provisioned \u2014 there is no drain to manage`
1917
+ }
1918
+ } : { ok: false, outcome: { status: "unknown-provider", reason: `unknown provider "${entry.provider}"` } };
1919
+ }
1920
+ const creds = resolveEntryCredentials(dispatch, entry, env);
1921
+ if (!creds.ok) {
1922
+ const status = creds.kind === "unset-env" ? "unset-env" : creds.kind === "missing-field" ? "missing-field" : "failed";
1923
+ return { ok: false, outcome: { status, reason: creds.reason } };
1924
+ }
1925
+ const options = entry.options ?? {};
1926
+ const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
1927
+ if (missingOpts.length > 0) {
1928
+ return {
1929
+ ok: false,
1930
+ outcome: { status: "missing-field", reason: `options missing required field(s): ${missingOpts.join(", ")}` }
1931
+ };
1932
+ }
1933
+ return { ok: true, dispatch, credentials: creds.credentials, options };
1934
+ }
1935
+ async function provisionConnector(entry, env = process.env, fetchImpl) {
1936
+ const resolved = resolvePushEntry(entry, env);
1937
+ if (!resolved.ok) return resolved.outcome;
1938
+ const result = await resolved.dispatch.provision({
1939
+ credentials: resolved.credentials,
1940
+ options: resolved.options,
1941
+ ...fetchImpl ? { fetchImpl } : {}
1942
+ });
1943
+ if (!result.ok) return { status: "failed", reason: result.reason };
1944
+ return { status: "ok", ...result.options ? { options: result.options } : {}, ...result.note ? { note: result.note } : {} };
1945
+ }
1946
+ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
1947
+ const resolved = resolvePushEntry(entry, env);
1948
+ if (!resolved.ok) return resolved.outcome;
1949
+ const result = await resolved.dispatch.deprovision({
1950
+ credentials: resolved.credentials,
1951
+ options: resolved.options,
1952
+ ...fetchImpl ? { fetchImpl } : {}
1953
+ });
1954
+ if (!result.ok) return { status: "failed", reason: result.reason };
1955
+ return { status: "ok", ...result.note ? { note: result.note } : {} };
1956
+ }
1701
1957
 
1702
1958
  // src/unrouted.ts
1703
1959
  import { promises as fs } from "fs";
@@ -2493,9 +2749,12 @@ async function startDaemon(opts = {}) {
2493
2749
  }
2494
2750
 
2495
2751
  export {
2496
- PROVIDER_DISPATCH,
2497
- getProviderDispatch,
2752
+ isPushProvider,
2753
+ getProviderFieldSchema,
2754
+ knownProviderNames,
2498
2755
  validateConnectorEntry,
2756
+ provisionConnector,
2757
+ deprovisionConnector,
2499
2758
  readDaemonRecord,
2500
2759
  resolveNeatVersion,
2501
2760
  writeDaemonRecord,
@@ -2506,4 +2765,4 @@ export {
2506
2765
  resolveHost,
2507
2766
  startDaemon
2508
2767
  };
2509
- //# sourceMappingURL=chunk-X2AMX3QZ.js.map
2768
+ //# sourceMappingURL=chunk-5PVQJLPR.js.map