-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathlogin.js
More file actions
150 lines (136 loc) · 4.91 KB
/
login.js
File metadata and controls
150 lines (136 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import readline from "node:readline";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { setConfigValue, getApiKey } from "../utils/config.js";
import { print, printError } from "../utils/common/output.js";
import { browserLogin } from "../utils/auth/browser-flow.js";
import { readSecret } from "../utils/common/prompt.js";
import { API_BASE, CONFIG_PATH } from "../utils/common/constants.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"));
function prompt(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
return new Promise((resolve) =>
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
})
);
}
function banner() {
const w = (s) => process.stderr.write(s + "\n");
w("");
w(` zerion cli v${pkg.version}`);
w(` Wallet analysis & autonomous trading for AI agents`);
w("");
}
function maskKey(key) {
if (!key || typeof key !== "string") return "(unknown)";
if (key.length <= 10) return key;
return `${key.slice(0, 6)}…${key.slice(-4)}`;
}
// Dashboard issues keys prefixed `zk_dev_` or `zk_prod_`. Older guidance
// mentioned `zk-` so we accept either to avoid rejecting valid keys.
function isValidKeyFormat(key) {
return typeof key === "string" && /^zk[_-]/.test(key);
}
function successBlock({ team, method, key }) {
const w = (s) => process.stderr.write(s + "\n");
w("");
w(`✓ Login successful!`);
if (team) w(` Team: ${team}`);
w(` API: ${API_BASE}`);
w(` Key: ${maskKey(key)}`);
w(` Config: ${CONFIG_PATH}`);
w(` Mode: ${method}`);
w("");
}
export default async function loginCmd(args, flags) {
const existingKey = getApiKey();
if (existingKey && !flags.force) {
process.stderr.write("Already logged in. Use --force to replace the current API key.\n");
print({ loggedIn: true, api: API_BASE, config: CONFIG_PATH, keyPrefix: maskKey(existingKey) });
return;
}
if (flags["api-key"]) {
if (flags.browser) {
process.stderr.write("Note: --api-key takes precedence over --browser.\n");
}
const key = flags["api-key"];
if (!isValidKeyFormat(key)) {
printError("invalid_key_format", "API keys start with 'zk_' (e.g. zk_dev_…)");
process.exit(1);
}
setConfigValue("apiKey", key);
print({ loggedIn: true, method: "api-key", api: API_BASE, config: CONFIG_PATH, keyPrefix: maskKey(key) });
return;
}
if (flags.browser) {
if (!flags.quiet) banner();
return runBrowser({ quiet: flags.quiet });
}
// Interactive menu needs a TTY. In non-TTY contexts (CI, pipes, containers),
// an interactive prompt blocks forever — fail loudly with a fix.
if (!process.stdin.isTTY) {
printError(
"no_tty",
"Interactive login requires a terminal. Use --browser, --api-key <key>, or set ZERION_API_KEY."
);
process.exit(1);
}
if (!flags.quiet) banner();
const w = (s) => process.stderr.write(s + "\n");
w(`Welcome! To get started, authenticate with your Zerion account.`);
w("");
w(` 1. Login with browser (recommended)`);
w(` 2. Enter API key manually`);
w("");
w(`Tip: You can also set ZERION_API_KEY environment variable`);
w(` API endpoint: ${API_BASE}`);
w("");
const choice = await prompt("Enter choice [1/2]: ");
if (choice === "" || choice === "1") {
return runBrowser({ quiet: flags.quiet });
}
if (choice !== "2") {
printError("invalid_choice", "Enter 1 or 2");
process.exit(1);
}
const key = await readSecret("Enter your Zerion API key: ", { mask: true });
if (!isValidKeyFormat(key)) {
printError("invalid_key_format", "API keys start with 'zk_' (e.g. zk_dev_…)");
process.exit(1);
}
setConfigValue("apiKey", key);
successBlock({ method: "api-key", key });
print({ loggedIn: true, method: "api-key", api: API_BASE });
}
async function runBrowser({ quiet = false } = {}) {
try {
const result = await browserLogin();
setConfigValue("apiKey", result.apiKey);
successBlock({ team: result.teamName || "(unknown)", method: "browser", key: result.apiKey });
print({
loggedIn: true,
method: "browser",
email: result.email,
team: result.teamName,
api: API_BASE,
config: CONFIG_PATH,
keyPrefix: maskKey(result.apiKey),
});
} catch (err) {
// Top-level `zerion login` is a standalone command — exit non-zero on
// failure. When called inline from another flow (wallet create/import
// pass quiet: true), rethrow so the caller can recover instead of
// killing the outer process mid-setup.
if (quiet) {
const e = err instanceof Error ? err : new Error(String(err));
e.code = e.code || "login_failed";
throw e;
}
printError("login_failed", err.message || "Login failed");
process.exit(1);
}
}