@nlxai/cli 1.2.2-alpha.8 → 1.2.2-alpha.9
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/lib/commands/auth/login.js +9 -1
- package/lib/commands/login.js +109 -101
- package/package.json +2 -2
|
@@ -4,9 +4,16 @@ import open from "open";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { consola } from "consola";
|
|
7
|
-
import keytar from "keytar";
|
|
8
7
|
export const ACCOUNTS_PATH = path.join(os.homedir(), ".nlx-cli-auth.json");
|
|
8
|
+
let _keytar;
|
|
9
|
+
async function getKeytar() {
|
|
10
|
+
if (_keytar)
|
|
11
|
+
return _keytar;
|
|
12
|
+
_keytar = await import("keytar");
|
|
13
|
+
return _keytar;
|
|
14
|
+
}
|
|
9
15
|
async function saveTokens(account, tokenData) {
|
|
16
|
+
const keytar = await getKeytar();
|
|
10
17
|
await keytar.setPassword("nlx-cli", account, JSON.stringify(tokenData));
|
|
11
18
|
}
|
|
12
19
|
async function loadTokens() {
|
|
@@ -23,6 +30,7 @@ async function loadTokens() {
|
|
|
23
30
|
const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
|
|
24
31
|
const accounts = JSON.parse(data);
|
|
25
32
|
if (accounts.currentAccount) {
|
|
33
|
+
const keytar = await getKeytar();
|
|
26
34
|
const res = await keytar.getPassword("nlx-cli", accounts.currentAccount);
|
|
27
35
|
if (res)
|
|
28
36
|
return [accounts.currentAccount, JSON.parse(res)];
|
package/lib/commands/login.js
CHANGED
|
@@ -4,142 +4,150 @@ import open from "open";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { consola } from "consola";
|
|
7
|
-
import keytar from "keytar";
|
|
8
7
|
const ACCOUNTS_PATH = path.join(os.homedir(), ".nlx-cli-auth.json");
|
|
9
8
|
async function saveTokens(account, tokenData) {
|
|
10
|
-
|
|
9
|
+
await keytar.setPassword("nlx-cli", account, JSON.stringify(tokenData));
|
|
11
10
|
}
|
|
12
11
|
async function loadTokens() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return [accounts.currentAccount, JSON.parse(res)];
|
|
20
|
-
}
|
|
21
|
-
throw new Error("No tokens found for current account");
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
throw new Error("Failed to load tokens");
|
|
12
|
+
try {
|
|
13
|
+
const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
|
|
14
|
+
const accounts = JSON.parse(data);
|
|
15
|
+
if (accounts.currentAccount) {
|
|
16
|
+
const res = await keytar.getPassword("nlx-cli", accounts.currentAccount);
|
|
17
|
+
if (res) return [accounts.currentAccount, JSON.parse(res)];
|
|
25
18
|
}
|
|
19
|
+
throw new Error("No tokens found for current account");
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("Failed to load tokens");
|
|
22
|
+
}
|
|
26
23
|
}
|
|
27
24
|
async function refreshTokenIfNeeded() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
consola.error("Error loading tokens");
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
if (!tokens || !tokens.refresh_token)
|
|
37
|
-
return null;
|
|
38
|
-
// Check expiry
|
|
39
|
-
const now = Math.floor(Date.now() / 1000);
|
|
40
|
-
if (tokens.expires_in &&
|
|
41
|
-
tokens.obtained_at &&
|
|
42
|
-
now < tokens.obtained_at + tokens.expires_in - 60) {
|
|
43
|
-
consola.debug("Access token is still valid.");
|
|
44
|
-
return tokens.access_token;
|
|
45
|
-
}
|
|
46
|
-
consola.debug("Access token is expired or invalid. Refreshing...");
|
|
47
|
-
// Refresh
|
|
48
|
-
const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
51
|
-
body: new URLSearchParams({
|
|
52
|
-
grant_type: "refresh_token",
|
|
53
|
-
client_id: CLIENT_ID,
|
|
54
|
-
refresh_token: tokens.refresh_token,
|
|
55
|
-
}),
|
|
56
|
-
});
|
|
57
|
-
const newTokens = await res.json();
|
|
58
|
-
if (newTokens.access_token) {
|
|
59
|
-
newTokens.refresh_token = newTokens.refresh_token || tokens.refresh_token;
|
|
60
|
-
newTokens.obtained_at = now;
|
|
61
|
-
await saveTokens(account, newTokens);
|
|
62
|
-
return newTokens.access_token;
|
|
63
|
-
}
|
|
25
|
+
let account, tokens;
|
|
26
|
+
try {
|
|
27
|
+
[account, tokens] = await loadTokens();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
consola.error("Error loading tokens");
|
|
64
30
|
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!tokens || !tokens.refresh_token) return null;
|
|
33
|
+
// Check expiry
|
|
34
|
+
const now = Math.floor(Date.now() / 1000);
|
|
35
|
+
if (
|
|
36
|
+
tokens.expires_in &&
|
|
37
|
+
tokens.obtained_at &&
|
|
38
|
+
now < tokens.obtained_at + tokens.expires_in - 60
|
|
39
|
+
) {
|
|
40
|
+
consola.debug("Access token is still valid.");
|
|
41
|
+
return tokens.access_token;
|
|
42
|
+
}
|
|
43
|
+
consola.debug("Access token is expired or invalid. Refreshing...");
|
|
44
|
+
// Refresh
|
|
45
|
+
const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
48
|
+
body: new URLSearchParams({
|
|
49
|
+
grant_type: "refresh_token",
|
|
50
|
+
client_id: CLIENT_ID,
|
|
51
|
+
refresh_token: tokens.refresh_token,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
const newTokens = await res.json();
|
|
55
|
+
if (newTokens.access_token) {
|
|
56
|
+
newTokens.refresh_token = newTokens.refresh_token || tokens.refresh_token;
|
|
57
|
+
newTokens.obtained_at = now;
|
|
58
|
+
await saveTokens(account, newTokens);
|
|
59
|
+
return newTokens.access_token;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
65
62
|
}
|
|
66
63
|
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || "nlxdev.us.auth0.com"; // e.g. 'dev-xxxxxx.us.auth0.com'
|
|
67
|
-
const CLIENT_ID =
|
|
68
|
-
|
|
64
|
+
const CLIENT_ID =
|
|
65
|
+
process.env.AUTH0_CLIENT_ID || "A0qluq7wJQjFjMLle9pvrWWaVHM1QHE3";
|
|
66
|
+
const AUDIENCE =
|
|
67
|
+
process.env.AUTH0_AUDIENCE || "https://nlxdev.us.auth0.com/api/v2/";
|
|
69
68
|
export const loginCommand = new Command("login")
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
.description("Authenticate with NLX")
|
|
70
|
+
.action(async () => {
|
|
72
71
|
// Step 1: Start device flow
|
|
73
|
-
const deviceCodeRes = await fetch(
|
|
72
|
+
const deviceCodeRes = await fetch(
|
|
73
|
+
`https://${AUTH0_DOMAIN}/oauth/device/code`,
|
|
74
|
+
{
|
|
74
75
|
method: "POST",
|
|
75
76
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
76
77
|
body: new URLSearchParams({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
scope: "openid profile email offline_access",
|
|
80
|
+
audience: AUDIENCE,
|
|
80
81
|
}),
|
|
81
|
-
|
|
82
|
+
},
|
|
83
|
+
);
|
|
82
84
|
const deviceCodeData = await deviceCodeRes.json();
|
|
83
85
|
open(deviceCodeData.verification_uri_complete);
|
|
84
|
-
consola.box(
|
|
86
|
+
consola.box(
|
|
87
|
+
`Please visit ${deviceCodeData.verification_uri_complete} and enter code: ${deviceCodeData.user_code}`,
|
|
88
|
+
);
|
|
85
89
|
// Step 2: Poll for token
|
|
86
90
|
let tokenData;
|
|
87
91
|
while (!tokenData) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
92
|
+
await new Promise((r) => setTimeout(r, deviceCodeData.interval * 1000));
|
|
93
|
+
const tokenRes = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
96
|
+
body: new URLSearchParams({
|
|
97
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
98
|
+
device_code: deviceCodeData.device_code,
|
|
99
|
+
client_id: CLIENT_ID,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
const resData = await tokenRes.json();
|
|
103
|
+
if (resData.access_token) {
|
|
104
|
+
tokenData = resData;
|
|
105
|
+
} else if (resData.error !== "authorization_pending") {
|
|
106
|
+
consola.error("Error:", resData.error_description || resData.error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
106
109
|
}
|
|
107
110
|
// Step 3: Fetch user object
|
|
108
111
|
let accounts = { currentAccount: null, accounts: [] };
|
|
109
112
|
if (fs.existsSync(ACCOUNTS_PATH)) {
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
|
|
114
|
+
accounts = JSON.parse(data);
|
|
112
115
|
}
|
|
113
116
|
const userRes = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${tokenData.access_token}`,
|
|
119
|
+
},
|
|
117
120
|
});
|
|
118
121
|
const userData = await userRes.json();
|
|
119
122
|
if (!accounts.currentAccount) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
await fs.promises.writeFile(
|
|
124
|
+
ACCOUNTS_PATH,
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
currentAccount: userData.email,
|
|
127
|
+
accounts: [userData.email],
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
} else if (accounts.currentAccount !== userData.email) {
|
|
131
|
+
accounts.currentAccount = userData.email;
|
|
132
|
+
await fs.promises.writeFile(
|
|
133
|
+
ACCOUNTS_PATH,
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
currentAccount: userData.email,
|
|
136
|
+
accounts: [userData.email, ...accounts.accounts],
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
131
139
|
}
|
|
132
140
|
// Step 4: Store token securely
|
|
133
141
|
tokenData.obtained_at = Math.floor(Date.now() / 1000);
|
|
134
142
|
await saveTokens(userData.email, tokenData);
|
|
135
143
|
consola.success("Login successful! Access token stored securely.");
|
|
136
|
-
});
|
|
144
|
+
});
|
|
137
145
|
// Example usage: get a valid access token
|
|
138
146
|
export async function ensureToken() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
const accessToken = await refreshTokenIfNeeded();
|
|
148
|
+
if (!accessToken) {
|
|
149
|
+
consola.error("Not authenticated. Please run 'login' first.");
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
return accessToken;
|
|
145
153
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nlxai/cli",
|
|
3
|
-
"version": "1.2.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.9",
|
|
4
4
|
"description": "Tools for integrating with NLX apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NLX",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"@vitest/ui": "^3.2.4",
|
|
59
59
|
"vitest": "^3.2.4"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "690c384cd98ec1fba1811e6913c8c5a5b498bd59"
|
|
62
62
|
}
|