@bankr/cli 0.3.35 → 0.3.38
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 +12 -7
- package/dist/cli.js +10 -6
- package/dist/commands/llm.js +2 -2
- package/dist/commands/login.d.ts +52 -0
- package/dist/commands/login.js +242 -49
- package/dist/commands/update.js +2 -3
- package/dist/lib/api.d.ts +3 -2
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ bankr whoami
|
|
|
58
58
|
bankr logout
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Accounts with two-factor authentication enabled get a browser step during `bankr login email`: the CLI prints (and, when interactive, opens) an approval link on bankr.bot, you verify with your passkey there, and the CLI continues once it sees the approval. The link is valid for five minutes; in a headless run, pass it along to whoever holds the passkey. The approval also covers the rest of that login for five minutes, so if you linger on the key-name prompts past that you'll get a second link. If it expires, or you'd rather skip the hop, generate a key at [bankr.bot/api-keys](https://bankr.bot/api-keys) and use `bankr login --api-key`.
|
|
62
|
+
|
|
61
63
|
### Prompts
|
|
62
64
|
|
|
63
65
|
```bash
|
|
@@ -94,9 +96,11 @@ bankr launch --name "TESTCOIN" --image "https://example.com/img.png" \
|
|
|
94
96
|
# Quick launch (name only, skip confirmation)
|
|
95
97
|
bankr launch --name "TESTCOIN" -y
|
|
96
98
|
|
|
97
|
-
# Pair the pool with a tokenized stock instead of WETH (Robinhood Chain or Base)
|
|
99
|
+
# Pair the pool with a tokenized stock or another listed quote token instead of WETH (Robinhood Chain or Base)
|
|
98
100
|
bankr launch quotes --chain robinhood # list the quote tokens you can pair with
|
|
99
101
|
bankr launch --name "TESTCOIN" --chain robinhood --quote TSLA -y
|
|
102
|
+
bankr launch --name "TESTCOIN" --chain robinhood --quote musebook -y
|
|
103
|
+
bankr launch --name "TESTCOIN" --chain robinhood --quote BNKR -y # the Robinhood Chain BNKR contract
|
|
100
104
|
|
|
101
105
|
# Fee recipient types: x, farcaster, ens, wallet
|
|
102
106
|
bankr launch --name "TESTCOIN" --fee "vitalik.eth" --fee-type ens -y
|
|
@@ -178,12 +182,13 @@ Credentials are stored in `~/.bankr/config.json`.
|
|
|
178
182
|
|
|
179
183
|
Environment variables override stored values:
|
|
180
184
|
|
|
181
|
-
| Variable | Description
|
|
182
|
-
| --------------- |
|
|
183
|
-
| `BANKR_API_KEY` | API key (overrides stored key)
|
|
184
|
-
| `BANKR_API_URL` | API URL (default: `https://api.bankr.bot`)
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
185
|
+
| Variable | Description |
|
|
186
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
187
|
+
| `BANKR_API_KEY` | API key (overrides stored key) |
|
|
188
|
+
| `BANKR_API_URL` | API URL (default: `https://api.bankr.bot`) |
|
|
189
|
+
| `BANKR_WEB_URL` | Web app URL for browser approval links (default: derived from the API URL) |
|
|
190
|
+
| `BANKR_LLM_KEY` | LLM gateway key (falls back to `BANKR_API_KEY` if not set) |
|
|
191
|
+
| `BANKR_LLM_URL` | LLM gateway URL (default: `https://llm.bankr.bot`) |
|
|
187
192
|
|
|
188
193
|
## API Key
|
|
189
194
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { Command } from "commander";
|
|
4
3
|
import { input } from "@inquirer/prompts";
|
|
5
4
|
import { balancesCommand } from "./commands/balances.js";
|
|
@@ -9,7 +8,7 @@ import { swapCommand } from "./commands/swap.js";
|
|
|
9
8
|
import { skillsCommand } from "./commands/capabilities.js";
|
|
10
9
|
import { cancelCommand } from "./commands/cancel.js";
|
|
11
10
|
import { configGetCommand, configSetCommand } from "./commands/config.js";
|
|
12
|
-
import { setConfigPath } from "./lib/config.js";
|
|
11
|
+
import { CLI_VERSION, setConfigPath } from "./lib/config.js";
|
|
13
12
|
import { getErrorMessage } from "./lib/errors.js";
|
|
14
13
|
import { passthroughArgs } from "./lib/passthrough.js";
|
|
15
14
|
import { claudeCommand, creditsCommand, creditsAddCommand, creditsAutoCommand, modelsCommand, opencodeCommand, setupClaudeCodeCommand, setupCursorCommand, setupOpenclawCommand, setupOpenCodeCommand, } from "./commands/llm.js";
|
|
@@ -33,7 +32,6 @@ import { filesListCommand, filesUploadCommand, filesDownloadCommand, filesMkdirC
|
|
|
33
32
|
import { clubStatusCommand, clubSignupCommand, clubCancelCommand, } from "./commands/club.js";
|
|
34
33
|
import * as output from "./lib/output.js";
|
|
35
34
|
import { checkForUpdate } from "./lib/updateCheck.js";
|
|
36
|
-
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
37
35
|
/**
|
|
38
36
|
* Read prompt text from stdin (piped) or interactively.
|
|
39
37
|
* Bypasses shell expansion so dollar signs and special characters are preserved.
|
|
@@ -81,7 +79,7 @@ function colorizeHelp(text) {
|
|
|
81
79
|
program
|
|
82
80
|
.name("bankr")
|
|
83
81
|
.description("Bankr AI agent CLI\n\n Docs: https://docs.bankr.bot\n Skills: https://docs.bankr.bot/skills/overview")
|
|
84
|
-
.version(
|
|
82
|
+
.version(CLI_VERSION, "-v, --version", "Show CLI version")
|
|
85
83
|
.helpOption("-h, --help", "Display help")
|
|
86
84
|
.helpCommand(false)
|
|
87
85
|
.option("--config <path>", "Path to config file (default: ~/.bankr/config.json)")
|
|
@@ -126,8 +124,10 @@ loginCmd
|
|
|
126
124
|
.option("--accept-terms", "Accept Terms of Service (skips prompt)")
|
|
127
125
|
.option("--key-name <name>", "API key name (default: CLI)")
|
|
128
126
|
.optionsGroup("API Permissions")
|
|
129
|
-
.option("--read-
|
|
127
|
+
.option("--read-only", "Restrict the API key to read operations")
|
|
128
|
+
.option("--read-write", "Allow transactions (the default for new keys)")
|
|
130
129
|
.option("--no-wallet-api", "Disable Wallet API (enabled by default)")
|
|
130
|
+
.option("--no-agent-api", "Disable Agent API (enabled by default)")
|
|
131
131
|
.option("--no-token-launch", "Disable Token Launch API (enabled by default)")
|
|
132
132
|
.option("--llm", "Enable LLM gateway on the API key")
|
|
133
133
|
.optionsGroup("Security")
|
|
@@ -142,8 +142,10 @@ loginCmd
|
|
|
142
142
|
code: opts.code,
|
|
143
143
|
acceptTerms: opts.acceptTerms,
|
|
144
144
|
keyName: opts.keyName,
|
|
145
|
+
readOnly: opts.readOnly,
|
|
145
146
|
readWrite: opts.readWrite,
|
|
146
147
|
walletApi: opts.walletApi,
|
|
148
|
+
agentApi: opts.agentApi,
|
|
147
149
|
tokenLaunch: opts.tokenLaunch,
|
|
148
150
|
llm: opts.llm,
|
|
149
151
|
allowedIps: opts.allowedIps,
|
|
@@ -158,6 +160,7 @@ loginCmd
|
|
|
158
160
|
.optionsGroup("API Permissions")
|
|
159
161
|
.option("--read-write", "Disable read-only mode (allow transactions)")
|
|
160
162
|
.option("--no-wallet-api", "Disable Wallet API (enabled by default)")
|
|
163
|
+
.option("--no-agent-api", "Disable Agent API (enabled by default)")
|
|
161
164
|
.option("--no-token-launch", "Disable Token Launch API (enabled by default)")
|
|
162
165
|
.optionsGroup("Security")
|
|
163
166
|
.option("--allowed-ips <ips>", "Comma-separated IP/CIDR allowlist, e.g. 1.2.3.4,10.0.0.0/24")
|
|
@@ -171,6 +174,7 @@ loginCmd
|
|
|
171
174
|
keyName: opts.keyName,
|
|
172
175
|
readWrite: opts.readWrite,
|
|
173
176
|
walletApi: opts.walletApi,
|
|
177
|
+
agentApi: opts.agentApi,
|
|
174
178
|
tokenLaunch: opts.tokenLaunch,
|
|
175
179
|
allowedIps: opts.allowedIps,
|
|
176
180
|
allowedRecipients: opts.allowedRecipients,
|
|
@@ -1078,7 +1082,7 @@ async function main() {
|
|
|
1078
1082
|
}
|
|
1079
1083
|
output.fatal(getErrorMessage(err));
|
|
1080
1084
|
}
|
|
1081
|
-
await checkForUpdate(
|
|
1085
|
+
await checkForUpdate(CLI_VERSION);
|
|
1082
1086
|
}
|
|
1083
1087
|
main();
|
|
1084
1088
|
//# sourceMappingURL=cli.js.map
|
package/dist/commands/llm.js
CHANGED
|
@@ -869,10 +869,10 @@ export async function modelsCommand(opts = {}) {
|
|
|
869
869
|
output.dim(` Gateway: ${getLlmUrl()}`);
|
|
870
870
|
output.dim(` ${shown.length} model${shown.length === 1 ? "" : "s"}${view?.countSuffix ?? " available"}${live ? "" : " (cached)"}`);
|
|
871
871
|
if (!view && shown.some((m) => m.private)) {
|
|
872
|
-
output.dim(" Private (TEE) — append :private to the model id (e.g. glm-5.
|
|
872
|
+
output.dim(" Private (TEE) — append :private to the model id (e.g. glm-5.3-flash:private). List: bankr llm models --private");
|
|
873
873
|
}
|
|
874
874
|
if (!view && shown.some((m) => m.zdr)) {
|
|
875
|
-
output.dim(" Zero data retention — append :zdr to the model id (e.g. glm-5.
|
|
875
|
+
output.dim(" Zero data retention — append :zdr to the model id (e.g. glm-5.3-flash:zdr), or turn it on account-wide under Settings in the web terminal. List: bankr llm models --zdr");
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
/* ─────────────────────── bankr llm credits ─────────────────────────────────── */
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a login-gated call answers with the MFA step-up envelope. A
|
|
3
|
+
* passkey ceremony can't run in a terminal, so the email flow can't recover;
|
|
4
|
+
* the caller turns this into the browser-key guidance instead of the server's
|
|
5
|
+
* fallback text.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MfaStepUpError extends Error {
|
|
8
|
+
readonly context: string;
|
|
9
|
+
constructor(context: string);
|
|
10
|
+
}
|
|
11
|
+
/** Keyed on `code`, never on message text, so a server wording change can't
|
|
12
|
+
* silently turn this back into the opaque failure. */
|
|
13
|
+
export declare function mfaStepUpErrorFrom(body: unknown): MfaStepUpError | null;
|
|
14
|
+
/** The web app that fronts an API URL: `BANKR_WEB_URL` when set (local dev,
|
|
15
|
+
* staging), bankr.bot for the official API, else the API host with its
|
|
16
|
+
* `api.`/`api-` prefix dropped. */
|
|
17
|
+
export declare function deriveWebOrigin(apiUrl: string): string;
|
|
1
18
|
/**
|
|
2
19
|
* Both Privy credentials minted by the OTP exchange. The identity token alone
|
|
3
20
|
* authenticates, but only the ACCESS token carries the session `sid` claim —
|
|
@@ -11,6 +28,39 @@ interface CliAuthTokens {
|
|
|
11
28
|
/** The auth headers every authenticated CLI call sends — one place, so a new
|
|
12
29
|
* endpoint can't silently omit the access token and go unrevocable again. */
|
|
13
30
|
export declare function authHeaders(tokens: CliAuthTokens): Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Maps the permission opt-out flags to the create-key body. Omitted fields
|
|
33
|
+
* are dropped by JSON.stringify so the server's API_KEY_CREATE_DEFAULTS
|
|
34
|
+
* decide them; only an explicit flag sends a value.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveKeyPermissionOverrides(opts: {
|
|
37
|
+
readOnly?: boolean;
|
|
38
|
+
readWrite?: boolean;
|
|
39
|
+
agentApi?: boolean;
|
|
40
|
+
}): {
|
|
41
|
+
readOnly?: boolean;
|
|
42
|
+
agentApiEnabled?: boolean;
|
|
43
|
+
};
|
|
44
|
+
declare const CHALLENGE_STATUSES: readonly ["pending", "verified", "expired", "abandoned", "failed"];
|
|
45
|
+
export type ChallengeStatus = (typeof CHALLENGE_STATUSES)[number];
|
|
46
|
+
/**
|
|
47
|
+
* One poll's outcome from its HTTP status. Only a definitive refusal ends the
|
|
48
|
+
* wait: the gate (401, an API without this route yet), another wallet's token
|
|
49
|
+
* (403) or a gone challenge (404). Anything else — 429, 5xx, a dropped
|
|
50
|
+
* connection — is a blip, so the wait keeps polling as if still pending.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pollOutcome(httpStatus: number, body: unknown): ChallengeStatus;
|
|
53
|
+
/**
|
|
54
|
+
* Poll until the challenge leaves `pending` or the deadline passes: once at
|
|
55
|
+
* once, then every 2s for the first 30s, then every 5s. Pure over its
|
|
56
|
+
* collaborators so the loop is unit-testable without a network.
|
|
57
|
+
*/
|
|
58
|
+
export declare function waitForChallengeApproval(deps: {
|
|
59
|
+
poll: () => Promise<ChallengeStatus>;
|
|
60
|
+
sleep: (ms: number) => Promise<void>;
|
|
61
|
+
deadlineMs: number;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
}): Promise<ChallengeStatus>;
|
|
14
64
|
export declare function loginCommand(opts: {
|
|
15
65
|
apiUrl?: string;
|
|
16
66
|
apiKey?: string;
|
|
@@ -18,8 +68,10 @@ export declare function loginCommand(opts: {
|
|
|
18
68
|
code?: string;
|
|
19
69
|
acceptTerms?: boolean;
|
|
20
70
|
keyName?: string;
|
|
71
|
+
readOnly?: boolean;
|
|
21
72
|
readWrite?: boolean;
|
|
22
73
|
walletApi?: boolean;
|
|
74
|
+
agentApi?: boolean;
|
|
23
75
|
tokenLaunch?: boolean;
|
|
24
76
|
llm?: boolean;
|
|
25
77
|
llmKey?: string;
|
package/dist/commands/login.js
CHANGED
|
@@ -8,12 +8,50 @@ 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
|
+
/** The web app that fronts an API URL: `BANKR_WEB_URL` when set (local dev,
|
|
39
|
+
* staging), bankr.bot for the official API, else the API host with its
|
|
40
|
+
* `api.`/`api-` prefix dropped. */
|
|
41
|
+
export function deriveWebOrigin(apiUrl) {
|
|
42
|
+
const override = process.env.BANKR_WEB_URL?.trim();
|
|
43
|
+
if (override)
|
|
44
|
+
return override.replace(/\/+$/, "");
|
|
45
|
+
if (apiUrl === DEFAULT_API_URL)
|
|
46
|
+
return "https://bankr.bot";
|
|
47
|
+
// Anchored to the host's start so a host merely containing "api." is intact.
|
|
48
|
+
return apiUrl.replace(/\/+$/, "").replace(/^(https?:\/\/)api[.-]/, "$1");
|
|
49
|
+
}
|
|
11
50
|
function deriveDashboardUrl(apiUrl) {
|
|
12
51
|
if (apiUrl === DEFAULT_API_URL) {
|
|
13
52
|
return DEFAULT_DASHBOARD_URL;
|
|
14
53
|
}
|
|
15
|
-
return (apiUrl
|
|
16
|
-
"/api-keys");
|
|
54
|
+
return `${deriveWebOrigin(apiUrl)}/api-keys`;
|
|
17
55
|
}
|
|
18
56
|
async function fetchPrivyConfig(apiUrl) {
|
|
19
57
|
const res = await fetch(`${apiUrl}/cli/config`, {
|
|
@@ -52,6 +90,26 @@ export function authHeaders(tokens) {
|
|
|
52
90
|
"x-access-token": tokens.accessToken,
|
|
53
91
|
};
|
|
54
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Maps the permission opt-out flags to the create-key body. Omitted fields
|
|
95
|
+
* are dropped by JSON.stringify so the server's API_KEY_CREATE_DEFAULTS
|
|
96
|
+
* decide them; only an explicit flag sends a value.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveKeyPermissionOverrides(opts) {
|
|
99
|
+
if (opts.readOnly && opts.readWrite) {
|
|
100
|
+
throw new Error("--read-only and --read-write are mutually exclusive");
|
|
101
|
+
}
|
|
102
|
+
// Build conditionally: an own `undefined` key would clobber a value the
|
|
103
|
+
// caller set before spreading this in.
|
|
104
|
+
const overrides = {};
|
|
105
|
+
if (opts.readOnly)
|
|
106
|
+
overrides.readOnly = true;
|
|
107
|
+
else if (opts.readWrite)
|
|
108
|
+
overrides.readOnly = false;
|
|
109
|
+
if (opts.agentApi === false)
|
|
110
|
+
overrides.agentApiEnabled = false;
|
|
111
|
+
return overrides;
|
|
112
|
+
}
|
|
55
113
|
async function verifyPrivyOtp(privyConfig, email, code) {
|
|
56
114
|
const res = await fetch("https://auth.privy.io/api/v1/passwordless/authenticate", {
|
|
57
115
|
method: "POST",
|
|
@@ -80,7 +138,10 @@ async function callGenerateWallet(apiUrl, tokens) {
|
|
|
80
138
|
});
|
|
81
139
|
const body = (await res.json());
|
|
82
140
|
if (!res.ok) {
|
|
83
|
-
throw
|
|
141
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
142
|
+
new Error(body.message ||
|
|
143
|
+
body.error ||
|
|
144
|
+
`Wallet generation failed (${res.status})`));
|
|
84
145
|
}
|
|
85
146
|
return body;
|
|
86
147
|
}
|
|
@@ -91,7 +152,8 @@ async function callAcceptTerms(apiUrl, tokens) {
|
|
|
91
152
|
});
|
|
92
153
|
if (!res.ok) {
|
|
93
154
|
const body = (await res.json().catch(() => ({})));
|
|
94
|
-
throw
|
|
155
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
156
|
+
new Error(body.message || body.error || `Accept terms failed (${res.status})`));
|
|
95
157
|
}
|
|
96
158
|
}
|
|
97
159
|
async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
@@ -102,10 +164,115 @@ async function callGenerateApiKey(apiUrl, tokens, opts) {
|
|
|
102
164
|
});
|
|
103
165
|
const body = (await res.json());
|
|
104
166
|
if (!res.ok) {
|
|
105
|
-
throw
|
|
167
|
+
throw (mfaStepUpErrorFrom(body) ??
|
|
168
|
+
new Error(body.message ||
|
|
169
|
+
body.error ||
|
|
170
|
+
`API key generation failed (${res.status})`));
|
|
106
171
|
}
|
|
107
172
|
return body;
|
|
108
173
|
}
|
|
174
|
+
// ── Out-of-band MFA approval ─────────────────────────────────────────
|
|
175
|
+
// Mirrors `MFA_CHALLENGE_STATUSES` in @bankr/shared (the CLI can't import it).
|
|
176
|
+
const CHALLENGE_STATUSES = [
|
|
177
|
+
"pending",
|
|
178
|
+
"verified",
|
|
179
|
+
"expired",
|
|
180
|
+
"abandoned",
|
|
181
|
+
"failed",
|
|
182
|
+
];
|
|
183
|
+
/** Narrow the status poll body; anything unrecognised reads as `failed`. */
|
|
184
|
+
function parseChallengeStatus(body) {
|
|
185
|
+
const status = body?.status;
|
|
186
|
+
return CHALLENGE_STATUSES.includes(status)
|
|
187
|
+
? status
|
|
188
|
+
: "failed";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One poll's outcome from its HTTP status. Only a definitive refusal ends the
|
|
192
|
+
* wait: the gate (401, an API without this route yet), another wallet's token
|
|
193
|
+
* (403) or a gone challenge (404). Anything else — 429, 5xx, a dropped
|
|
194
|
+
* connection — is a blip, so the wait keeps polling as if still pending.
|
|
195
|
+
*/
|
|
196
|
+
export function pollOutcome(httpStatus, body) {
|
|
197
|
+
if (httpStatus >= 200 && httpStatus < 300)
|
|
198
|
+
return parseChallengeStatus(body);
|
|
199
|
+
return [401, 403, 404].includes(httpStatus) ? "failed" : "pending";
|
|
200
|
+
}
|
|
201
|
+
/** Poll cadence: 2s while an approval is likely imminent, then 5s. */
|
|
202
|
+
const POLL_FAST_MS = 2000;
|
|
203
|
+
const POLL_SLOW_MS = 5000;
|
|
204
|
+
const POLL_FAST_WINDOW_MS = 30000;
|
|
205
|
+
/**
|
|
206
|
+
* Poll until the challenge leaves `pending` or the deadline passes: once at
|
|
207
|
+
* once, then every 2s for the first 30s, then every 5s. Pure over its
|
|
208
|
+
* collaborators so the loop is unit-testable without a network.
|
|
209
|
+
*/
|
|
210
|
+
export async function waitForChallengeApproval(deps) {
|
|
211
|
+
const now = deps.now ?? Date.now;
|
|
212
|
+
const startedAt = now();
|
|
213
|
+
for (;;) {
|
|
214
|
+
const status = await deps.poll();
|
|
215
|
+
if (status !== "pending")
|
|
216
|
+
return status;
|
|
217
|
+
if (now() >= deps.deadlineMs)
|
|
218
|
+
return "expired";
|
|
219
|
+
await deps.sleep(now() - startedAt < POLL_FAST_WINDOW_MS ? POLL_FAST_MS : POLL_SLOW_MS);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function createMfaChallenge(apiUrl, tokens, context) {
|
|
223
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: authHeaders(tokens),
|
|
226
|
+
body: JSON.stringify({ context }),
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
throw new Error(`Couldn't start passkey approval (${res.status})`);
|
|
230
|
+
}
|
|
231
|
+
return (await res.json());
|
|
232
|
+
}
|
|
233
|
+
async function readMfaChallengeStatus(apiUrl, tokens, token) {
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(`${apiUrl}/user/mfa/challenge/${token}`, {
|
|
236
|
+
headers: authHeaders(tokens),
|
|
237
|
+
});
|
|
238
|
+
return pollOutcome(res.status, await res.json().catch(() => null));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return "pending";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Mirrors the server's challenge TTL; the deadline is kept on the local clock
|
|
245
|
+
* so a skewed machine can't expire the wait early (the poll reports a real
|
|
246
|
+
* server-side expiry as `expired` regardless). */
|
|
247
|
+
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
248
|
+
/**
|
|
249
|
+
* The browser hop for an MFA-enabled account: create a challenge for THIS
|
|
250
|
+
* session, hand the user the confirm link, and wait for their passkey on the
|
|
251
|
+
* web to mint the grants. Resolves true once approved.
|
|
252
|
+
*/
|
|
253
|
+
async function approveInBrowser(apiUrl, tokens) {
|
|
254
|
+
const { token } = await createMfaChallenge(apiUrl, tokens, "api-key:generate");
|
|
255
|
+
const url = `${deriveWebOrigin(apiUrl)}/mfa/confirm/${token}`;
|
|
256
|
+
output.blank();
|
|
257
|
+
output.warn("Two-factor authentication is enabled on this account.");
|
|
258
|
+
output.info("Approve this login with your passkey:");
|
|
259
|
+
output.brandBold(` ${url}`);
|
|
260
|
+
if (!output.isNonInteractive()) {
|
|
261
|
+
await open(url).catch(() => undefined);
|
|
262
|
+
}
|
|
263
|
+
const spin = output.spinner("Waiting for approval in your browser...");
|
|
264
|
+
const status = await waitForChallengeApproval({
|
|
265
|
+
poll: () => readMfaChallengeStatus(apiUrl, tokens, token),
|
|
266
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
267
|
+
deadlineMs: Date.now() + CHALLENGE_TTL_MS,
|
|
268
|
+
});
|
|
269
|
+
if (status === "verified") {
|
|
270
|
+
spin.succeed("Approved");
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
spin.fail(`Approval ${status}`);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
109
276
|
// ── OTP verification helpers ─────────────────────────────────────────
|
|
110
277
|
async function fetchPrivyConfigOrFail(apiUrl) {
|
|
111
278
|
const config = await fetchPrivyConfig(apiUrl).catch(() => null);
|
|
@@ -200,20 +367,57 @@ async function authenticateWithEmail(apiUrl, providedEmail, providedCode) {
|
|
|
200
367
|
}
|
|
201
368
|
output.fatal("Verification failed. Please try again.");
|
|
202
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Run a login-gated call; on the MFA step-up envelope, take the browser
|
|
372
|
+
* approval hop once and retry. A second refusal, or an approval that expires
|
|
373
|
+
* or fails, exits with the browser-key guidance.
|
|
374
|
+
*/
|
|
375
|
+
async function runGatedStep(apiUrl, tokens, label, run) {
|
|
376
|
+
for (const retry of [false, true]) {
|
|
377
|
+
const spin = output.spinner(label);
|
|
378
|
+
try {
|
|
379
|
+
const result = await run();
|
|
380
|
+
spin.stop();
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
spin.stop();
|
|
385
|
+
if (!(err instanceof MfaStepUpError) || retry) {
|
|
386
|
+
return failLoginStep(`${label} failed`, err, apiUrl);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const approved = await approveInBrowser(apiUrl, tokens).catch((err) => {
|
|
390
|
+
output.error(getErrorMessage(err));
|
|
391
|
+
return false;
|
|
392
|
+
});
|
|
393
|
+
if (!approved)
|
|
394
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
395
|
+
}
|
|
396
|
+
throw new Error("unreachable");
|
|
397
|
+
}
|
|
398
|
+
/** Exit with the generic epilogue, or the browser-key path on a step-up
|
|
399
|
+
* refusal the approval hop didn't clear. */
|
|
400
|
+
function failLoginStep(label, err, apiUrl) {
|
|
401
|
+
if (!(err instanceof MfaStepUpError)) {
|
|
402
|
+
return output.fatal(`${label}: ${getErrorMessage(err)}`);
|
|
403
|
+
}
|
|
404
|
+
return exitWithApiKeyGuidance(apiUrl);
|
|
405
|
+
}
|
|
406
|
+
/** The remediation is the point of this message, so every line stays on
|
|
407
|
+
* stderr with the headline: a scripted caller capturing stderr sees all of it. */
|
|
408
|
+
function exitWithApiKeyGuidance(apiUrl) {
|
|
409
|
+
output.error("Two-factor authentication is enabled on this account.");
|
|
410
|
+
output.errorDetail(" The passkey approval didn't complete.");
|
|
411
|
+
output.errorDetail(` Generate an API key at ${deriveDashboardUrl(apiUrl)} instead (verify with your passkey there), then run:`);
|
|
412
|
+
output.errorDetail(" bankr login --api-key bk_YOUR_KEY");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
203
415
|
// ── Email login flow ────────────────────────────────────────────────
|
|
204
416
|
async function emailLoginFlow(apiUrl, opts) {
|
|
205
417
|
// Step 1: Authenticate via email OTP
|
|
206
418
|
const { tokens } = await authenticateWithEmail(apiUrl, opts.email, opts.code);
|
|
207
419
|
// Step 2: Generate/resolve wallet
|
|
208
|
-
const
|
|
209
|
-
let wallet;
|
|
210
|
-
try {
|
|
211
|
-
wallet = await callGenerateWallet(apiUrl, tokens);
|
|
212
|
-
walletSpin.stop();
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
output.failWith(walletSpin, "Wallet setup failed", err);
|
|
216
|
-
}
|
|
420
|
+
const wallet = await runGatedStep(apiUrl, tokens, "Setting up wallet...", () => callGenerateWallet(apiUrl, tokens));
|
|
217
421
|
// Show wallet info
|
|
218
422
|
output.blank();
|
|
219
423
|
if (wallet.isNewUser) {
|
|
@@ -244,14 +448,8 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
244
448
|
if (!accepted) {
|
|
245
449
|
output.fatal("Terms must be accepted to continue.");
|
|
246
450
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
await callAcceptTerms(apiUrl, tokens);
|
|
250
|
-
termsSpin.succeed("Terms accepted");
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
output.failWith(termsSpin, "Failed to accept terms", err);
|
|
254
|
-
}
|
|
451
|
+
await runGatedStep(apiUrl, tokens, "Accepting terms...", () => callAcceptTerms(apiUrl, tokens));
|
|
452
|
+
output.success("Terms accepted");
|
|
255
453
|
}
|
|
256
454
|
// Step 4: Generate API key
|
|
257
455
|
output.blank();
|
|
@@ -266,10 +464,12 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
266
464
|
validate: (v) => v.trim().length >= 3 ? true : "Name must be at least 3 characters.",
|
|
267
465
|
}));
|
|
268
466
|
// Fine-grained API flags. walletApi and tokenLaunch default to enabled.
|
|
269
|
-
// --read-write only
|
|
467
|
+
// --read-only/--read-write only control readOnly, not which APIs are enabled.
|
|
270
468
|
const enableWallet = opts.walletApi ?? true;
|
|
271
|
-
//
|
|
272
|
-
|
|
469
|
+
// readOnly and agentApiEnabled are only sent when a flag asks for it, so the
|
|
470
|
+
// server's API_KEY_CREATE_DEFAULTS (the bankr.bot/api-keys form) decide them.
|
|
471
|
+
// The --read-only/--read-write conflict was already rejected in loginCommand.
|
|
472
|
+
const overrides = resolveKeyPermissionOverrides(opts);
|
|
273
473
|
const enableTokenLaunch = opts.tokenLaunch ?? true;
|
|
274
474
|
const enableLlm = opts.llm ??
|
|
275
475
|
(headless
|
|
@@ -279,32 +479,20 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
279
479
|
default: false,
|
|
280
480
|
theme: output.bankrTheme,
|
|
281
481
|
}));
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
agentApiEnabled: enableAgent,
|
|
292
|
-
readOnly: opts.readWrite ? false : undefined,
|
|
293
|
-
tokenLaunchApiEnabled: enableTokenLaunch,
|
|
294
|
-
llmGatewayEnabled: enableLlm,
|
|
295
|
-
allowedIps: opts.allowedIps,
|
|
296
|
-
allowedRecipients: opts.allowedRecipients,
|
|
297
|
-
});
|
|
298
|
-
keySpin.stop();
|
|
299
|
-
}
|
|
300
|
-
catch (err) {
|
|
301
|
-
output.failWith(keySpin, "Failed to generate API key", err);
|
|
302
|
-
}
|
|
482
|
+
const apiKeyResult = await runGatedStep(apiUrl, tokens, "Generating API key...", () => callGenerateApiKey(apiUrl, tokens, {
|
|
483
|
+
name: keyName.trim(),
|
|
484
|
+
walletApiEnabled: enableWallet,
|
|
485
|
+
...overrides,
|
|
486
|
+
tokenLaunchApiEnabled: enableTokenLaunch,
|
|
487
|
+
llmGatewayEnabled: enableLlm,
|
|
488
|
+
allowedIps: opts.allowedIps,
|
|
489
|
+
allowedRecipients: opts.allowedRecipients,
|
|
490
|
+
}));
|
|
303
491
|
// Show result
|
|
304
492
|
output.blank();
|
|
305
493
|
const features = [
|
|
306
494
|
enableWallet && "Wallet",
|
|
307
|
-
|
|
495
|
+
apiKeyResult.agentApiEnabled && "Agent",
|
|
308
496
|
enableTokenLaunch && "Token Launch",
|
|
309
497
|
enableLlm && "LLM",
|
|
310
498
|
].filter(Boolean);
|
|
@@ -321,7 +509,6 @@ async function emailLoginFlow(apiUrl, opts) {
|
|
|
321
509
|
];
|
|
322
510
|
output.dim(` Recipients: ${parts.join(", ")}`);
|
|
323
511
|
}
|
|
324
|
-
output.dim(" Agent API: enable at bankr.bot/api-keys");
|
|
325
512
|
output.dim(" Manage keys at bankr.bot/api-keys");
|
|
326
513
|
return apiKeyResult.apiKey;
|
|
327
514
|
}
|
|
@@ -372,7 +559,7 @@ async function siweLoginFlow(apiUrl, opts) {
|
|
|
372
559
|
keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
|
|
373
560
|
readOnly: opts.readOnly ?? false,
|
|
374
561
|
walletApiEnabled: opts.walletApi ?? true,
|
|
375
|
-
|
|
562
|
+
...resolveKeyPermissionOverrides({ agentApi: opts.agentApi }),
|
|
376
563
|
tokenLaunchApiEnabled: opts.tokenLaunch ?? true,
|
|
377
564
|
allowedIps: opts.allowedIps,
|
|
378
565
|
allowedRecipients: opts.allowedRecipients,
|
|
@@ -492,6 +679,11 @@ export async function loginCommand(opts) {
|
|
|
492
679
|
if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
|
|
493
680
|
output.fatal("--api-key requires a non-empty value");
|
|
494
681
|
}
|
|
682
|
+
// Reject conflicting permission flags before any network call, so a
|
|
683
|
+
// headless step-2 login doesn't burn its one-time code on a usage error.
|
|
684
|
+
if (opts.readOnly && opts.readWrite) {
|
|
685
|
+
output.fatal("--read-only and --read-write are mutually exclusive");
|
|
686
|
+
}
|
|
495
687
|
// Non-interactive mode requires a headless flag combo. Without one, the
|
|
496
688
|
// command would hang on a prompt — fail fast with the available paths.
|
|
497
689
|
// Email step 1 (`login email <addr>` without --code) is also headless:
|
|
@@ -569,6 +761,7 @@ export async function loginCommand(opts) {
|
|
|
569
761
|
keyName: opts.keyName,
|
|
570
762
|
readOnly: !opts.readWrite,
|
|
571
763
|
walletApi: opts.walletApi,
|
|
764
|
+
agentApi: opts.agentApi,
|
|
572
765
|
tokenLaunch: opts.tokenLaunch,
|
|
573
766
|
...getParsedRestrictions(),
|
|
574
767
|
});
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { execSync } from "node:child_process";
|
|
3
2
|
import * as output from "../lib/output.js";
|
|
4
3
|
import { getErrorMessage } from "../lib/errors.js";
|
|
4
|
+
import { CLI_VERSION } from "../lib/config.js";
|
|
5
5
|
const PACKAGE_NAME = "@bankr/cli";
|
|
6
6
|
/**
|
|
7
7
|
* Compare two semver version strings.
|
|
@@ -31,8 +31,7 @@ async function fetchLatestVersion() {
|
|
|
31
31
|
return data.version;
|
|
32
32
|
}
|
|
33
33
|
function getCurrentVersion() {
|
|
34
|
-
|
|
35
|
-
return pkg.version;
|
|
34
|
+
return CLI_VERSION;
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
38
37
|
* Detect which package manager installed the CLI globally.
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -224,7 +224,8 @@ export interface DeployTokenRequest {
|
|
|
224
224
|
provider?: LaunchProvider;
|
|
225
225
|
/** Quote the pool in a tokenized stock (Robinhood Chain / Base, Doppler). */
|
|
226
226
|
pairedStockAddress?: string;
|
|
227
|
-
/** Quote the pool in one of the fixed
|
|
227
|
+
/** Quote the pool in one of the chain's fixed Doppler allowlist tokens
|
|
228
|
+
* (Base or Robinhood Chain). */
|
|
228
229
|
pairedTokenAddress?: string;
|
|
229
230
|
/** Bankr Launch v3 options; `quoteAddress` picks the pool's quote token. */
|
|
230
231
|
launchV3?: {
|
|
@@ -243,7 +244,7 @@ export interface LaunchQuoteTokenOption {
|
|
|
243
244
|
deployField: LaunchQuoteTokenDeployField | null;
|
|
244
245
|
/** Stocks only: advisory thin-pool verdict, null when unmeasured. */
|
|
245
246
|
illiquid?: boolean | null;
|
|
246
|
-
/**
|
|
247
|
+
/** Doppler allowlist tokens only (pairedTokenAddress): reviewed-liquidity gate. */
|
|
247
248
|
readiness?: "live" | "pending" | "unknown";
|
|
248
249
|
}
|
|
249
250
|
export interface LaunchQuoteTokensResponse {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare function setConfigPath(path: string): void;
|
|
|
11
11
|
export declare function getConfigPath(): string;
|
|
12
12
|
export declare const DEFAULT_API_URL = "https://api.bankr.bot";
|
|
13
13
|
export declare const DEFAULT_LLM_URL = "https://llm.bankr.bot";
|
|
14
|
-
export declare const
|
|
14
|
+
export declare const CLI_VERSION: string;
|
|
15
|
+
export declare const CLI_USER_AGENT: string;
|
|
15
16
|
export declare function readConfig(): BankrConfig;
|
|
16
17
|
export declare function writeConfig(config: BankrConfig): void;
|
|
17
18
|
export declare function clearConfig(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
2
|
+
import { homedir, platform, release } from "os";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
4
|
let configPathOverride;
|
|
5
5
|
const DEFAULT_CONFIG_FILE = join(homedir(), ".bankr", "config.json");
|
|
@@ -11,7 +11,23 @@ export function getConfigPath() {
|
|
|
11
11
|
}
|
|
12
12
|
export const DEFAULT_API_URL = "https://api.bankr.bot";
|
|
13
13
|
export const DEFAULT_LLM_URL = "https://llm.bankr.bot";
|
|
14
|
-
export const
|
|
14
|
+
export const CLI_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")).version;
|
|
15
|
+
/** Browser-style platform token so the API's shared user-agent parser (which
|
|
16
|
+
* clamps to known families) names the OS on an out-of-band approval screen
|
|
17
|
+
* as "Bankr CLI · macOS". Never the hostname or username. */
|
|
18
|
+
function platformToken() {
|
|
19
|
+
switch (platform()) {
|
|
20
|
+
case "darwin":
|
|
21
|
+
return "Macintosh";
|
|
22
|
+
case "win32":
|
|
23
|
+
return "Windows NT";
|
|
24
|
+
case "linux":
|
|
25
|
+
return "Linux";
|
|
26
|
+
default:
|
|
27
|
+
return platform();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const CLI_USER_AGENT = `bankr-cli/${CLI_VERSION} (${platformToken()}; ${platform()} ${release()})`;
|
|
15
31
|
function ensureConfigDir() {
|
|
16
32
|
const configDir = dirname(getConfigPath());
|
|
17
33
|
if (!existsSync(configDir)) {
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare function brand(msg: string): void;
|
|
|
22
22
|
export declare function brandBold(msg: string): void;
|
|
23
23
|
export declare function success(msg: string): void;
|
|
24
24
|
export declare function error(msg: string): void;
|
|
25
|
+
/** Dim follow-up line for an error, on stderr so it stays with the headline. */
|
|
26
|
+
export declare function errorDetail(msg: string): void;
|
|
25
27
|
/** Collapses the error-then-exit epilogue every command repeats. */
|
|
26
28
|
export declare function fatal(message: string): never;
|
|
27
29
|
/** Same epilogue, for the spinner-driven variant that reports a caught error. */
|
package/dist/lib/output.js
CHANGED
|
@@ -40,6 +40,10 @@ export function success(msg) {
|
|
|
40
40
|
export function error(msg) {
|
|
41
41
|
console.error(chalk.red(`✗ ${msg}`));
|
|
42
42
|
}
|
|
43
|
+
/** Dim follow-up line for an error, on stderr so it stays with the headline. */
|
|
44
|
+
export function errorDetail(msg) {
|
|
45
|
+
console.error(chalk.dim(msg));
|
|
46
|
+
}
|
|
43
47
|
/** Collapses the error-then-exit epilogue every command repeats. */
|
|
44
48
|
export function fatal(message) {
|
|
45
49
|
error(message);
|