@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 +248 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +247 -6
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
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>
|
|
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.
|
|
5818
|
-
out("
|
|
5819
|
-
out('
|
|
5820
|
-
out("
|
|
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) {
|