@alchemy/cli 0.5.1 → 0.5.2

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 CHANGED
@@ -39,26 +39,20 @@ alchemy completions fish > ~/.config/fish/completions/alchemy.fish
39
39
 
40
40
  ### Authentication Quick Start
41
41
 
42
- Authentication is required before making requests. Configure auth first, then run commands.
43
-
44
- If you are using the CLI as a human in an interactive terminal, the easiest path is:
42
+ Authentication is required before making requests. The recommended path is browser login:
45
43
 
46
44
  ```bash
47
- alchemy
45
+ alchemy auth
48
46
  ```
49
47
 
50
- Then follow the setup flow in the terminal UI to configure auth.
48
+ This opens a browser to link your Alchemy account, then prompts you to select an app. The selected app's API key is saved to your config automatically. Pass `-y` to skip the confirmation prompt.
51
49
 
52
- Know which auth method does what:
50
+ If you run `alchemy` with no command and no auth configured, the CLI will guide you through browser login automatically.
53
51
 
54
- - **API key** - direct auth for blockchain queries (`balance`, `tx`, `block`, `nfts`, `tokens`, `rpc`)
55
- - **Access key** - Admin/API app management; app setup/selection can also provide API key auth for blockchain queries
56
- - **x402 wallet auth** - wallet-authenticated, pay-per-request model for supported blockchain queries
52
+ If you have an auth token but haven't selected an app yet, the CLI will prompt you to pick one before running any command that requires an API key. Teams with many apps can type to search by name.
57
53
 
58
54
  If you use Notify webhooks, add webhook auth on top via `alchemy config set webhook-api-key <key>`, `--webhook-api-key`, or `ALCHEMY_WEBHOOK_API_KEY`.
59
55
 
