@neat.is/core 0.9.15 → 0.9.16

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.cjs CHANGED
@@ -30249,6 +30249,205 @@ async function clearActiveProfile(home = neatHome3()) {
30249
30249
  });
30250
30250
  }
30251
30251
 
30252
+ // src/login-sso.ts
30253
+ init_cjs_shims();
30254
+ var import_node_http2 = require("http");
30255
+ var import_node_child_process4 = require("child_process");
30256
+ var import_node_crypto6 = require("crypto");
30257
+ var DEFAULT_CP_URL = "https://api.neat.is";
30258
+ var DEFAULT_WEB_URL = "https://app.neat.is";
30259
+ var CALLBACK_TIMEOUT_MS = 5 * 6e4;
30260
+ function resolveCpUrl(env = process.env, override) {
30261
+ const v = override ?? env.NEAT_CP_URL;
30262
+ return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
30263
+ }
30264
+ function resolveWebUrl(env = process.env, override) {
30265
+ const v = override ?? env.NEAT_WEB_URL;
30266
+ return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
30267
+ }
30268
+ function pickProject(projects, want) {
30269
+ if (want) {
30270
+ const p = projects.find((x) => x.id === want || x.name === want);
30271
+ if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
30272
+ return { project: p };
30273
+ }
30274
+ const running = projects.filter((p) => p.status === "running");
30275
+ if (running.length === 1) return { project: running[0] };
30276
+ if (running.length === 0) {
30277
+ const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
30278
+ return {
30279
+ error: {
30280
+ code: 1,
30281
+ message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
30282
+ }
30283
+ };
30284
+ }
30285
+ return {
30286
+ error: {
30287
+ code: 2,
30288
+ message: `several running projects \u2014 pass --project <name>: ${running.map((p) => p.name).join(", ")}`
30289
+ }
30290
+ };
30291
+ }
30292
+ async function exchangeCredential(cpUrl, accessToken, opts, deps) {
30293
+ const fetchImpl = deps.fetchImpl ?? fetch;
30294
+ const auth = { authorization: `Bearer ${accessToken}` };
30295
+ let meRes;
30296
+ try {
30297
+ meRes = await fetchImpl(`${cpUrl}/me`, { headers: auth });
30298
+ } catch (e) {
30299
+ return { error: { code: 3, message: `can't reach the control plane at ${cpUrl} \u2014 ${e.message}` } };
30300
+ }
30301
+ if (meRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
30302
+ if (!meRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${meRes.status} on /me` } };
30303
+ const me = await meRes.json().catch(() => ({}));
30304
+ const projects = Array.isArray(me.projects) ? me.projects : [];
30305
+ const picked = pickProject(projects, opts.project);
30306
+ if ("error" in picked) return picked;
30307
+ const project = picked.project;
30308
+ let credRes;
30309
+ try {
30310
+ credRes = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/cli-credential`, {
30311
+ headers: auth
30312
+ });
30313
+ } catch (e) {
30314
+ return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
30315
+ }
30316
+ if (credRes.status === 409) {
30317
+ return {
30318
+ error: {
30319
+ code: 1,
30320
+ message: `project "${project.name}" isn't provisioned yet (status: ${project.status}) \u2014 provision it first`
30321
+ }
30322
+ };
30323
+ }
30324
+ if (credRes.status === 404) return { error: { code: 1, message: `project "${project.name}" was not found, or isn't yours` } };
30325
+ if (credRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
30326
+ if (!credRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${credRes.status} for the credential` } };
30327
+ const cred = await credRes.json().catch(() => ({}));
30328
+ if (!cred.endpoint || !cred.authToken) {
30329
+ return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
30330
+ }
30331
+ return { project, cred };
30332
+ }
30333
+ function defaultOpenBrowser(url) {
30334
+ const platform = process.platform;
30335
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
30336
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
30337
+ try {
30338
+ const child = (0, import_node_child_process4.spawn)(cmd, args, { detached: true, stdio: "ignore" });
30339
+ child.on("error", () => {
30340
+ });
30341
+ child.unref();
30342
+ return true;
30343
+ } catch {
30344
+ return false;
30345
+ }
30346
+ }
30347
+ async function loopbackReceiveToken(webUrl, deps, opts = {}) {
30348
+ const out = deps.out ?? (() => {
30349
+ });
30350
+ const openFn = deps.openBrowser ?? defaultOpenBrowser;
30351
+ const state = (0, import_node_crypto6.randomBytes)(16).toString("hex");
30352
+ return new Promise((resolve) => {
30353
+ let settled = false;
30354
+ let timer;
30355
+ const done = (r) => {
30356
+ if (settled) return;
30357
+ settled = true;
30358
+ clearTimeout(timer);
30359
+ try {
30360
+ server.close();
30361
+ } catch {
30362
+ }
30363
+ resolve(r);
30364
+ };
30365
+ const server = (0, import_node_http2.createServer)((req, res) => {
30366
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
30367
+ if (reqUrl.pathname !== "/callback") {
30368
+ res.writeHead(404);
30369
+ res.end();
30370
+ return;
30371
+ }
30372
+ const token = reqUrl.searchParams.get("token");
30373
+ const gotState = reqUrl.searchParams.get("state");
30374
+ if (!token || gotState !== state) {
30375
+ res.writeHead(400, { "content-type": "text/html" });
30376
+ res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
30377
+ done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
30378
+ return;
30379
+ }
30380
+ res.writeHead(200, { "content-type": "text/html" });
30381
+ res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
30382
+ done({ token });
30383
+ });
30384
+ server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
30385
+ timer = setTimeout(
30386
+ () => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
30387
+ opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
30388
+ );
30389
+ server.listen(0, "127.0.0.1", () => {
30390
+ const addr = server.address();
30391
+ const port = typeof addr === "object" && addr ? addr.port : 0;
30392
+ const callback = `http://127.0.0.1:${port}/callback`;
30393
+ const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
30394
+ out("Opening your browser to log in to NEAT\u2026");
30395
+ out(`If it doesn't open, visit:
30396
+ ${authUrl}`);
30397
+ openFn(authUrl);
30398
+ });
30399
+ });
30400
+ }
30401
+ async function runSsoLogin(opts, deps) {
30402
+ const out = deps.out ?? ((l) => console.log(l));
30403
+ const err = deps.err ?? ((l) => console.error(l));
30404
+ let accessToken = opts.ssoToken;
30405
+ if (!accessToken) {
30406
+ const lb = await loopbackReceiveToken(opts.webUrl, deps, { timeoutMs: opts.timeoutMs ?? CALLBACK_TIMEOUT_MS });
30407
+ if ("error" in lb) {
30408
+ err(`neat login: ${lb.error.message}`);
30409
+ return lb.error.code;
30410
+ }
30411
+ accessToken = lb.token;
30412
+ }
30413
+ const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
30414
+ if ("error" in ex) {
30415
+ err(`neat login: ${ex.error.message}`);
30416
+ return ex.error.code;
30417
+ }
30418
+ const { project, cred } = ex;
30419
+ await upsertProfile(
30420
+ { name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
30421
+ { makeActive: true, ...deps.home ? { home: deps.home } : {} }
30422
+ );
30423
+ if (opts.json) {
30424
+ out(
30425
+ JSON.stringify(
30426
+ {
30427
+ status: "logged-in",
30428
+ profile: opts.name,
30429
+ project: project.name,
30430
+ endpoint: cred.endpoint,
30431
+ ...cred.ingestEndpoint ? { ingestEndpoint: cred.ingestEndpoint } : {}
30432
+ },
30433
+ null,
30434
+ 2
30435
+ )
30436
+ );
30437
+ } else {
30438
+ out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
30439
+ out("The neat CLI and the MCP server now read this hosted graph by default.");
30440
+ if (cred.ingestEndpoint && cred.otelToken) {
30441
+ out("");
30442
+ out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
30443
+ out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
30444
+ out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
30445
+ }
30446
+ out("Run `neat logout` to switch back to your local daemon.");
30447
+ }
30448
+ return 0;
30449
+ }
30450
+
30252
30451
  // src/login-cli.ts
30253
30452
  var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
30254
30453
  var PROBE_TOTAL_BUDGET_MS = 12e4;
@@ -30261,11 +30460,12 @@ function readFlagValue(argv, i) {
30261
30460
  return { value: argv[i + 1], next: i + 1 };
30262
30461
  }
30263
30462
  function parseLoginArgs(argv) {
30264
- const parsed = { name: DEFAULT_PROFILE_NAME, json: false, help: false };
30463
+ const parsed = { name: DEFAULT_PROFILE_NAME, json: false, help: false, browser: false };
30265
30464
  for (let i = 0; i < argv.length; i++) {
30266
30465
  const arg = argv[i];
30267
30466
  if (arg === "-h" || arg === "--help") parsed.help = true;
30268
30467
  else if (arg === "--json") parsed.json = true;
30468
+ else if (arg === "--browser") parsed.browser = true;
30269
30469
  else if (arg === "--endpoint" || arg.startsWith("--endpoint=")) {
30270
30470
  const { value, next } = readFlagValue(argv, i);
30271
30471
  parsed.endpoint = value;
@@ -30274,6 +30474,22 @@ function parseLoginArgs(argv) {
30274
30474
  const { value, next } = readFlagValue(argv, i);
30275
30475
  parsed.token = value;
30276
30476
  i = next;
30477
+ } else if (arg === "--sso-token" || arg.startsWith("--sso-token=")) {
30478
+ const { value, next } = readFlagValue(argv, i);
30479
+ parsed.ssoToken = value;
30480
+ i = next;
30481
+ } else if (arg === "--cp-url" || arg.startsWith("--cp-url=")) {
30482
+ const { value, next } = readFlagValue(argv, i);
30483
+ parsed.cpUrl = value;
30484
+ i = next;
30485
+ } else if (arg === "--web-url" || arg.startsWith("--web-url=")) {
30486
+ const { value, next } = readFlagValue(argv, i);
30487
+ parsed.webUrl = value;
30488
+ i = next;
30489
+ } else if (arg === "--project" || arg.startsWith("--project=")) {
30490
+ const { value, next } = readFlagValue(argv, i);
30491
+ parsed.project = value;
30492
+ i = next;
30277
30493
  } else if (arg === "--name" || arg.startsWith("--name=")) {
30278
30494
  const { value, next } = readFlagValue(argv, i);
30279
30495
  if (value && value.length > 0) parsed.name = value;
@@ -30286,12 +30502,19 @@ function parseLoginArgs(argv) {
30286
30502
  return parsed;
30287
30503
  }
30288
30504
  function printLoginHelp(out) {
30289
- out("usage: neat login [--endpoint <url>] [--token <token>] [--name <name>] [--json]");
30505
+ out("usage: neat login [--browser | --endpoint <url> --token <token>] [--name <name>] [--json]");
30290
30506
  out(" Connect this machine to a hosted NEAT and make it the default for the");
30291
- out(" neat CLI and the MCP server. Omit --endpoint / --token to be prompted");
30292
- out(" (the token is read without echo). --name labels the profile (default");
30293
- out(' "hosted"). The token can also come from NEAT_LOGIN_TOKEN.');
30294
- out(" Exit 0 on success, 1 rejected token/endpoint, 2 misuse, 3 unreachable.");
30507
+ out(" neat CLI and the MCP server. Methods:");
30508
+ out(" --browser log in through your browser, then connect the");
30509
+ out(" account's running project (Method 1)");
30510
+ out(" --sso-token <jwt> exchange a pasted session token (headless / scripts)");
30511
+ out(" --endpoint <url> --token <token>");
30512
+ out(" paste a daemon endpoint + token directly, or omit");
30513
+ out(" both to be prompted (the token is read without echo)");
30514
+ out(" --project <name|id> picks among several running projects. --cp-url / --web-url");
30515
+ out(" (or NEAT_CP_URL / NEAT_WEB_URL) override the hosted endpoints; --name labels");
30516
+ out(' the profile (default "hosted"); the paste token can also come from NEAT_LOGIN_TOKEN.');
30517
+ out(" Exit 0 on success, 1 rejected/needs-action, 2 misuse, 3 unreachable.");
30295
30518
  }
30296
30519
  function isTimeoutError(err) {
30297
30520
  const name = err?.name;
@@ -30349,6 +30572,25 @@ async function runLoginCommand(argv, deps = {}) {
30349
30572
  err(`neat login: ${args.error}`);
30350
30573
  return 2;
30351
30574
  }
30575
+ if (args.browser || args.ssoToken) {
30576
+ return runSsoLogin(
30577
+ {
30578
+ cpUrl: resolveCpUrl(env, args.cpUrl),
30579
+ webUrl: resolveWebUrl(env, args.webUrl),
30580
+ ssoToken: args.ssoToken,
30581
+ name: args.name,
30582
+ project: args.project,
30583
+ json: args.json
30584
+ },
30585
+ {
30586
+ fetchImpl,
30587
+ out,
30588
+ err,
30589
+ ...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
30590
+ ...deps.home ? { home: deps.home } : {}
30591
+ }
30592
+ );
30593
+ }
30352
30594
  let endpoint2 = args.endpoint;
30353
30595
  if (!endpoint2) endpoint2 = (await readLine("Hosted NEAT endpoint (https://\u2026): "))?.trim();
30354
30596
  if (!endpoint2) {