@bankr/cli 0.3.37 → 0.3.43
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 +51 -156
- package/dist/cli.js +162 -150
- package/dist/commands/fees.js +17 -7
- package/dist/commands/llm.js +64 -0
- package/dist/commands/login.d.ts +75 -0
- package/dist/commands/login.js +321 -60
- package/dist/commands/{profile.d.ts → project.d.ts} +6 -6
- package/dist/commands/{profile.js → project.js} +65 -65
- package/dist/commands/prompt.d.ts +11 -0
- package/dist/commands/prompt.js +20 -11
- package/dist/commands/status.d.ts +5 -1
- package/dist/commands/status.js +54 -5
- package/dist/commands/update.js +2 -3
- package/dist/commands/x402.js +2 -2
- package/dist/lib/api.d.ts +29 -8
- package/dist/lib/api.js +26 -12
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.js +18 -2
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +4 -0
- package/dist/lib/statusUpdates.d.ts +14 -0
- package/dist/lib/statusUpdates.js +38 -0
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -8,12 +8,121 @@ import { getErrorMessage } from "../lib/errors.js";
|
|
|
8
8
|
const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api-keys";
|
|
9
9
|
const MAX_OTP_ATTEMPTS = 3;
|
|
10
10
|
const INVALID_CODE_STATUSES = [400, 401, 422];
|
|
11
|
+
/** The `code` the API's MFA gates send on every step-up 401 envelope
|
|
12
|
+
* (`sendStepUpRequired` in packages/api/src/middleware/mfaGate.ts). Duplicated
|
|
13
|
+
* here because the CLI can't import `@bankr/*` packages. */
|
|
14
|
+
const MFA_STEP_UP_CODE = "MFA_STEP_UP_REQUIRED";
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a login-gated call answers with the MFA step-up envelope. A
|
|
17
|
+
* passkey ceremony can't run in a terminal, so the email flow can't recover;
|
|
18
|
+
* the caller turns this into the browser-key guidance instead of the server's
|
|
19
|
+
* fallback text.
|
|
20
|
+
*/
|
|
21
|
+
export class MfaStepUpError extends Error {
|
|
22
|
+
constructor(context) {
|
|
23
|
+
super("MFA step-up required");
|
|
24
|
+
this.context = context;
|
|
25
|
+
this.name = "MfaStepUpError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Keyed on `code`, never on message text, so a server wording change can't
|
|
29
|
+
* silently turn this back into the opaque failure. */
|
|
30
|
+
export function mfaStepUpErrorFrom(body) {
|
|
31
|
+
if (!body || typeof body !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
const { code, context } = body;
|
|
34
|
+
if (code !== MFA_STEP_UP_CODE)
|
|
35
|
+
return null;
|
|
36
|
+
return new MfaStepUpError(typeof context === "string" ? context : "unknown");
|
|
37
|
+
}
|
|
38
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
39
|
+
"ECONNRESET",
|
|
40
|
+
"ECONNREFUSED",
|
|
41
|
+
"ETIMEDOUT",
|
|
42
|
+
"ENOTFOUND",
|
|
43
|
+
"EAI_AGAIN",
|
|
44
|
+
"EPIPE",
|
|
45
|
+
"UND_ERR_SOCKET",
|
|
46
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
47
|
+
]);
|
|
48
|
+
/** Any 5xx: a gateway page (ALB/CloudFront 502/503/504) the API never saw, or
|
|
49
|
+
* an API catch-all such as a DB blip. Idempotent callers retry it; the API's
|
|
50
|
+
* own message, when it sent one, is kept for the final report. */
|
|
51
|
+
export class TransientHttpError extends Error {
|
|
52
|
+
constructor(status, apiMessage) {
|
|
53
|
+
super(apiMessage ?? `Bankr API unavailable (${status})`);
|
|
54
|
+
this.status = status;
|
|
55
|
+
this.name = "TransientHttpError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** True for a request the API never answered: undici's `fetch failed`, a
|
|
59
|
+
* socket-level code, or a gateway error page. An API refusal is not one. */
|
|
60
|
+
export function isTransientNetworkError(err, depth = 0) {
|
|
61
|
+
if (depth > 5)
|
|
62
|
+
return false;
|
|
63
|
+
if (!(err instanceof Error) || err instanceof MfaStepUpError)
|
|
64
|
+
return false;
|
|
65
|
+
if (err instanceof TransientHttpError)
|
|
66
|
+
return true;
|
|
67
|
+
// undici: "fetch failed" when no connection, "terminated" on a mid-body drop.
|
|
68
|
+
if (err instanceof TypeError &&
|
|
69
|
+
(err.message === "fetch failed" || err.message === "terminated")) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const code = err.code;
|
|
73
|
+
if (typeof code === "string" && NETWORK_ERROR_CODES.has(code))
|
|
74
|
+
return true;
|
|
75
|
+
return isTransientNetworkError(err.cause, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Retry a login step on network blips only. The OTP is already spent by the
|
|
79
|
+
* time the wallet step runs, so a dropped connection here used to cost the
|
|
80
|
+
* user a fresh code; three quick attempts ride out the transient case.
|
|
81
|
+
*/
|
|
82
|
+
export async function withNetworkRetry(run, opts = {}) {
|
|
83
|
+
const { attempts = 3, baseDelayMs = 1000, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), } = opts;
|
|
84
|
+
for (let attempt = 1;; attempt++) {
|
|
85
|
+
try {
|
|
86
|
+
return await run();
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (attempt >= attempts || !isTransientNetworkError(err))
|
|
90
|
+
throw err;
|
|
91
|
+
await sleep(baseDelayMs * 2 ** (attempt - 1));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Key names are unique per user, so the default carries the time to the
|
|
96
|
+
* second: a scripted re-login seconds after a dropped mint response (which
|
|
97
|
+
* already created the key) must not collide with the earlier default. */
|
|
98
|
+
export function defaultCliKeyName(now = new Date()) {
|
|
99
|
+
const iso = now.toISOString();
|
|
100
|
+
return `CLI-${iso.slice(0, 10)}-${iso.slice(11, 19).replace(/:/g, "")}`;
|
|
101
|
+
}
|
|
102
|
+
/** Headless login (`--code` / `--ni`) accepts the Terms of Service as part of
|
|
103
|
+
* logging in, so the one-time code isn't burned on a missing flag. Interactive
|
|
104
|
+
* login returns undefined to defer to the prompt. */
|
|
105
|
+
export function resolveTermsAcceptance(opts) {
|
|
106
|
+
// `--accept-terms` is set-only (no `--no-accept-terms`), so `||` is safe.
|
|
107
|
+
return opts.acceptTerms || opts.headless || undefined;
|
|
108
|
+
}
|
|
109
|
+
/** The web app that fronts an API URL: `BANKR_WEB_URL` when set (local dev,
|
|
110
|
+
* staging), bankr.bot for the official API, else the API host with its
|
|
111
|
+
* `api.`/`api-` prefix dropped. */
|
|
112
|
+
export function deriveWebOrigin(apiUrl) {
|
|
113
|
+
const override = process.env.BANKR_WEB_URL?.trim();
|
|
114
|
+
if (override)
|
|
115
|
+
return override.replace(/\/+$/, "");
|
|
116
|
+
if (apiUrl === DEFAULT_API_URL)
|
|
117
|
+
return "https://bankr.bot";
|
|
118
|
+
// Anchored to the host's start so a host merely containing "api." is intact.
|
|
119
|
+
return apiUrl.replace(/\/+$/, "").replace(/^(https?:\/\/)api[.-]/, "$1");
|
|
120
|
+
}
|
|
11
121
|
function deriveDashboardUrl(apiUrl) {
|
|
12
122
|
if (apiUrl === DEFAULT_API_URL) {
|
|
13
123
|
return DEFAULT_DASHBOARD_URL;
|
|
14
124
|
}
|
|
15
|
-
return (apiUrl
|
|
16
|
-
"/api-keys");
|
|
125
|
+
return `${deriveWebOrigin(apiUrl)}/api-keys`;
|
|
17
126
|
}
|
|
18
127
|
async function fetchPrivyConfig(apiUrl) {
|
|
19
128
|
const res = await fetch(`${apiUrl}/cli/config`, {
|
|
@@ -73,7 +182,9 @@ export function resolveKeyPermissionOverrides(opts) {
|
|
|
73
182
|
return overrides;
|
|
74
183
|
}
|
|
75
184
|
async function verifyPrivyOtp(privyConfig, email, code) {
|
|
76
|
-
|
|
185
|
+
// Retry only a request that never got an answer. If Privy did answer and
|
|
186
|
+
// the socket dropped, the replay reads as INVALID_CODE: no worse than today.
|
|
187
|
+
const res = await withNetworkRetry(() => fetch("https://auth.privy.io/api/v1/passwordless/authenticate", {
|
|
77
188
|
method: "POST",
|
|
78
189
|
headers: {
|
|
79
190
|
"Content-Type": "application/json",
|
|
@@ -81,7 +192,7 @@ async function verifyPrivyOtp(privyConfig, email, code) {
|
|
|
81
192
|
"privy-client-id": privyConfig.privyClientId,
|
|
82
193
|
},
|
|
83
194
|
body: JSON.stringify({ email, code, mode: "login-or-sign-up" }),
|
|
84
|
-
});
|
|
195
|
+
}));
|
|
85
196
|
if (!res.ok) {
|
|
86
197
|
if (INVALID_CODE_STATUSES.includes(res.status)) {
|
|
87
198
|
throw new Error("INVALID_CODE");
|
|
@@ -93,26 +204,49 @@ async function verifyPrivyOtp(privyConfig, email, code) {
|
|
|
93
204
|
}
|
|
94
205
|
return (await res.json());
|
|
95
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Parse a login-gated response. The body is read as text first so a gateway
|
|
209
|
+
* HTML error page becomes a `TransientHttpError` instead of a `SyntaxError`
|
|
210
|
+
* from `res.json()`. Every 5xx is transient (callers that opted into the
|
|
211
|
+
* retry replay it; others just report it); a 4xx keeps the API's message.
|
|
212
|
+
*/
|
|
213
|
+
export async function readGatedResponse(res, label) {
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
let body;
|
|
216
|
+
try {
|
|
217
|
+
body = JSON.parse(text);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
body = undefined;
|
|
221
|
+
}
|
|
222
|
+
const isJsonObject = !!body && typeof body === "object";
|
|
223
|
+
const { message, error } = (isJsonObject ? body : {});
|
|
224
|
+
const apiMessage = (typeof message === "string" && message) ||
|
|
225
|
+
(typeof error === "string" && error) ||
|
|
226
|
+
undefined;
|
|
227
|
+
if (!res.ok) {
|
|
228
|
+
if (res.status >= 500)
|
|
229
|
+
throw new TransientHttpError(res.status, apiMessage);
|
|
230
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
231
|
+
new Error(apiMessage ?? `${label} (${res.status})`));
|
|
232
|
+
}
|
|
233
|
+
if (!isJsonObject)
|
|
234
|
+
throw new Error(`${label}: unexpected response`);
|
|
235
|
+
return body;
|
|
236
|
+
}
|
|
96
237
|
async function callGenerateWallet(apiUrl, tokens) {
|
|
97
238
|
const res = await fetch(`${apiUrl}/cli/generate-wallet`, {
|
|
98
239
|
method: "POST",
|
|
99
240
|
headers: authHeaders(tokens),
|
|
100
241
|
});
|
|
101
|
-
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
throw new Error(body.message || body.error || `Wallet generation failed (${res.status})`);
|
|
104
|
-
}
|
|
105
|
-
return body;
|
|
242
|
+
return readGatedResponse(res, "Wallet generation failed");
|
|
106
243
|
}
|
|
107
244
|
async function callAcceptTerms(apiUrl, tokens) {
|
|
108
245
|
const res = await fetch(`${apiUrl}/user/accept-terms`, {
|
|
109
246
|
method: "POST",
|
|
110
247
|
headers: authHeaders(tokens),
|
|
111
248
|
});
|
|
112
|
-
|
|
113
|
-
const body = (await res.json().catch(() => ({})));
|
|
114
|
-
throw new Error(body.message || body.error || `Accept terms failed (${res.status})`);
|
|
115
|
-
}
|
|
249
|
+
await readGatedResponse(res, "Accept terms failed");
|
|
116
250
|
}
|
|
117
251
|
async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
118
252
|
const res = await fetch(`${apiUrl}/api-keys`, {
|
|
@@ -120,11 +254,111 @@ async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
|
120
254
|
headers: authHeaders(tokens),
|
|
121
255
|
body: JSON.stringify(opts),
|
|
122
256
|
});
|
|
123
|
-
|
|
257
|
+
// Parsed through the same seam so a gateway page reports cleanly; the mint
|
|
258
|
+
// itself is still never retried (see runGatedStep).
|
|
259
|
+
return readGatedResponse(res, "API key generation failed");
|
|
260
|
+
}
|
|
261
|
+
// ── Out-of-band MFA approval ─────────────────────────────────────────
|
|
262
|
+
// Mirrors `MFA_CHALLENGE_STATUSES` in @bankr/shared (the CLI can't import it).
|
|
263
|
+
const CHALLENGE_STATUSES = [
|
|
264
|
+
"pending",
|
|
265
|
+
"verified",
|
|
266
|
+
"expired",
|
|
267
|
+
"abandoned",
|
|
268
|
+
"failed",
|
|
269
|
+
];
|
|
270
|
+
/** Narrow the status poll body; anything unrecognised reads as `failed`. */
|
|
271
|
+
function parseChallengeStatus(body) {
|
|
272
|
+
const status = body?.status;
|
|
273
|
+
return CHALLENGE_STATUSES.includes(status)
|
|
274
|
+
? status
|
|
275
|
+
: "failed";
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* One poll's outcome from its HTTP status. Only a definitive refusal ends the
|
|
279
|
+
* wait: the gate (401, an API without this route yet), another wallet's token
|
|
280
|
+
* (403) or a gone challenge (404). Anything else — 429, 5xx, a dropped
|
|
281
|
+
* connection — is a blip, so the wait keeps polling as if still pending.
|
|
282
|
+
*/
|
|
283
|
+
export function pollOutcome(httpStatus, body) {
|
|
284
|
+
if (httpStatus >= 200 && httpStatus < 300)
|
|
285
|
+
return parseChallengeStatus(body);
|
|
286
|
+
return [401, 403, 404].includes(httpStatus) ? "failed" : "pending";
|
|
287
|
+
}
|
|
288
|
+
/** Poll cadence: 2s while an approval is likely imminent, then 5s. */
|
|
289
|
+
const POLL_FAST_MS = 2000;
|
|
290
|
+
const POLL_SLOW_MS = 5000;
|
|
291
|
+
const POLL_FAST_WINDOW_MS = 30000;
|
|
292
|
+
/**
|
|
293
|
+
* Poll until the challenge leaves `pending` or the deadline passes: once at
|
|
294
|
+
* once, then every 2s for the first 30s, then every 5s. Pure over its
|
|
295
|
+
* collaborators so the loop is unit-testable without a network.
|
|
296
|
+
*/
|
|
297
|
+
export async function waitForChallengeApproval(deps) {
|
|
298
|
+
const now = deps.now ?? Date.now;
|
|
299
|
+
const startedAt = now();
|
|
300
|
+
for (;;) {
|
|
301
|
+
const status = await deps.poll();
|
|
302
|
+
if (status !== "pending")
|
|
303
|
+
return status;
|
|
304
|
+
if (now() >= deps.deadlineMs)
|
|
305
|
+
return "expired";
|
|
306
|
+
await deps.sleep(now() - startedAt < POLL_FAST_WINDOW_MS ? POLL_FAST_MS : POLL_SLOW_MS);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function createMfaChallenge(apiUrl, tokens, context) {
|
|
310
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: authHeaders(tokens),
|
|
313
|
+
body: JSON.stringify({ context }),
|
|
314
|
+
});
|
|
124
315
|
if (!res.ok) {
|
|
125
|
-
throw new Error(
|
|
316
|
+
throw new Error(`Couldn't start passkey approval (${res.status})`);
|
|
126
317
|
}
|
|
127
|
-
return
|
|
318
|
+
return (await res.json());
|
|
319
|
+
}
|
|
320
|
+
async function readMfaChallengeStatus(apiUrl, tokens, token) {
|
|
321
|
+
try {
|
|
322
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge/${token}`, {
|
|
323
|
+
headers: authHeaders(tokens),
|
|
324
|
+
});
|
|
325
|
+
return pollOutcome(res.status, await res.json().catch(() => null));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return "pending";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Mirrors the server's challenge TTL; the deadline is kept on the local clock
|
|
332
|
+
* so a skewed machine can't expire the wait early (the poll reports a real
|
|
333
|
+
* server-side expiry as `expired` regardless). */
|
|
334
|
+
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
335
|
+
/**
|
|
336
|
+
* The browser hop for an MFA-enabled account: create a challenge for THIS
|
|
337
|
+
* session, hand the user the confirm link, and wait for their passkey on the
|
|
338
|
+
* web to mint the grants. Resolves true once approved.
|
|
339
|
+
*/
|
|
340
|
+
async function approveInBrowser(apiUrl, tokens) {
|
|
341
|
+
const { token } = await createMfaChallenge(apiUrl, tokens, "api-key:generate");
|
|
342
|
+
const url = `${deriveWebOrigin(apiUrl)}/mfa/confirm/${token}`;
|
|
343
|
+
output.blank();
|
|
344
|
+
output.warn("Two-factor authentication is enabled on this account.");
|
|
345
|
+
output.info("Approve this login with your passkey:");
|
|
346
|
+
output.brandBold(` ${url}`);
|
|
347
|
+
if (!output.isNonInteractive()) {
|
|
348
|
+
await open(url).catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
const spin = output.spinner("Waiting for approval in your browser...");
|
|
351
|
+
const status = await waitForChallengeApproval({
|
|
352
|
+
poll: () => readMfaChallengeStatus(apiUrl, tokens, token),
|
|
353
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
354
|
+
deadlineMs: Date.now() + CHALLENGE_TTL_MS,
|
|
355
|
+
});
|
|
356
|
+
if (status === "verified") {
|
|
357
|
+
spin.succeed("Approved");
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
spin.fail(`Approval ${status}`);
|
|
361
|
+
return false;
|
|
128
362
|
}
|
|
129
363
|
// ── OTP verification helpers ─────────────────────────────────────────
|
|
130
364
|
async function fetchPrivyConfigOrFail(apiUrl) {
|
|
@@ -220,20 +454,63 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
|
|
|
220
454
|
}
|
|
221
455
|
output.fatal("Verification failed. Please try again.");
|
|
222
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Run a login-gated call; on the MFA step-up envelope, take the browser
|
|
459
|
+
* approval hop once and retry. A second refusal, or an approval that expires
|
|
460
|
+
* or fails, exits with the browser-key guidance. Network blips are retried
|
|
461
|
+
* only where the caller opts in, so a step is never replayed by default: a key
|
|
462
|
+
* mint whose response was dropped has already consumed its unique name.
|
|
463
|
+
*/
|
|
464
|
+
async function runGatedStep(apiUrl, tokens, label, run, opts = {}) {
|
|
465
|
+
const { retryOnNetworkBlip = false } = opts;
|
|
466
|
+
for (const retry of [false, true]) {
|
|
467
|
+
const spin = output.spinner(label);
|
|
468
|
+
try {
|
|
469
|
+
const result = retryOnNetworkBlip
|
|
470
|
+
? await withNetworkRetry(run)
|
|
471
|
+
: await run();
|
|
472
|
+
spin.stop();
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
spin.stop();
|
|
477
|
+
if (!(err instanceof MfaStepUpError) || retry) {
|
|
478
|
+
return failLoginStep(`${label} failed`, err, apiUrl);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const approved = await approveInBrowser(apiUrl, tokens).catch((err) => {
|
|
482
|
+
output.error(getErrorMessage(err));
|
|
483
|
+
return false;
|
|
484
|
+
});
|
|
485
|
+
if (!approved)
|
|
486
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
487
|
+
}
|
|
488
|
+
throw new Error("unreachable");
|
|
489
|
+
}
|
|
490
|
+
/** Exit with the generic epilogue, or the browser-key path on a step-up
|
|
491
|
+
* refusal the approval hop didn't clear. */
|
|
492
|
+
function failLoginStep(label, err, apiUrl) {
|
|
493
|
+
if (!(err instanceof MfaStepUpError)) {
|
|
494
|
+
return output.fatal(`${label}: ${getErrorMessage(err)}`);
|
|
495
|
+
}
|
|
496
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
497
|
+
}
|
|
498
|
+
/** The remediation is the point of this message, so every line stays on
|
|
499
|
+
* stderr with the headline: a scripted caller capturing stderr sees all of it. */
|
|
500
|
+
function exitWithApiKeyGuidance(apiUrl) {
|
|
501
|
+
output.error("Two-factor authentication is enabled on this account.");
|
|
502
|
+
output.errorDetail(" The passkey approval didn't complete.");
|
|
503
|
+
output.errorDetail(` Generate an API key at ${deriveDashboardUrl(apiUrl)} instead (verify with your passkey there), then run:`);
|
|
504
|
+
output.errorDetail(" bankr login --api-key bk_YOUR_KEY");
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
223
507
|
// ── Email login flow ────────────────────────────────────────────────
|
|
224
508
|
async function emailLoginFlow(apiUrl, opts) {
|
|
225
509
|
// Step 1: Authenticate via email OTP
|
|
226
510
|
const { tokens } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
|
|
227
|
-
// Step 2:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
wallet = await callGenerateWallet(apiUrl, tokens);
|
|
232
|
-
walletSpin.stop();
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
output.failWith(walletSpin, "Wallet setup failed", err);
|
|
236
|
-
}
|
|
511
|
+
// Step 2: Resolve (or create) the wallet. The label stays neutral because
|
|
512
|
+
// whether the user is new only comes back in the response.
|
|
513
|
+
const wallet = await runGatedStep(apiUrl, tokens, "Loading wallet...", () => callGenerateWallet(apiUrl, tokens), { retryOnNetworkBlip: true });
|
|
237
514
|
// Show wallet info
|
|
238
515
|
output.blank();
|
|
239
516
|
if (wallet.isNewUser) {
|
|
@@ -253,29 +530,21 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
253
530
|
output.blank();
|
|
254
531
|
output.dim(" Please review our Terms of Service: https://bankr.bot/terms");
|
|
255
532
|
output.blank();
|
|
256
|
-
const accepted = opts.acceptTerms ??
|
|
257
|
-
(
|
|
258
|
-
?
|
|
259
|
-
:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
theme: output.bankrTheme,
|
|
263
|
-
}));
|
|
533
|
+
const accepted = resolveTermsAcceptance({ headless, acceptTerms: opts.acceptTerms }) ??
|
|
534
|
+
(await confirm({
|
|
535
|
+
message: "Do you accept the Terms of Service?",
|
|
536
|
+
default: false,
|
|
537
|
+
theme: output.bankrTheme,
|
|
538
|
+
}));
|
|
264
539
|
if (!accepted) {
|
|
265
540
|
output.fatal("Terms must be accepted to continue.");
|
|
266
541
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
await callAcceptTerms(apiUrl, tokens);
|
|
270
|
-
termsSpin.succeed("Terms accepted");
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
output.failWith(termsSpin, "Failed to accept terms", err);
|
|
274
|
-
}
|
|
542
|
+
await runGatedStep(apiUrl, tokens, "Accepting terms...", () => callAcceptTerms(apiUrl, tokens), { retryOnNetworkBlip: true });
|
|
543
|
+
output.success("Terms accepted");
|
|
275
544
|
}
|
|
276
545
|
// Step 4: Generate API key
|
|
277
546
|
output.blank();
|
|
278
|
-
const defaultKeyName =
|
|
547
|
+
const defaultKeyName = defaultCliKeyName();
|
|
279
548
|
const keyName = opts.keyName ??
|
|
280
549
|
(headless
|
|
281
550
|
? defaultKeyName
|
|
@@ -301,23 +570,15 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
301
570
|
default: false,
|
|
302
571
|
theme: output.bankrTheme,
|
|
303
572
|
}));
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
allowedIps: opts.allowedIps,
|
|
314
|
-
allowedRecipients: opts.allowedRecipients,
|
|
315
|
-
});
|
|
316
|
-
keySpin.stop();
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
output.failWith(keySpin, "Failed to generate API key", err);
|
|
320
|
-
}
|
|
573
|
+
const apiKeyResult = await runGatedStep(apiUrl, tokens, "Generating API key...", () => callGenerateApiKey(apiUrl, tokens, {
|
|
574
|
+
name: keyName.trim(),
|
|
575
|
+
walletApiEnabled: enableWallet,
|
|
576
|
+
...overrides,
|
|
577
|
+
tokenLaunchApiEnabled: enableTokenLaunch,
|
|
578
|
+
llmGatewayEnabled: enableLlm,
|
|
579
|
+
allowedIps: opts.allowedIps,
|
|
580
|
+
allowedRecipients: opts.allowedRecipients,
|
|
581
|
+
}));
|
|
321
582
|
// Show result
|
|
322
583
|
output.blank();
|
|
323
584
|
const features = [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function projectViewCommand(opts: {
|
|
2
2
|
json?: boolean;
|
|
3
3
|
}): Promise<void>;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function projectCreateCommand(opts: {
|
|
5
5
|
name?: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
token?: string;
|
|
@@ -9,7 +9,7 @@ export declare function profileCreateCommand(opts: {
|
|
|
9
9
|
website?: string;
|
|
10
10
|
json?: boolean;
|
|
11
11
|
}): Promise<void>;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function projectUpdateCommand(opts: {
|
|
13
13
|
slug?: string;
|
|
14
14
|
name?: string;
|
|
15
15
|
description?: string;
|
|
@@ -18,13 +18,13 @@ export declare function profileUpdateCommand(opts: {
|
|
|
18
18
|
website?: string;
|
|
19
19
|
json?: boolean;
|
|
20
20
|
}): Promise<void>;
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function projectDeleteCommand(opts: {
|
|
22
22
|
slug?: string;
|
|
23
23
|
}): Promise<void>;
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function projectAddUpdateCommand(opts: {
|
|
25
25
|
slug?: string;
|
|
26
26
|
title?: string;
|
|
27
27
|
content?: string;
|
|
28
28
|
json?: boolean;
|
|
29
29
|
}): Promise<void>;
|
|
30
|
-
//# sourceMappingURL=
|
|
30
|
+
//# sourceMappingURL=project.d.ts.map
|