nexarch 0.13.4 → 0.13.5
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/commands/login.js +86 -11
- package/dist/index.js +7 -1
- package/package.json +39 -39
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from "http";
|
|
2
2
|
import { createServer as createNetServer } from "net";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
3
|
+
import { createHash, randomBytes } from "crypto";
|
|
4
|
+
import * as readline from "node:readline/promises";
|
|
4
5
|
import { execFile } from "child_process";
|
|
5
6
|
import { saveCredentials } from "../lib/credentials.js";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -136,6 +137,68 @@ function waitForCallback(port, expectedState) {
|
|
|
136
137
|
});
|
|
137
138
|
});
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether the browser that approves this login can reach the CLI at
|
|
142
|
+
* localhost. It can't over SSH (the browser is on the person's own machine,
|
|
143
|
+
* the CLI on the remote one) or on a Linux machine with no display; the
|
|
144
|
+
* loopback callback then lands on the wrong machine and the login hangs.
|
|
145
|
+
* `--no-browser` forces paste mode, `--browser` forces the loopback flow.
|
|
146
|
+
*/
|
|
147
|
+
export function shouldUsePasteLogin(args, env = process.env, platform = process.platform) {
|
|
148
|
+
if (args.includes("--browser"))
|
|
149
|
+
return false;
|
|
150
|
+
if (args.includes("--no-browser"))
|
|
151
|
+
return true;
|
|
152
|
+
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
|
|
153
|
+
return true;
|
|
154
|
+
return platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
155
|
+
}
|
|
156
|
+
/** A random verifier the CLI keeps, and the challenge it sends: base64url(SHA-256(verifier)). */
|
|
157
|
+
export function createPasteChallenge() {
|
|
158
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
159
|
+
return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
|
|
160
|
+
}
|
|
161
|
+
/** Mistakes worth letting someone paste again rather than starting over. */
|
|
162
|
+
const RETRYABLE_EXCHANGE_ERRORS = new Set(["malformed", "bad_signature"]);
|
|
163
|
+
async function pasteLogin(qp, verifier) {
|
|
164
|
+
const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
|
|
165
|
+
console.log("\nThis terminal can't receive the browser's reply directly (for example over SSH),");
|
|
166
|
+
console.log("so you'll paste a one-time code instead.\n");
|
|
167
|
+
console.log(" 1. Open this link in a browser on any machine:\n");
|
|
168
|
+
console.log(` ${authUrl}\n`);
|
|
169
|
+
console.log(" 2. Sign in and authorize.");
|
|
170
|
+
console.log(" 3. Copy the code the page shows and paste it here.\n");
|
|
171
|
+
if (!process.stdin.isTTY) {
|
|
172
|
+
throw new Error("Paste login needs an interactive terminal to paste the code into. Run this command directly in a terminal.");
|
|
173
|
+
}
|
|
174
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
175
|
+
try {
|
|
176
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
177
|
+
const code = (await rl.question("Code: ")).trim();
|
|
178
|
+
if (!code)
|
|
179
|
+
continue;
|
|
180
|
+
const response = await fetch(`${NEXARCH_URL}/api/auth/cli/exchange`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
body: JSON.stringify({ code, verifier }),
|
|
184
|
+
});
|
|
185
|
+
const body = (await response.json().catch(() => ({})));
|
|
186
|
+
if (response.ok && body.token && body.companyId) {
|
|
187
|
+
return { token: body.token, companyId: body.companyId, companyName: body.companyName, companyCode: body.companyCode };
|
|
188
|
+
}
|
|
189
|
+
const message = body.message ?? `Login failed (${response.status}).`;
|
|
190
|
+
if (body.error && RETRYABLE_EXCHANGE_ERRORS.has(body.error) && attempt < 3) {
|
|
191
|
+
console.log(`\n${message}\n`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw new Error(message);
|
|
195
|
+
}
|
|
196
|
+
throw new Error("No code was entered. Run the command again to get a new link.");
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
rl.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
139
202
|
function getArgValue(args, flag) {
|
|
140
203
|
const idx = args.indexOf(flag);
|
|
141
204
|
if (idx === -1)
|
|
@@ -148,21 +211,33 @@ function getArgValue(args, flag) {
|
|
|
148
211
|
export async function login(args) {
|
|
149
212
|
printLoginBanner();
|
|
150
213
|
const state = generateState();
|
|
151
|
-
const port = await findFreePort();
|
|
152
214
|
const requestedCompany = getArgValue(args, "--company");
|
|
153
215
|
const requestedProfile = getArgValue(args, "--profile");
|
|
154
216
|
const isCapabilityProfile = (value) => PROFILE_CHOICES.some((c) => c.value === value);
|
|
155
217
|
const profile = isCapabilityProfile(requestedProfile) ? requestedProfile : await promptCapabilityProfile();
|
|
156
|
-
const qp = new URLSearchParams({ port: String(port), state, profile });
|
|
157
|
-
if (requestedCompany)
|
|
158
|
-
qp.set("company", requestedCompany);
|
|
159
|
-
const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
|
|
160
218
|
console.log(`\nCapability profile: ${profile}`);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
let result;
|
|
220
|
+
if (shouldUsePasteLogin(args)) {
|
|
221
|
+
const { verifier, challenge } = createPasteChallenge();
|
|
222
|
+
const qp = new URLSearchParams({ mode: "paste", challenge, state, profile });
|
|
223
|
+
if (requestedCompany)
|
|
224
|
+
qp.set("company", requestedCompany);
|
|
225
|
+
result = await pasteLogin(qp, verifier);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const port = await findFreePort();
|
|
229
|
+
const qp = new URLSearchParams({ port: String(port), state, profile });
|
|
230
|
+
if (requestedCompany)
|
|
231
|
+
qp.set("company", requestedCompany);
|
|
232
|
+
const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
|
|
233
|
+
console.log("Opening Nexarch in your browser…");
|
|
234
|
+
console.log(`\n ${authUrl}\n`);
|
|
235
|
+
console.log("If the browser did not open, copy the URL above and paste it in manually.");
|
|
236
|
+
console.log("If your browser is on a different machine (for example over SSH), stop this and run it again with --no-browser.\n");
|
|
237
|
+
openBrowser(authUrl);
|
|
238
|
+
result = await waitForCallback(port, state);
|
|
239
|
+
}
|
|
240
|
+
const { token, companyId, companyName, companyCode } = result;
|
|
166
241
|
const expiresAt = new Date();
|
|
167
242
|
expiresAt.setDate(expiresAt.getDate() + 90);
|
|
168
243
|
saveCredentials({
|
package/dist/index.js
CHANGED
|
@@ -107,7 +107,12 @@ nexarch — Your architecture workspace for AI delivery.
|
|
|
107
107
|
|
|
108
108
|
Usage:
|
|
109
109
|
nexarch login Authenticate in browser and store company-scoped credentials
|
|
110
|
-
|
|
110
|
+
Options: --company <id>
|
|
111
|
+
--no-browser paste a one-time code instead of
|
|
112
|
+
receiving the browser's reply
|
|
113
|
+
locally; automatic over SSH or on
|
|
114
|
+
Linux with no display
|
|
115
|
+
--browser always use the local browser reply
|
|
111
116
|
nexarch --version Print the version and the path this copy runs from
|
|
112
117
|
nexarch enroll Bootstrap a headless (browserless) agent with a one-time
|
|
113
118
|
enrollment code — no login required. Writes the resulting
|
|
@@ -135,6 +140,7 @@ Usage:
|
|
|
135
140
|
registering anything; answer 'n' to pick a different one.
|
|
136
141
|
Options: --company <id|code> target a workspace directly
|
|
137
142
|
--yes accept the stored workspace without asking
|
|
143
|
+
--no-browser log in by pasting a one-time code (see login)
|
|
138
144
|
nexarch mcp-config Print MCP server config block for manual setup
|
|
139
145
|
Client list is registry-managed (see 'nexarch mcp-config --client <code>')
|
|
140
146
|
nexarch mcp-proxy Run as stdio MCP proxy (used by MCP clients)
|
package/package.json
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nexarch",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"nexarch",
|
|
7
|
-
"mcp",
|
|
8
|
-
"architecture",
|
|
9
|
-
"ai"
|
|
10
|
-
],
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
-
"homepage": "https://nexarch.ai",
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18"
|
|
16
|
-
},
|
|
17
|
-
"type": "module",
|
|
18
|
-
"bin": {
|
|
19
|
-
"nexarch": "dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"prepublishOnly": "tsc",
|
|
27
|
-
"dev": "tsx src/index.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/node": "^22",
|
|
33
|
-
"tsx": "^4",
|
|
34
|
-
"typescript": "^5"
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"yaml": "^2.9.0"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nexarch",
|
|
3
|
+
"version": "0.13.5",
|
|
4
|
+
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nexarch",
|
|
7
|
+
"mcp",
|
|
8
|
+
"architecture",
|
|
9
|
+
"ai"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
+
"homepage": "https://nexarch.ai",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"nexarch": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepublishOnly": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts && tsx scripts/test-paste-login.ts"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22",
|
|
33
|
+
"tsx": "^4",
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"yaml": "^2.9.0"
|
|
38
|
+
}
|
|
39
|
+
}
|