60
- For setup commands, env vars, and resolution order, see [Authentication Reference](#authentication-reference).
61
-
62
56
  ### Usage By Workflow
63
57
 
64
58
  After auth is configured, use the CLI differently depending on who is driving it:
@@ -165,6 +159,9 @@ Use `alchemy help` or `alchemy help <command>` for generated command help.
165
159
  | Command | What it does | Example |
166
160
  |---|---|---|
167
161
  | `(no command)` | Starts interactive REPL mode (TTY only) | `alchemy` |
162
+ | `auth` (`auth login`) | Log in via browser (PKCE) | `alchemy auth` |
163
+ | `auth status` | Show current authentication status | `alchemy auth status` |
164
+ | `auth logout` | Clear saved authentication token | `alchemy auth logout` |
168
165
  | `apps list` | Lists apps (supports pagination/filtering) | `alchemy apps list --all` |
169
166
  | `apps chains` | Lists Admin API chain identifiers (e.g. `ETH_MAINNET`) | `alchemy apps chains` |
170
167
  | `apps get <id>` | Gets app details | `alchemy apps get <app-id>` |
@@ -231,6 +228,7 @@ Additional env vars:
231
228
 
232
229
  | Command | Flags |
233
230
  |---|---|
231
+ | `auth login` | `--force`, `-y, --yes` |
234
232
  | `nfts` | `--limit <n>`, `--page-key <key>` |
235
233
  | `nfts metadata` | `--contract <address>` (required), `--token-id <id>` (required) |
236
234
  | `tokens` | `--page-key <key>` |
@@ -258,74 +256,27 @@ Additional env vars:
258
256
 
259
257
  ## Authentication Reference
260
258
 
261
- The CLI supports three auth inputs:
262
-
263
- - API key for blockchain queries (`balance`, `tx`, `block`, `nfts`, `tokens`, `rpc`)
264
- - Access key for Admin API operations (`apps`, `chains`, configured network lookups`) and app setup/selection, which can also supply the API key used by blockchain query commands
265
- - x402 wallet key for wallet-authenticated blockchain queries in a pay-per-request model
266
-
267
- Notify/webhook commands use a webhook API key with resolution order:
268
- `--webhook-api-key` -> `ALCHEMY_WEBHOOK_API_KEY` -> `ALCHEMY_NOTIFY_AUTH_TOKEN` -> config `webhook-api-key` -> configured app webhook key.
269
-
270
- Get API/access keys at [alchemy.com](https://dashboard.alchemy.com/).
271
-
272
- #### API key
273
-
274
259
  ```bash
275
- # Config
276
- alchemy config set api-key <your-key>
260
+ # Interactive login — opens browser to link your Alchemy account
261
+ alchemy auth
277
262
 
278
- # Environment variable
279
- export ALCHEMY_API_KEY=<your-key>
280
-
281
- # Per-command override
282
- alchemy balance 0x... --api-key <your-key>
283
- ```
263
+ # Skip the confirmation prompt
264
+ alchemy auth -y
284
265
 
285
- Resolution order: `--api-key` -> `ALCHEMY_API_KEY` -> config file -> configured app API key.
286
-
287
- #### Access key
288
-
289
- ```bash
290
- # Config (in TTY, this may trigger app setup flow)
291
- alchemy config set access-key <your-key>
266
+ # Force re-authentication
267
+ alchemy auth login --force
292
268
 
293
- # Environment variable
294
- export ALCHEMY_ACCESS_KEY=<your-key>
269
+ # Check auth status
270
+ alchemy auth status
295
271
 
296
- # Per-command override
297
- alchemy apps list --access-key <your-key>
272
+ # Log out
273
+ alchemy auth logout
298
274
  ```
299
275
 
300
- Resolution order: `--access-key` -> `ALCHEMY_ACCESS_KEY` -> config file.
276
+ After login, the CLI prompts you to select an app. The app's API key is saved to config and used for all subsequent commands. If you skip app selection during login, the CLI will prompt you to pick one before running any command that needs an API key.
301
277
 
302
- #### x402 wallet auth
303
-
304
- x402 is a wallet-authenticated, pay-per-request usage model for supported blockchain queries.
305
- The CLI can generate or import the wallet key used for these requests.
306
-
307
- ```bash
308
- # Generate/import a wallet managed by CLI
309
- alchemy wallet generate
310
- # or
311
- alchemy wallet import ./private-key.txt
312
-
313
- # Use x402 per command
314
- alchemy balance 0x... --x402
315
-
316
- # Or enable by default
317
- alchemy config set x402 true
318
- ```
319
-
320
- Generated/imported wallets are stored as unique key files under `~/.config/alchemy/wallet-keys/` so creating another wallet does not overwrite prior private keys.
321
-
322
- You can also provide wallet key directly:
323
-
324
- ```bash
325
- export ALCHEMY_WALLET_KEY=0x...
326
- ```
327
-
328
- Wallet key resolution order: `--wallet-key-file` -> `ALCHEMY_WALLET_KEY` -> `wallet-key-file` in config.
278
+ Notify/webhook commands use a webhook API key with resolution order:
279
+ `--webhook-api-key` -> `ALCHEMY_WEBHOOK_API_KEY` -> `ALCHEMY_NOTIFY_AUTH_TOKEN` -> config `webhook-api-key` -> configured app webhook key.
329
280
 
330
281
  ## REPL Mode
331
282
 
@@ -9,7 +9,7 @@ import {
9
9
  performBrowserLogin,
10
10
  revokeToken,
11
11
  waitForCallback
12
- } from "./chunk-IGD4NIK7.js";
12
+ } from "./chunk-5ZAK2VSS.js";
13
13
  import "./chunk-56ZVYB4G.js";
14
14
  export {
15
15
  AUTH_PORT,
@@ -3,9 +3,12 @@ if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
3
  import {
4
4
  registerAuth,
5
5
  selectAppAfterAuth
6
- } from "./chunk-LYUW7O6X.js";
7
- import "./chunk-IGD4NIK7.js";
8
- import "./chunk-T2XSNZE3.js";
6
+ } from "./chunk-DBTRDS35.js";
7
+ import "./chunk-5ZAK2VSS.js";
8
+ import "./chunk-KDMIWPZH.js";
9
+ import "./chunk-NM25MEJZ.js";
10
+ import "./chunk-NBDWF4ZQ.js";
11
+ import "./chunk-BAAQ7ELR.js";
9
12
  import "./chunk-56ZVYB4G.js";
10
13
  export {
11
14
  registerAuth,
@@ -18,7 +18,7 @@ var SHARED_STYLE = `
18
18
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
19
19
  display: flex; justify-content: center; align-items: center;
20
20
  min-height: 100vh;
21
- background: #000;
21
+ background: linear-gradient(180deg, #4F46E5 0%, #06B6D4 100%);
22
22
  color: #fff;
23
23
  overflow: hidden;
24
24
  }
@@ -35,7 +35,7 @@ var SHARED_STYLE = `
35
35
  letter-spacing: -0.01em;
36
36
  }
37
37
  p {
38
- color: #6b6b6b;
38
+ color: #fff;
39
39
  font-size: 0.875rem;
40
40
  }
41
41
  `;
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
+ import {
4
+ isRevealMode
5
+ } from "./chunk-56ZVYB4G.js";
6
+
7
+ // src/lib/secrets.ts
8
+ function maskSecret(value) {
9
+ if (value.length <= 8) return "\u2022".repeat(value.length);
10
+ return value.slice(0, 4) + "\u2022".repeat(value.length - 8) + value.slice(-4);
11
+ }
12
+ function maskIf(value) {
13
+ return isRevealMode() ? value : maskSecret(value);
14
+ }
15
+
16
+ // src/lib/config.ts
17
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join, dirname } from "path";
20
+ import { z } from "zod";
21
+ var KEY_MAP = {
22
+ "api-key": "api_key",
23
+ api_key: "api_key",
24
+ "access-key": "access_key",
25
+ access_key: "access_key",
26
+ "webhook-api-key": "webhook_api_key",
27
+ webhook_api_key: "webhook_api_key",
28
+ network: "network",
29
+ verbose: "verbose",
30
+ "wallet-key-file": "wallet_key_file",
31
+ wallet_key_file: "wallet_key_file",
32
+ "wallet-address": "wallet_address",
33
+ wallet_address: "wallet_address",
34
+ x402: "x402",
35
+ "auth-token": "auth_token",
36
+ auth_token: "auth_token",
37
+ "auth-token-expires-at": "auth_token_expires_at",
38
+ auth_token_expires_at: "auth_token_expires_at"
39
+ };
40
+ var SAFE_ID_RE = /^[A-Za-z0-9:_-]{1,128}$/;
41
+ var SAFE_NETWORK_RE = /^[A-Za-z0-9:_-]{1,128}$/;
42
+ var MAX_SECRET_LEN = 512;
43
+ var MAX_APP_NAME_LEN = 128;
44
+ var CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
45
+ var safeTextSchema = (maxLen) => z.string().min(1).max(maxLen).refine((value) => !CONTROL_CHAR_RE.test(value));
46
+ var appConfigSchema = z.object({
47
+ id: z.string().regex(SAFE_ID_RE),
48
+ name: safeTextSchema(MAX_APP_NAME_LEN),
49
+ apiKey: safeTextSchema(MAX_SECRET_LEN),
50
+ webhookApiKey: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0)
51
+ }).strip();
52
+ var MAX_PATH_LEN = 4096;
53
+ var configSchema = z.object({
54
+ api_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
55
+ access_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
56
+ webhook_api_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
57
+ app: appConfigSchema.optional().catch(void 0),
58
+ network: z.string().regex(SAFE_NETWORK_RE).optional().catch(void 0),
59
+ verbose: z.boolean().optional().catch(void 0),
60
+ wallet_key_file: safeTextSchema(MAX_PATH_LEN).optional().catch(void 0),
61
+ wallet_address: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
62
+ x402: z.boolean().optional().catch(void 0),
63
+ auth_token: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
64
+ auth_token_expires_at: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
65
+ siwe_token: safeTextSchema(MAX_PATH_LEN).optional().catch(void 0),
66
+ siwe_token_expires_at: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0)
67
+ }).strip();
68
+ function sanitizeConfig(input) {
69
+ const parsed = configSchema.safeParse(input);
70
+ if (!parsed.success) {
71
+ return {};
72
+ }
73
+ return parsed.data;
74
+ }
75
+ function getHome() {
76
+ return process.env.HOME || homedir();
77
+ }
78
+ function configPath() {
79
+ if (process.env.ALCHEMY_CONFIG) return process.env.ALCHEMY_CONFIG;
80
+ const configHome = process.env.XDG_CONFIG_HOME || join(getHome(), ".config");
81
+ return join(configHome, "alchemy", "config.json");
82
+ }
83
+ function configDir() {
84
+ return dirname(configPath());
85
+ }
86
+ function load() {
87
+ const p = configPath();
88
+ if (!existsSync(p)) return {};
89
+ try {
90
+ const data = readFileSync(p, "utf-8");
91
+ return sanitizeConfig(JSON.parse(data));
92
+ } catch {
93
+ console.error(`warning: could not parse config file at ${p} \u2014 using defaults`);
94
+ return {};
95
+ }
96
+ }
97
+ function save(cfg) {
98
+ const p = configPath();
99
+ const sanitized = sanitizeConfig(cfg);
100
+ mkdirSync(dirname(p), { recursive: true, mode: 493 });
101
+ writeFileSync(p, JSON.stringify(sanitized, null, 2) + "\n", {
102
+ mode: 384
103
+ });
104
+ }
105
+ function get(cfg, key) {
106
+ if (key === "app") {
107
+ if (!cfg.app) return void 0;
108
+ return `${cfg.app.name} (${cfg.app.id})`;
109
+ }
110
+ const mapped = KEY_MAP[key];
111
+ if (!mapped) return void 0;
112
+ const value = cfg[mapped];
113
+ if (value === void 0) return void 0;
114
+ if (typeof value === "boolean") return String(value);
115
+ if (typeof value === "string") return value;
116
+ return void 0;
117
+ }
118
+ function toMap(cfg) {
119
+ const m = {};
120
+ if (cfg.api_key) m["api-key"] = maskIf(cfg.api_key);
121
+ if (cfg.access_key) m["access-key"] = maskIf(cfg.access_key);
122
+ if (cfg.webhook_api_key) m["webhook-api-key"] = maskIf(cfg.webhook_api_key);
123
+ if (cfg.app) m["app"] = `${cfg.app.name} (${cfg.app.id})`;
124
+ if (cfg.network) m["network"] = cfg.network;
125
+ if (cfg.verbose !== void 0) m["verbose"] = String(cfg.verbose);
126
+ if (cfg.wallet_key_file) m["wallet-key-file"] = cfg.wallet_key_file;
127
+ if (cfg.wallet_address) m["wallet-address"] = cfg.wallet_address;
128
+ if (cfg.x402 !== void 0) m["x402"] = String(cfg.x402);
129
+ if (cfg.auth_token) m["auth-token"] = maskIf(cfg.auth_token);
130
+ if (cfg.auth_token_expires_at) m["auth-token-expires-at"] = cfg.auth_token_expires_at;
131
+ return m;
132
+ }
133
+
134
+ export {
135
+ maskIf,
136
+ KEY_MAP,
137
+ configPath,
138
+ configDir,
139
+ load,
140
+ save,
141
+ get,
142
+ toMap
143
+ };
@@ -5,22 +5,29 @@ import {
5
5
  getLoginUrl,
6
6
  performBrowserLogin,
7
7
  revokeToken
8
- } from "./chunk-IGD4NIK7.js";
8
+ } from "./chunk-5ZAK2VSS.js";
9
+ import {
10
+ isInteractiveAllowed
11
+ } from "./chunk-KDMIWPZH.js";
9
12
  import {
10
13
  AdminClient,
14
+ resolveAuthToken
15
+ } from "./chunk-NM25MEJZ.js";
16
+ import {
11
17
  bold,
12
18
  brand,
13
- configPath,
14
19
  dim,
15
20
  green,
16
- isInteractiveAllowed,
21
+ promptAutocomplete,
22
+ promptText,
23
+ withSpinner
24
+ } from "./chunk-NBDWF4ZQ.js";
25
+ import {
26
+ configPath,
17
27
  load,
18
28
  maskIf,
19
- promptSelect,
20
- resolveAuthToken,
21
- save,
22
- withSpinner
23
- } from "./chunk-T2XSNZE3.js";
29
+ save
30
+ } from "./chunk-BAAQ7ELR.js";
24
31
  import {
25
32
  CLIError,
26
33
  ErrorCode,
@@ -32,8 +39,9 @@ import {
32
39
 
33
40
  // src/commands/auth.ts
34
41
  function registerAuth(program) {
35
- const cmd = program.command("auth").description("Authenticate with your Alchemy account");
36
- cmd.command("login", { isDefault: true }).description("Log in via browser").option("--force", "Force re-authentication even if a valid token exists").action(async (opts) => {
42
+ const cmd = program.command("auth").description("Authenticate with your Alchemy account").option("-y, --yes", "Skip confirmation prompt and open browser immediately");
43
+ cmd.command("login", { isDefault: true }).description("Log in via browser").option("--force", "Force re-authentication even if a valid token exists").option("-y, --yes", "Skip confirmation prompt and open browser immediately").action(async (opts) => {
44
+ const yes = opts.yes || cmd.opts().yes;
37
45
  try {
38
46
  if (!opts.force) {
39
47
  const existing = resolveAuthToken();
@@ -60,9 +68,18 @@ function registerAuth(program) {
60
68
  console.log(` ${brand("\u25C6")} ${bold("Alchemy Authentication")}`);
61
69
  console.log(` ${dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
62
70
  console.log("");
63
- console.log(` Opening browser to log in...`);
64
71
  console.log(` ${dim(getLoginUrl(AUTH_PORT))}`);
65
72
  console.log("");
73
+ }
74
+ if (!yes && !isJSONMode() && isInteractiveAllowed(program)) {
75
+ const answer = await promptText({
76
+ message: "Press Enter to open browser and link your Alchemy account",
77
+ cancelMessage: "Login cancelled."
78
+ });
79
+ if (answer === null) return;
80
+ }
81
+ if (!isJSONMode()) {
82
+ console.log(` Opening browser to log in...`);
66
83
  console.log(` ${dim("Waiting for authentication...")}`);
67
84
  }
68
85
  const result = await performBrowserLogin();
@@ -196,8 +213,9 @@ async function selectAppAfterAuth(authToken) {
196
213
  console.log(` ${green("\u2713")} Auto-selected app: ${bold(selectedApp.name)}`);
197
214
  } else {
198
215
  console.log("");
199
- const appId = await promptSelect({
216
+ const appId = await promptAutocomplete({
200
217
  message: "Select an app",
218
+ placeholder: "Type to search by name",
201
219
  options: apps.map((app) => ({
202
220
  value: app.id,
203
221
  label: app.name,
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
3
  import {
4
- isInteractiveAllowed,
4
+ isInteractiveAllowed
5
+ } from "./chunk-KDMIWPZH.js";
6
+ import {
5
7
  resolveAuthToken
6
- } from "./chunk-T2XSNZE3.js";
8
+ } from "./chunk-NM25MEJZ.js";
7
9
  import {
8
10
  getBaseDomain
9
11
  } from "./chunk-56ZVYB4G.js";
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
+ import {
4
+ isJSONMode
5
+ } from "./chunk-56ZVYB4G.js";
6
+
7
+ // src/lib/interaction.ts
8
+ import { stdin, stdout } from "process";
9
+ function isTruthy(value) {
10
+ if (!value) return false;
11
+ const normalized = value.trim().toLowerCase();
12
+ return normalized === "1" || normalized === "true" || normalized === "yes";
13
+ }
14
+ function isNonInteractiveEnv() {
15
+ return isTruthy(process.env.ALCHEMY_NON_INTERACTIVE);
16
+ }
17
+ function isInteractiveAllowed(program) {
18
+ if (!stdin.isTTY || !stdout.isTTY) return false;
19
+ if (isJSONMode()) return false;
20
+ if (isNonInteractiveEnv()) return false;
21
+ if (program && program.opts().interactive === false) return false;
22
+ return true;
23
+ }
24
+
25
+ export {
26
+ isInteractiveAllowed
27
+ };