@neat.is/core 0.9.15 → 0.9.17-dev.20260914

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/cli.js CHANGED
@@ -5775,6 +5775,204 @@ async function clearActiveProfile(home = neatHome()) {
5775
5775
  });
5776
5776
  }
5777
5777
 
5778
+ // src/login-sso.ts
5779
+ import { createServer } from "http";
5780
+ import { spawn as spawn3 } from "child_process";
5781
+ import { randomBytes as randomBytes2 } from "crypto";
5782
+ var DEFAULT_CP_URL = "https://api.neat.is";
5783
+ var DEFAULT_WEB_URL = "https://app.neat.is";
5784
+ var CALLBACK_TIMEOUT_MS = 5 * 6e4;
5785
+ function resolveCpUrl(env = process.env, override) {
5786
+ const v = override ?? env.NEAT_CP_URL;
5787
+ return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
5788
+ }
5789
+ function resolveWebUrl(env = process.env, override) {
5790
+ const v = override ?? env.NEAT_WEB_URL;
5791
+ return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
5792
+ }
5793
+ function pickProject(projects, want) {
5794
+ if (want) {
5795
+ const p = projects.find((x) => x.id === want || x.name === want);
5796
+ if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
5797
+ return { project: p };
5798
+ }
5799
+ const running = projects.filter((p) => p.status === "running");
5800
+ if (running.length === 1) return { project: running[0] };
5801
+ if (running.length === 0) {
5802
+ const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
5803
+ return {
5804
+ error: {
5805
+ code: 1,
5806
+ message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
5807
+ }
5808
+ };
5809
+ }
5810
+ return {
5811
+ error: {
5812
+ code: 2,
5813
+ message: `several running projects \u2014 pass --project <name>: ${running.map((p) => p.name).join(", ")}`
5814
+ }
5815
+ };
5816
+ }
5817
+ async function exchangeCredential(cpUrl, accessToken, opts, deps) {
5818
+ const fetchImpl = deps.fetchImpl ?? fetch;
5819
+ const auth = { authorization: `Bearer ${accessToken}` };
5820
+ let meRes;
5821
+ try {
5822
+ meRes = await fetchImpl(`${cpUrl}/me`, { headers: auth });
5823
+ } catch (e) {
5824
+ return { error: { code: 3, message: `can't reach the control plane at ${cpUrl} \u2014 ${e.message}` } };
5825
+ }
5826
+ if (meRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
5827
+ if (!meRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${meRes.status} on /me` } };
5828
+ const me = await meRes.json().catch(() => ({}));
5829
+ const projects = Array.isArray(me.projects) ? me.projects : [];
5830
+ const picked = pickProject(projects, opts.project);
5831
+ if ("error" in picked) return picked;
5832
+ const project = picked.project;
5833
+ let credRes;
5834
+ try {
5835
+ credRes = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/cli-credential`, {
5836
+ headers: auth
5837
+ });
5838
+ } catch (e) {
5839
+ return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
5840
+ }
5841
+ if (credRes.status === 409) {
5842
+ return {
5843
+ error: {
5844
+ code: 1,
5845
+ message: `project "${project.name}" isn't provisioned yet (status: ${project.status}) \u2014 provision it first`
5846
+ }
5847
+ };
5848
+ }
5849
+ if (credRes.status === 404) return { error: { code: 1, message: `project "${project.name}" was not found, or isn't yours` } };
5850
+ if (credRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
5851
+ if (!credRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${credRes.status} for the credential` } };
5852
+ const cred = await credRes.json().catch(() => ({}));
5853
+ if (!cred.endpoint || !cred.authToken) {
5854
+ return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
5855
+ }
5856
+ return { project, cred };
5857
+ }
5858
+ function defaultOpenBrowser(url) {
5859
+ const platform = process.platform;
5860
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
5861
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
5862
+ try {
5863
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
5864
+ child.on("error", () => {
5865
+ });
5866
+ child.unref();
5867
+ return true;
5868
+ } catch {
5869
+ return false;
5870
+ }
5871
+ }
5872
+ async function loopbackReceiveToken(webUrl, deps, opts = {}) {
5873
+ const out = deps.out ?? (() => {
5874
+ });
5875
+ const openFn = deps.openBrowser ?? defaultOpenBrowser;
5876
+ const state = randomBytes2(16).toString("hex");
5877
+ return new Promise((resolve) => {
5878
+ let settled = false;
5879
+ let timer;
5880
+ const done = (r) => {
5881
+ if (settled) return;
5882
+ settled = true;
5883
+ clearTimeout(timer);
5884
+ try {
5885
+ server.close();
5886
+ } catch {
5887
+ }
5888
+ resolve(r);
5889
+ };
5890
+ const server = createServer((req, res) => {
5891
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
5892
+ if (reqUrl.pathname !== "/callback") {
5893
+ res.writeHead(404);
5894
+ res.end();
5895
+ return;
5896
+ }
5897
+ const token = reqUrl.searchParams.get("token");
5898
+ const gotState = reqUrl.searchParams.get("state");
5899
+ if (!token || gotState !== state) {
5900
+ res.writeHead(400, { "content-type": "text/html" });
5901
+ res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
5902
+ done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
5903
+ return;
5904
+ }
5905
+ res.writeHead(200, { "content-type": "text/html" });
5906
+ res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
5907
+ done({ token });
5908
+ });
5909
+ server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
5910
+ timer = setTimeout(
5911
+ () => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
5912
+ opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
5913
+ );
5914
+ server.listen(0, "127.0.0.1", () => {
5915
+ const addr = server.address();
5916
+ const port = typeof addr === "object" && addr ? addr.port : 0;
5917
+ const callback = `http://127.0.0.1:${port}/callback`;
5918
+ const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
5919
+ out("Opening your browser to log in to NEAT\u2026");
5920
+ out(`If it doesn't open, visit:
5921
+ ${authUrl}`);
5922
+ openFn(authUrl);
5923
+ });
5924
+ });
5925
+ }
5926
+ async function runSsoLogin(opts, deps) {
5927
+ const out = deps.out ?? ((l) => console.log(l));
5928
+ const err = deps.err ?? ((l) => console.error(l));
5929
+ let accessToken = opts.ssoToken;
5930
+ if (!accessToken) {
5931
+ const lb = await loopbackReceiveToken(opts.webUrl, deps, { timeoutMs: opts.timeoutMs ?? CALLBACK_TIMEOUT_MS });
5932
+ if ("error" in lb) {
5933
+ err(`neat login: ${lb.error.message}`);
5934
+ return lb.error.code;
5935
+ }
5936
+ accessToken = lb.token;
5937
+ }
5938
+ const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
5939
+ if ("error" in ex) {
5940
+ err(`neat login: ${ex.error.message}`);
5941
+ return ex.error.code;
5942
+ }
5943
+ const { project, cred } = ex;
5944
+ await upsertProfile(
5945
+ { name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
5946
+ { makeActive: true, ...deps.home ? { home: deps.home } : {} }
5947
+ );
5948
+ if (opts.json) {
5949
+ out(
5950
+ JSON.stringify(
5951
+ {
5952
+ status: "logged-in",
5953
+ profile: opts.name,
5954
+ project: project.name,
5955
+ endpoint: cred.endpoint,
5956
+ ...cred.ingestEndpoint ? { ingestEndpoint: cred.ingestEndpoint } : {}
5957
+ },
5958
+ null,
5959
+ 2
5960
+ )
5961
+ );
5962
+ } else {
5963
+ out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
5964
+ out("The neat CLI and the MCP server now read this hosted graph by default.");
5965
+ if (cred.ingestEndpoint && cred.otelToken) {
5966
+ out("");
5967
+ out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
5968
+ out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
5969
+ out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
5970
+ }
5971
+ out("Run `neat logout` to switch back to your local daemon.");
5972
+ }
5973
+ return 0;
5974
+ }
5975
+
5778
5976
  // src/login-cli.ts
5779
5977
  var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
5780
5978
  var PROBE_TOTAL_BUDGET_MS = 12e4;
@@ -5787,11 +5985,12 @@ function readFlagValue(argv, i) {
5787
5985
  return { value: argv[i + 1], next: i + 1 };
5788
5986
  }
5789
5987
  function parseLoginArgs(argv) {
5790
- const parsed = { name: DEFAULT_PROFILE_NAME, json: false, help: false };
5988
+ const parsed = { name: DEFAULT_PROFILE_NAME, json: false, help: false, browser: false };
5791
5989
  for (let i = 0; i < argv.length; i++) {
5792
5990
  const arg = argv[i];
5793
5991
  if (arg === "-h" || arg === "--help") parsed.help = true;
5794
5992
  else if (arg === "--json") parsed.json = true;
5993
+ else if (arg === "--browser") parsed.browser = true;
5795
5994
  else if (arg === "--endpoint" || arg.startsWith("--endpoint=")) {
5796
5995
  const { value, next } = readFlagValue(argv, i);
5797
5996
  parsed.endpoint = value;
@@ -5800,6 +5999,22 @@ function parseLoginArgs(argv) {
5800
5999
  const { value, next } = readFlagValue(argv, i);
5801
6000
  parsed.token = value;
5802
6001
  i = next;
6002
+ } else if (arg === "--sso-token" || arg.startsWith("--sso-token=")) {
6003
+ const { value, next } = readFlagValue(argv, i);
6004
+ parsed.ssoToken = value;
6005
+ i = next;
6006
+ } else if (arg === "--cp-url" || arg.startsWith("--cp-url=")) {
6007
+ const { value, next } = readFlagValue(argv, i);
6008
+ parsed.cpUrl = value;
6009
+ i = next;
6010
+ } else if (arg === "--web-url" || arg.startsWith("--web-url=")) {
6011
+ const { value, next } = readFlagValue(argv, i);
6012
+ parsed.webUrl = value;
6013
+ i = next;
6014
+ } else if (arg === "--project" || arg.startsWith("--project=")) {
6015
+ const { value, next } = readFlagValue(argv, i);
6016
+ parsed.project = value;
6017
+ i = next;
5803
6018
  } else if (arg === "--name" || arg.startsWith("--name=")) {
5804
6019
  const { value, next } = readFlagValue(argv, i);
5805
6020
  if (value && value.length > 0) parsed.name = value;
@@ -5812,12 +6027,19 @@ function parseLoginArgs(argv) {
5812
6027
  return parsed;
5813
6028
  }
5814
6029
  function printLoginHelp(out) {
5815
- out("usage: neat login [--endpoint <url>] [--token <token>] [--name <name>] [--json]");
6030
+ out("usage: neat login [--browser | --endpoint <url> --token <token>] [--name <name>] [--json]");
5816
6031
  out(" Connect this machine to a hosted NEAT and make it the default for the");
5817
- out(" neat CLI and the MCP server. Omit --endpoint / --token to be prompted");
5818
- out(" (the token is read without echo). --name labels the profile (default");
5819
- out(' "hosted"). The token can also come from NEAT_LOGIN_TOKEN.');
5820
- out(" Exit 0 on success, 1 rejected token/endpoint, 2 misuse, 3 unreachable.");
6032
+ out(" neat CLI and the MCP server. Methods:");
6033
+ out(" --browser log in through your browser, then connect the");
6034
+ out(" account's running project (Method 1)");
6035
+ out(" --sso-token <jwt> exchange a pasted session token (headless / scripts)");
6036
+ out(" --endpoint <url> --token <token>");
6037
+ out(" paste a daemon endpoint + token directly, or omit");
6038
+ out(" both to be prompted (the token is read without echo)");
6039
+ out(" --project <name|id> picks among several running projects. --cp-url / --web-url");
6040
+ out(" (or NEAT_CP_URL / NEAT_WEB_URL) override the hosted endpoints; --name labels");
6041
+ out(' the profile (default "hosted"); the paste token can also come from NEAT_LOGIN_TOKEN.');
6042
+ out(" Exit 0 on success, 1 rejected/needs-action, 2 misuse, 3 unreachable.");
5821
6043
  }
5822
6044
  function isTimeoutError(err) {
5823
6045
  const name = err?.name;
@@ -5875,6 +6097,25 @@ async function runLoginCommand(argv, deps = {}) {
5875
6097
  err(`neat login: ${args.error}`);
5876
6098
  return 2;
5877
6099
  }
6100
+ if (args.browser || args.ssoToken) {
6101
+ return runSsoLogin(
6102
+ {
6103
+ cpUrl: resolveCpUrl(env, args.cpUrl),
6104
+ webUrl: resolveWebUrl(env, args.webUrl),
6105
+ ssoToken: args.ssoToken,
6106
+ name: args.name,
6107
+ project: args.project,
6108
+ json: args.json
6109
+ },
6110
+ {
6111
+ fetchImpl,
6112
+ out,
6113
+ err,
6114
+ ...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
6115
+ ...deps.home ? { home: deps.home } : {}
6116
+ }
6117
+ );
6118
+ }
5878
6119
  let endpoint = args.endpoint;
5879
6120
  if (!endpoint) endpoint = (await readLine("Hosted NEAT endpoint (https://\u2026): "))?.trim();
5880
6121
  if (!endpoint) {