@bankr/cli 0.1.0-beta.6 → 0.1.0
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/README.md +27 -1
- package/dist/cli.js +222 -4
- package/dist/commands/balances.d.ts +5 -0
- package/dist/commands/balances.js +113 -0
- package/dist/commands/fees.d.ts +18 -0
- package/dist/commands/fees.js +793 -0
- package/dist/commands/launch.d.ts +13 -0
- package/dist/commands/launch.js +174 -0
- package/dist/commands/llm.d.ts +11 -0
- package/dist/commands/llm.js +319 -3
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +464 -147
- package/dist/commands/profile.d.ts +26 -0
- package/dist/commands/profile.js +183 -0
- package/dist/commands/prompt.js +5 -0
- package/dist/commands/sign.js +3 -0
- package/dist/commands/sounds.d.ts +12 -0
- package/dist/commands/sounds.js +262 -0
- package/dist/commands/submit.js +14 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/api.d.ts +213 -0
- package/dist/lib/api.js +177 -3
- package/dist/lib/cesp/engine.d.ts +13 -0
- package/dist/lib/cesp/engine.js +132 -0
- package/dist/lib/cesp/player.d.ts +6 -0
- package/dist/lib/cesp/player.js +50 -0
- package/dist/lib/cesp/types.d.ts +38 -0
- package/dist/lib/cesp/types.js +2 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +3 -0
- package/package.json +4 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/cancel.d.ts.map +0 -1
- package/dist/commands/cancel.js.map +0 -1
- package/dist/commands/capabilities.d.ts.map +0 -1
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/llm.d.ts.map +0 -1
- package/dist/commands/llm.js.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/prompt.d.ts.map +0 -1
- package/dist/commands/prompt.js.map +0 -1
- package/dist/commands/sign.d.ts.map +0 -1
- package/dist/commands/sign.js.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/submit.d.ts.map +0 -1
- package/dist/commands/submit.js.map +0 -1
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/output.d.ts.map +0 -1
- package/dist/lib/output.js.map +0 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,37 +1,422 @@
|
|
|
1
1
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
2
2
|
import open from "open";
|
|
3
|
-
import { DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
|
|
3
|
+
import { CLI_USER_AGENT, DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
|
|
4
4
|
import * as output from "../lib/output.js";
|
|
5
5
|
const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api";
|
|
6
|
+
const MAX_OTP_ATTEMPTS = 3;
|
|
7
|
+
const INVALID_CODE_STATUSES = [400, 401, 422];
|
|
8
|
+
function fatal(message) {
|
|
9
|
+
output.error(message);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
6
12
|
function deriveDashboardUrl(apiUrl) {
|
|
7
13
|
if (apiUrl === DEFAULT_API_URL) {
|
|
8
14
|
return DEFAULT_DASHBOARD_URL;
|
|
9
15
|
}
|
|
10
16
|
return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") + "/api");
|
|
11
17
|
}
|
|
18
|
+
async function fetchPrivyConfig(apiUrl) {
|
|
19
|
+
const res = await fetch(`${apiUrl}/cli/config`, {
|
|
20
|
+
headers: { "User-Agent": CLI_USER_AGENT },
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`Failed to fetch auth config (${res.status})`);
|
|
24
|
+
}
|
|
25
|
+
return (await res.json());
|
|
26
|
+
}
|
|
27
|
+
async function sendPrivyOtp(privyConfig, email) {
|
|
28
|
+
const res = await fetch("https://auth.privy.io/api/v1/passwordless/init", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"privy-app-id": privyConfig.privyAppId,
|
|
33
|
+
"privy-client-id": privyConfig.privyClientId,
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ email, type: "email" }),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
if (res.status === 429) {
|
|
39
|
+
throw new Error("Too many attempts. Please wait and try again.");
|
|
40
|
+
}
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`Failed to send verification code: ${body || res.status}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function verifyPrivyOtp(privyConfig, email, code) {
|
|
46
|
+
const res = await fetch("https://auth.privy.io/api/v1/passwordless/authenticate", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"privy-app-id": privyConfig.privyAppId,
|
|
51
|
+
"privy-client-id": privyConfig.privyClientId,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ email, code, mode: "login-or-sign-up" }),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
if (INVALID_CODE_STATUSES.includes(res.status)) {
|
|
57
|
+
throw new Error("INVALID_CODE");
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 429) {
|
|
60
|
+
throw new Error("Too many attempts. Please wait and try again.");
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Verification failed (${res.status})`);
|
|
63
|
+
}
|
|
64
|
+
return (await res.json());
|
|
65
|
+
}
|
|
66
|
+
async function callGenerateWallet(apiUrl, identityToken) {
|
|
67
|
+
const res = await fetch(`${apiUrl}/cli/generate-wallet`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"User-Agent": CLI_USER_AGENT,
|
|
72
|
+
"privy-id-token": identityToken,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const body = (await res.json());
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(body.message || body.error || `Wallet generation failed (${res.status})`);
|
|
78
|
+
}
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
async function callAcceptTerms(apiUrl, identityToken) {
|
|
82
|
+
const res = await fetch(`${apiUrl}/user/accept-terms`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
"User-Agent": CLI_USER_AGENT,
|
|
87
|
+
"privy-id-token": identityToken,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const body = (await res.json().catch(() => ({})));
|
|
92
|
+
throw new Error(body.message || body.error || `Accept terms failed (${res.status})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function callGenerateApiKey(apiUrl, identityToken, opts) {
|
|
96
|
+
const res = await fetch(`${apiUrl}/generate-api-key`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"User-Agent": CLI_USER_AGENT,
|
|
101
|
+
"privy-id-token": identityToken,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(opts),
|
|
104
|
+
});
|
|
105
|
+
const body = (await res.json());
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(body.message || body.error || `API key generation failed (${res.status})`);
|
|
108
|
+
}
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
// ── OTP verification helpers ─────────────────────────────────────────
|
|
112
|
+
async function fetchPrivyConfigOrFail(apiUrl) {
|
|
113
|
+
const config = await fetchPrivyConfig(apiUrl).catch(() => null);
|
|
114
|
+
if (!config) {
|
|
115
|
+
fatal("Could not connect to Bankr API. Check your connection and try again.");
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
async function sendOtpWithSpinner(privyConfig, email) {
|
|
120
|
+
const spin = output.spinner("Sending verification code...");
|
|
121
|
+
try {
|
|
122
|
+
await sendPrivyOtp(privyConfig, email);
|
|
123
|
+
spin.succeed(`Verification code sent to ${email}`);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
spin.fail("Failed to send verification code");
|
|
127
|
+
fatal(err.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function verifyOtpWithSpinner(privyConfig, email, code) {
|
|
131
|
+
const spin = output.spinner("Verifying...");
|
|
132
|
+
try {
|
|
133
|
+
const result = await verifyPrivyOtp(privyConfig, email, code);
|
|
134
|
+
spin.succeed("Email verified");
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const errMsg = err.message;
|
|
139
|
+
spin.fail("Verification failed");
|
|
140
|
+
fatal(errMsg === "INVALID_CODE" ? "Invalid verification code." : errMsg);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Email authentication flow ───────────────────────────────────────
|
|
144
|
+
async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
|
|
145
|
+
const email = providedEmail?.trim() ||
|
|
146
|
+
(await input({
|
|
147
|
+
message: "Email address:",
|
|
148
|
+
theme: output.bankrTheme,
|
|
149
|
+
validate: (v) => {
|
|
150
|
+
const trimmed = v.trim();
|
|
151
|
+
if (!trimmed)
|
|
152
|
+
return "Email is required.";
|
|
153
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed))
|
|
154
|
+
return "Invalid email address.";
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
})).trim();
|
|
158
|
+
const privyConfig = await fetchPrivyConfigOrFail(apiUrl);
|
|
159
|
+
// Headless: code already provided, verify directly
|
|
160
|
+
if (providedCode) {
|
|
161
|
+
const result = await verifyOtpWithSpinner(privyConfig, email, providedCode.trim());
|
|
162
|
+
return { identityToken: result.identity_token, email };
|
|
163
|
+
}
|
|
164
|
+
// Interactive: send OTP, then prompt for code with retries
|
|
165
|
+
await sendOtpWithSpinner(privyConfig, email);
|
|
166
|
+
for (let attempt = 0; attempt < MAX_OTP_ATTEMPTS; attempt++) {
|
|
167
|
+
const code = await input({
|
|
168
|
+
message: "Enter the 6-digit code:",
|
|
169
|
+
theme: output.bankrTheme,
|
|
170
|
+
validate: (v) => /^\d{6}$/.test(v.trim()) ? true : "Enter a 6-digit code.",
|
|
171
|
+
});
|
|
172
|
+
const verifySpin = output.spinner("Verifying...");
|
|
173
|
+
try {
|
|
174
|
+
const result = await verifyPrivyOtp(privyConfig, email, code.trim());
|
|
175
|
+
verifySpin.succeed("Email verified");
|
|
176
|
+
return { identityToken: result.identity_token, email };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const errMsg = err.message;
|
|
180
|
+
if (errMsg === "INVALID_CODE") {
|
|
181
|
+
if (attempt < MAX_OTP_ATTEMPTS - 1) {
|
|
182
|
+
verifySpin.fail("Invalid code, please try again");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
verifySpin.fail("Invalid code");
|
|
186
|
+
fatal("Too many invalid attempts. Please restart login.");
|
|
187
|
+
}
|
|
188
|
+
verifySpin.fail("Verification failed");
|
|
189
|
+
fatal(errMsg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
fatal("Verification failed. Please try again.");
|
|
193
|
+
}
|
|
194
|
+
// ── Email login flow ────────────────────────────────────────────────
|
|
195
|
+
async function emailLoginFlow(apiUrl, opts) {
|
|
196
|
+
// Step 1: Authenticate via email OTP
|
|
197
|
+
const { identityToken } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
|
|
198
|
+
// Step 2: Generate/resolve wallet
|
|
199
|
+
const walletSpin = output.spinner("Setting up wallet...");
|
|
200
|
+
let wallet;
|
|
201
|
+
try {
|
|
202
|
+
wallet = await callGenerateWallet(apiUrl, identityToken);
|
|
203
|
+
walletSpin.stop();
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
walletSpin.fail("Wallet setup failed");
|
|
207
|
+
fatal(err.message);
|
|
208
|
+
}
|
|
209
|
+
// Show wallet info
|
|
210
|
+
output.blank();
|
|
211
|
+
if (wallet.isNewUser) {
|
|
212
|
+
output.success("Welcome to Bankr!");
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
output.success("Authenticated");
|
|
216
|
+
}
|
|
217
|
+
output.dim(` EVM: ${wallet.evmAddress}`);
|
|
218
|
+
if (wallet.solAddress) {
|
|
219
|
+
output.dim(` SOL: ${wallet.solAddress}`);
|
|
220
|
+
}
|
|
221
|
+
// Step 3: Accept terms if needed
|
|
222
|
+
if (!wallet.hasAcceptedTerms) {
|
|
223
|
+
output.blank();
|
|
224
|
+
output.dim(" Please review our Terms of Service: https://bankr.bot/terms");
|
|
225
|
+
output.blank();
|
|
226
|
+
const accepted = opts.acceptTerms ??
|
|
227
|
+
(await confirm({
|
|
228
|
+
message: "Do you accept the Terms of Service?",
|
|
229
|
+
default: false,
|
|
230
|
+
theme: output.bankrTheme,
|
|
231
|
+
}));
|
|
232
|
+
if (!accepted) {
|
|
233
|
+
fatal("Terms must be accepted to continue.");
|
|
234
|
+
}
|
|
235
|
+
const termsSpin = output.spinner("Accepting terms...");
|
|
236
|
+
try {
|
|
237
|
+
await callAcceptTerms(apiUrl, identityToken);
|
|
238
|
+
termsSpin.succeed("Terms accepted");
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
termsSpin.fail("Failed to accept terms");
|
|
242
|
+
fatal(err.message);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Step 4: Generate API key
|
|
246
|
+
output.blank();
|
|
247
|
+
const defaultKeyName = `CLI-${new Date().toISOString().slice(0, 10)}`;
|
|
248
|
+
const keyName = opts.keyName ??
|
|
249
|
+
(await input({
|
|
250
|
+
message: "Key name:",
|
|
251
|
+
default: defaultKeyName,
|
|
252
|
+
theme: output.bankrTheme,
|
|
253
|
+
validate: (v) => v.trim().length >= 3 ? true : "Name must be at least 3 characters.",
|
|
254
|
+
}));
|
|
255
|
+
const enableWrite = opts.readWrite ??
|
|
256
|
+
(await confirm({
|
|
257
|
+
message: "Enable write operations?",
|
|
258
|
+
default: false,
|
|
259
|
+
theme: output.bankrTheme,
|
|
260
|
+
}));
|
|
261
|
+
if (enableWrite) {
|
|
262
|
+
output.dim(" Includes: transactions, transfers, automations, order management");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
output.dim(" Read-only: portfolio, balances, prices, research");
|
|
266
|
+
}
|
|
267
|
+
const enableLlm = opts.llm ??
|
|
268
|
+
(await confirm({
|
|
269
|
+
message: "Enable LLM gateway?",
|
|
270
|
+
default: false,
|
|
271
|
+
theme: output.bankrTheme,
|
|
272
|
+
}));
|
|
273
|
+
const keySpin = output.spinner("Generating API key...");
|
|
274
|
+
let apiKeyResult;
|
|
275
|
+
try {
|
|
276
|
+
apiKeyResult = await callGenerateApiKey(apiUrl, identityToken, {
|
|
277
|
+
name: keyName.trim(),
|
|
278
|
+
agentApiEnabled: { readOnly: !enableWrite },
|
|
279
|
+
llmGatewayEnabled: enableLlm,
|
|
280
|
+
});
|
|
281
|
+
keySpin.stop();
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
keySpin.fail("Failed to generate API key");
|
|
285
|
+
fatal(err.message);
|
|
286
|
+
}
|
|
287
|
+
// Show result
|
|
288
|
+
output.blank();
|
|
289
|
+
const mode = enableWrite ? "read-write" : "read-only";
|
|
290
|
+
output.success(`API key "${apiKeyResult.name}" saved`);
|
|
291
|
+
output.dim(` Mode: ${mode}`);
|
|
292
|
+
output.dim(` LLM: ${apiKeyResult.llmGatewayEnabled ? "enabled" : "disabled"}`);
|
|
293
|
+
output.dim(" Manage keys at bankr.bot/api");
|
|
294
|
+
return apiKeyResult.apiKey;
|
|
295
|
+
}
|
|
296
|
+
// ── SIWE login flow ─────────────────────────────────────────────────
|
|
297
|
+
async function siweLoginFlow(apiUrl, opts) {
|
|
298
|
+
// Dynamic import to avoid loading viem for non-SIWE flows
|
|
299
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
300
|
+
const account = privateKeyToAccount(opts.privateKey);
|
|
301
|
+
// 1. Fetch nonce
|
|
302
|
+
const nonceSpinner = output.spinner("Fetching SIWE nonce...");
|
|
303
|
+
const nonceRes = await fetch(`${apiUrl}/cli/siwe/nonce`, {
|
|
304
|
+
headers: { "User-Agent": CLI_USER_AGENT },
|
|
305
|
+
});
|
|
306
|
+
if (!nonceRes.ok) {
|
|
307
|
+
nonceSpinner.fail("Failed to fetch nonce");
|
|
308
|
+
fatal(`Nonce request failed (${nonceRes.status})`);
|
|
309
|
+
}
|
|
310
|
+
const { nonce } = (await nonceRes.json());
|
|
311
|
+
nonceSpinner.succeed("Nonce received");
|
|
312
|
+
// 2. Create and sign SIWE message
|
|
313
|
+
const domain = new URL(apiUrl).host;
|
|
314
|
+
const message = [
|
|
315
|
+
`${domain} wants you to sign in with your Ethereum account:`,
|
|
316
|
+
account.address,
|
|
317
|
+
"",
|
|
318
|
+
"Sign in to Bankr",
|
|
319
|
+
"",
|
|
320
|
+
`URI: ${apiUrl}/cli/siwe/verify`,
|
|
321
|
+
`Version: 1`,
|
|
322
|
+
`Chain ID: 1`,
|
|
323
|
+
`Nonce: ${nonce}`,
|
|
324
|
+
`Issued At: ${new Date().toISOString()}`,
|
|
325
|
+
].join("\n");
|
|
326
|
+
const signSpinner = output.spinner("Signing SIWE message...");
|
|
327
|
+
const signature = await account.signMessage({ message });
|
|
328
|
+
signSpinner.succeed("Message signed");
|
|
329
|
+
// 3. Verify with API
|
|
330
|
+
const verifySpinner = output.spinner("Verifying and creating wallet...");
|
|
331
|
+
const verifyRes = await fetch(`${apiUrl}/cli/siwe/verify`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: {
|
|
334
|
+
"Content-Type": "application/json",
|
|
335
|
+
"User-Agent": CLI_USER_AGENT,
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
message,
|
|
339
|
+
signature,
|
|
340
|
+
partnerApiKey: opts.partnerKey,
|
|
341
|
+
keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
|
|
342
|
+
readOnly: opts.readOnly ?? false,
|
|
343
|
+
}),
|
|
344
|
+
});
|
|
345
|
+
if (!verifyRes.ok) {
|
|
346
|
+
verifySpinner.fail("SIWE verification failed");
|
|
347
|
+
const body = (await verifyRes.json().catch(() => ({})));
|
|
348
|
+
fatal(body.message || `Verification failed (${verifyRes.status})`);
|
|
349
|
+
}
|
|
350
|
+
const result = (await verifyRes.json());
|
|
351
|
+
verifySpinner.succeed("Wallet created");
|
|
352
|
+
output.blank();
|
|
353
|
+
output.success("SIWE authentication successful");
|
|
354
|
+
output.dim(` EVM: ${result.walletAddress}`);
|
|
355
|
+
if (result.solAddress) {
|
|
356
|
+
output.dim(` SOL: ${result.solAddress}`);
|
|
357
|
+
}
|
|
358
|
+
output.dim(` Mode: ${result.readOnly ? "read-only" : "read-write"}`);
|
|
359
|
+
return { apiKey: result.apiKey, walletAddress: result.walletAddress };
|
|
360
|
+
}
|
|
361
|
+
// ── API key validation helper ────────────────────────────────────────
|
|
362
|
+
async function validateApiKey(apiUrl, apiKey) {
|
|
363
|
+
const spin = output.spinner("Validating Bankr API key...");
|
|
364
|
+
try {
|
|
365
|
+
const res = await fetch(`${apiUrl}/agent/me`, {
|
|
366
|
+
headers: { "X-API-Key": apiKey, "User-Agent": CLI_USER_AGENT },
|
|
367
|
+
});
|
|
368
|
+
if (res.ok) {
|
|
369
|
+
spin.succeed("Bankr API key validated successfully");
|
|
370
|
+
}
|
|
371
|
+
else if (res.status === 401 || res.status === 403) {
|
|
372
|
+
const body = (await res.json().catch(() => ({})));
|
|
373
|
+
const errorType = body.error ?? "";
|
|
374
|
+
if (res.status === 403 || errorType === "Agent API access not enabled") {
|
|
375
|
+
spin.warn("Agent API access is not enabled");
|
|
376
|
+
output.warn("Your API key is valid but does not have Agent API access enabled.");
|
|
377
|
+
output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
|
|
378
|
+
output.info("Your key has been saved — once enabled, the CLI will work without re-login.");
|
|
379
|
+
}
|
|
380
|
+
else if (errorType === "Authentication required") {
|
|
381
|
+
spin.fail("API key is not linked to a wallet");
|
|
382
|
+
output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
spin.fail("Invalid Bankr API key");
|
|
387
|
+
output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
spin.warn("Could not validate Bankr API key (network error), saving anyway");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// ── Main login command ──────────────────────────────────────────────
|
|
12
400
|
export async function loginCommand(opts) {
|
|
13
|
-
// Fail fast if
|
|
401
|
+
// Fail fast if flags were passed with empty values
|
|
14
402
|
if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
|
|
15
|
-
|
|
16
|
-
process.exit(1);
|
|
403
|
+
fatal("--api-key requires a non-empty value");
|
|
17
404
|
}
|
|
18
|
-
if (opts.llmKey !== undefined && !opts.llmKey.trim()) {
|
|
19
|
-
output.error("--llm-key requires a non-empty value");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
const nonInteractive = !!(opts.apiKey || opts.url);
|
|
23
405
|
const config = readConfig();
|
|
24
406
|
const resolvedApiUrl = opts.apiUrl ?? getApiUrl();
|
|
25
407
|
if (resolvedApiUrl !== DEFAULT_API_URL) {
|
|
408
|
+
const nonInteractive = !!(opts.apiKey ||
|
|
409
|
+
opts.url ||
|
|
410
|
+
opts.email !== undefined ||
|
|
411
|
+
opts.siwe);
|
|
26
412
|
if (nonInteractive) {
|
|
27
|
-
// Non-interactive: accept the custom URL silently
|
|
28
413
|
config.apiUrl = resolvedApiUrl;
|
|
29
414
|
}
|
|
30
415
|
else {
|
|
31
|
-
|
|
416
|
+
output.blank();
|
|
32
417
|
output.warn(`Bankr API URL is set to a non-official URL: ${resolvedApiUrl}`);
|
|
33
418
|
output.dim(` Official URL: ${DEFAULT_API_URL}`);
|
|
34
|
-
|
|
419
|
+
output.blank();
|
|
35
420
|
const proceed = await confirm({
|
|
36
421
|
message: "Continue with this URL?",
|
|
37
422
|
default: false,
|
|
@@ -50,15 +435,43 @@ export async function loginCommand(opts) {
|
|
|
50
435
|
config.apiUrl = opts.apiUrl;
|
|
51
436
|
}
|
|
52
437
|
const apiUrl = config.apiUrl || DEFAULT_API_URL;
|
|
438
|
+
function saveCredentials(apiKey, llmKey) {
|
|
439
|
+
config.apiKey = apiKey;
|
|
440
|
+
if (llmKey) {
|
|
441
|
+
config.llmKey = llmKey.trim();
|
|
442
|
+
}
|
|
443
|
+
writeConfig(config);
|
|
444
|
+
output.success(`Credentials saved to ${getConfigPath()}`);
|
|
445
|
+
output.dim(`Bankr API URL: ${apiUrl}`);
|
|
446
|
+
}
|
|
447
|
+
// --siwe: SIWE login flow (headless agent onboarding)
|
|
448
|
+
if (opts.siwe) {
|
|
449
|
+
if (!opts.privateKey) {
|
|
450
|
+
fatal("--private-key is required with --siwe");
|
|
451
|
+
}
|
|
452
|
+
const { apiKey } = await siweLoginFlow(apiUrl, {
|
|
453
|
+
privateKey: opts.privateKey,
|
|
454
|
+
partnerKey: opts.partnerKey,
|
|
455
|
+
keyName: opts.keyName,
|
|
456
|
+
readOnly: !opts.readWrite,
|
|
457
|
+
});
|
|
458
|
+
config.apiKey = apiKey;
|
|
459
|
+
if (opts.partnerKey) {
|
|
460
|
+
config.partnerKey = opts.partnerKey;
|
|
461
|
+
}
|
|
462
|
+
writeConfig(config);
|
|
463
|
+
output.success(`Credentials saved to ${getConfigPath()}`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
53
466
|
// --url: print the dashboard URL and exit
|
|
54
467
|
if (opts.url) {
|
|
55
468
|
const dashboardUrl = deriveDashboardUrl(apiUrl);
|
|
56
|
-
|
|
469
|
+
output.blank();
|
|
57
470
|
output.brandBold(" Bankr CLI Login");
|
|
58
|
-
|
|
471
|
+
output.blank();
|
|
59
472
|
output.info("Generate your Bankr API key at:");
|
|
60
473
|
output.brandBold(` ${dashboardUrl}`);
|
|
61
|
-
|
|
474
|
+
output.blank();
|
|
62
475
|
output.dim("Once you have the key, run: bankr login --api-key bk_YOUR_KEY");
|
|
63
476
|
return;
|
|
64
477
|
}
|
|
@@ -66,50 +479,40 @@ export async function loginCommand(opts) {
|
|
|
66
479
|
if (opts.apiKey) {
|
|
67
480
|
const apiKey = opts.apiKey.trim();
|
|
68
481
|
if (!apiKey.startsWith("bk_")) {
|
|
69
|
-
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
const spin = output.spinner("Validating Bankr API key...");
|
|
73
|
-
try {
|
|
74
|
-
const res = await fetch(`${apiUrl}/agent/me`, {
|
|
75
|
-
headers: { "X-API-Key": apiKey },
|
|
76
|
-
});
|
|
77
|
-
if (res.ok) {
|
|
78
|
-
spin.succeed("Bankr API key validated successfully");
|
|
79
|
-
}
|
|
80
|
-
else if (res.status === 401 || res.status === 403) {
|
|
81
|
-
spin.fail("Invalid Bankr API key");
|
|
82
|
-
output.error("API rejected the provided key.");
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
spin.warn("Could not validate Bankr API key (network error), saving anyway");
|
|
482
|
+
fatal('Invalid format. Bankr API keys start with "bk_".');
|
|
91
483
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (opts.
|
|
100
|
-
|
|
484
|
+
await validateApiKey(apiUrl, apiKey);
|
|
485
|
+
saveCredentials(apiKey, opts.llmKey);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// `login email [address]` subcommand
|
|
489
|
+
if (opts.email !== undefined) {
|
|
490
|
+
// Headless step 1: email provided but no code -- send OTP and exit
|
|
491
|
+
if (opts.email && !opts.code) {
|
|
492
|
+
const privyConfig = await fetchPrivyConfigOrFail(apiUrl);
|
|
493
|
+
await sendOtpWithSpinner(privyConfig, opts.email);
|
|
494
|
+
output.blank();
|
|
495
|
+
output.info(`Complete login with: bankr login email ${opts.email} --code <code>`);
|
|
496
|
+
return;
|
|
101
497
|
}
|
|
498
|
+
const apiKey = await emailLoginFlow(apiUrl, opts);
|
|
499
|
+
output.blank();
|
|
500
|
+
saveCredentials(apiKey, opts.llmKey);
|
|
102
501
|
return;
|
|
103
502
|
}
|
|
104
|
-
// Interactive flow
|
|
105
|
-
|
|
503
|
+
// Interactive flow
|
|
504
|
+
output.blank();
|
|
106
505
|
output.brandBold(" Bankr CLI Login");
|
|
107
|
-
|
|
506
|
+
output.blank();
|
|
108
507
|
const choice = await select({
|
|
109
|
-
message: "How would you like to
|
|
508
|
+
message: "How would you like to log in?",
|
|
110
509
|
choices: [
|
|
111
510
|
{
|
|
112
|
-
name: "
|
|
511
|
+
name: "Sign in with email",
|
|
512
|
+
value: "email",
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "Open bankr.bot to generate an API key",
|
|
113
516
|
value: "browser",
|
|
114
517
|
},
|
|
115
518
|
{
|
|
@@ -119,6 +522,12 @@ export async function loginCommand(opts) {
|
|
|
119
522
|
],
|
|
120
523
|
theme: output.bankrTheme,
|
|
121
524
|
});
|
|
525
|
+
if (choice === "email") {
|
|
526
|
+
const apiKey = await emailLoginFlow(apiUrl, {});
|
|
527
|
+
output.blank();
|
|
528
|
+
saveCredentials(apiKey, opts.llmKey);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
122
531
|
if (choice === "browser") {
|
|
123
532
|
const dashboardUrl = deriveDashboardUrl(apiUrl);
|
|
124
533
|
output.info("Opening Bankr dashboard...");
|
|
@@ -130,9 +539,9 @@ export async function loginCommand(opts) {
|
|
|
130
539
|
output.dim("Could not open browser automatically.");
|
|
131
540
|
output.dim(`Visit ${dashboardUrl} to get your Bankr API key.`);
|
|
132
541
|
}
|
|
133
|
-
|
|
542
|
+
output.blank();
|
|
134
543
|
output.dim("Generate a Bankr API key, then paste it below.");
|
|
135
|
-
|
|
544
|
+
output.blank();
|
|
136
545
|
}
|
|
137
546
|
const apiKey = await input({
|
|
138
547
|
message: "Paste your Bankr API key:",
|
|
@@ -147,99 +556,7 @@ export async function loginCommand(opts) {
|
|
|
147
556
|
return true;
|
|
148
557
|
},
|
|
149
558
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const res = await fetch(`${apiUrl}/agent/me`, {
|
|
153
|
-
headers: { "X-API-Key": apiKey.trim() },
|
|
154
|
-
});
|
|
155
|
-
if (res.ok) {
|
|
156
|
-
spin.succeed("Bankr API key validated successfully");
|
|
157
|
-
}
|
|
158
|
-
else if (res.status === 401 || res.status === 403) {
|
|
159
|
-
const body = await res.json().catch(() => ({}));
|
|
160
|
-
const errorType = body.error ?? "";
|
|
161
|
-
if (res.status === 403 || errorType === "Agent API access not enabled") {
|
|
162
|
-
spin.fail("Agent API access is not enabled");
|
|
163
|
-
output.error("Your API key is valid but does not have Agent API access enabled.");
|
|
164
|
-
output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
|
|
165
|
-
}
|
|
166
|
-
else if (errorType === "Authentication required") {
|
|
167
|
-
spin.fail("API key is not linked to a wallet");
|
|
168
|
-
output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
spin.fail("Invalid Bankr API key");
|
|
172
|
-
output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
|
|
173
|
-
}
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
spin.warn("Could not validate Bankr API key (API unreachable), saving anyway");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
spin.warn("Could not validate Bankr API key (network error), saving anyway");
|
|
182
|
-
}
|
|
183
|
-
config.apiKey = apiKey.trim();
|
|
184
|
-
// Ask if user wants a separate LLM key (interactive only)
|
|
185
|
-
if (!opts.llmKey) {
|
|
186
|
-
console.log();
|
|
187
|
-
const useSeparateLlmKey = await confirm({
|
|
188
|
-
message: "Use a different key for the LLM gateway?",
|
|
189
|
-
default: false,
|
|
190
|
-
theme: output.bankrTheme,
|
|
191
|
-
});
|
|
192
|
-
if (useSeparateLlmKey) {
|
|
193
|
-
const llmChoice = await select({
|
|
194
|
-
message: "How would you like to provide the LLM gateway key?",
|
|
195
|
-
choices: [
|
|
196
|
-
{
|
|
197
|
-
name: "Open bankr.bot/api to generate a new key",
|
|
198
|
-
value: "browser",
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: "Paste an existing LLM gateway key",
|
|
202
|
-
value: "paste",
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
theme: output.bankrTheme,
|
|
206
|
-
});
|
|
207
|
-
if (llmChoice === "browser") {
|
|
208
|
-
const dashboardUrl = deriveDashboardUrl(apiUrl);
|
|
209
|
-
output.info("Opening Bankr dashboard...");
|
|
210
|
-
output.dim(dashboardUrl);
|
|
211
|
-
try {
|
|
212
|
-
await open(dashboardUrl);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
output.dim("Could not open browser automatically.");
|
|
216
|
-
output.dim(`Visit ${dashboardUrl} to get your LLM gateway key.`);
|
|
217
|
-
}
|
|
218
|
-
console.log();
|
|
219
|
-
output.dim("Generate an LLM gateway key, then paste it below.");
|
|
220
|
-
console.log();
|
|
221
|
-
}
|
|
222
|
-
const llmKey = await input({
|
|
223
|
-
message: "Paste your LLM gateway key:",
|
|
224
|
-
theme: output.bankrTheme,
|
|
225
|
-
transformer: (value) => "*".repeat(value.length),
|
|
226
|
-
validate: (value) => {
|
|
227
|
-
if (!value.trim())
|
|
228
|
-
return "LLM key is required.";
|
|
229
|
-
return true;
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
config.llmKey = llmKey.trim();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
config.llmKey = opts.llmKey.trim();
|
|
237
|
-
}
|
|
238
|
-
writeConfig(config);
|
|
239
|
-
output.success(`Credentials saved to ${getConfigPath()}`);
|
|
240
|
-
output.dim(`Bankr API URL: ${apiUrl}`);
|
|
241
|
-
if (config.llmKey) {
|
|
242
|
-
output.dim(`Bankr LLM Key: (saved separately)`);
|
|
243
|
-
}
|
|
559
|
+
await validateApiKey(apiUrl, apiKey.trim());
|
|
560
|
+
saveCredentials(apiKey.trim(), opts.llmKey);
|
|
244
561
|
}
|
|
245
562
|
//# sourceMappingURL=login.js.map
|