@alchemy/cli 0.3.1 → 0.5.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/dist/auth-7E33EMAI.js +13 -0
- package/dist/auth-E26YCAJV.js +23 -0
- package/dist/chunk-44OGGLN4.js +681 -0
- package/dist/chunk-56ZVYB4G.js +536 -0
- package/dist/{chunk-QDDJ3OYO.js → chunk-5X6YRTPU.js} +15 -5
- package/dist/chunk-DUQFOLLZ.js +118 -0
- package/dist/chunk-IGD4NIK7.js +300 -0
- package/dist/chunk-LYUW7O6X.js +231 -0
- package/dist/chunk-T2XSNZE3.js +1398 -0
- package/dist/index.js +417 -204
- package/dist/{interactive-CLPT5QDZ.js → interactive-K7XOS6U6.js} +12 -7
- package/dist/{onboarding-XNAWN5BR.js → onboarding-F5PZMFZU.js} +57 -15
- package/package.json +1 -1
- package/dist/chunk-6XTLILDF.js +0 -1246
- package/dist/chunk-J6RZM4CJ.js +0 -1333
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
|
|
3
|
+
import {
|
|
4
|
+
CLIError,
|
|
5
|
+
ErrorCode,
|
|
6
|
+
debug,
|
|
7
|
+
errAccessDenied,
|
|
8
|
+
errAccessKeyRequired,
|
|
9
|
+
errAdminAPI,
|
|
10
|
+
errAuthRequired,
|
|
11
|
+
errInvalidAPIKey,
|
|
12
|
+
errInvalidAccessKey,
|
|
13
|
+
errInvalidArgs,
|
|
14
|
+
errNetwork,
|
|
15
|
+
errNetworkNotEnabled,
|
|
16
|
+
errNotFound,
|
|
17
|
+
errRPC,
|
|
18
|
+
errRateLimited,
|
|
19
|
+
errWalletKeyRequired,
|
|
20
|
+
esc,
|
|
21
|
+
fetchWithTimeout,
|
|
22
|
+
getBaseDomain,
|
|
23
|
+
isJSONMode,
|
|
24
|
+
isLocalhost,
|
|
25
|
+
isRevealMode,
|
|
26
|
+
parseBaseURLOverride,
|
|
27
|
+
quiet,
|
|
28
|
+
redactSensitiveText,
|
|
29
|
+
rgb,
|
|
30
|
+
verbose
|
|
31
|
+
} from "./chunk-56ZVYB4G.js";
|
|
32
|
+
|
|
33
|
+
// src/lib/secrets.ts
|
|
34
|
+
function maskSecret(value) {
|
|
35
|
+
if (value.length <= 8) return "\u2022".repeat(value.length);
|
|
36
|
+
return value.slice(0, 4) + "\u2022".repeat(value.length - 8) + value.slice(-4);
|
|
37
|
+
}
|
|
38
|
+
function maskIf(value) {
|
|
39
|
+
return isRevealMode() ? value : maskSecret(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/lib/config.ts
|
|
43
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
44
|
+
import { homedir } from "os";
|
|
45
|
+
import { join, dirname } from "path";
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
var KEY_MAP = {
|
|
48
|
+
"api-key": "api_key",
|
|
49
|
+
api_key: "api_key",
|
|
50
|
+
"access-key": "access_key",
|
|
51
|
+
access_key: "access_key",
|
|
52
|
+
"webhook-api-key": "webhook_api_key",
|
|
53
|
+
webhook_api_key: "webhook_api_key",
|
|
54
|
+
network: "network",
|
|
55
|
+
verbose: "verbose",
|
|
56
|
+
"wallet-key-file": "wallet_key_file",
|
|
57
|
+
wallet_key_file: "wallet_key_file",
|
|
58
|
+
"wallet-address": "wallet_address",
|
|
59
|
+
wallet_address: "wallet_address",
|
|
60
|
+
x402: "x402",
|
|
61
|
+
"auth-token": "auth_token",
|
|
62
|
+
auth_token: "auth_token",
|
|
63
|
+
"auth-token-expires-at": "auth_token_expires_at",
|
|
64
|
+
auth_token_expires_at: "auth_token_expires_at"
|
|
65
|
+
};
|
|
66
|
+
var SAFE_ID_RE = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
67
|
+
var SAFE_NETWORK_RE = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
68
|
+
var MAX_SECRET_LEN = 512;
|
|
69
|
+
var MAX_APP_NAME_LEN = 128;
|
|
70
|
+
var CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
|
|
71
|
+
var safeTextSchema = (maxLen) => z.string().min(1).max(maxLen).refine((value) => !CONTROL_CHAR_RE.test(value));
|
|
72
|
+
var appConfigSchema = z.object({
|
|
73
|
+
id: z.string().regex(SAFE_ID_RE),
|
|
74
|
+
name: safeTextSchema(MAX_APP_NAME_LEN),
|
|
75
|
+
apiKey: safeTextSchema(MAX_SECRET_LEN),
|
|
76
|
+
webhookApiKey: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0)
|
|
77
|
+
}).strip();
|
|
78
|
+
var MAX_PATH_LEN = 4096;
|
|
79
|
+
var configSchema = z.object({
|
|
80
|
+
api_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
81
|
+
access_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
82
|
+
webhook_api_key: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
83
|
+
app: appConfigSchema.optional().catch(void 0),
|
|
84
|
+
network: z.string().regex(SAFE_NETWORK_RE).optional().catch(void 0),
|
|
85
|
+
verbose: z.boolean().optional().catch(void 0),
|
|
86
|
+
wallet_key_file: safeTextSchema(MAX_PATH_LEN).optional().catch(void 0),
|
|
87
|
+
wallet_address: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
88
|
+
x402: z.boolean().optional().catch(void 0),
|
|
89
|
+
auth_token: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
90
|
+
auth_token_expires_at: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0),
|
|
91
|
+
siwe_token: safeTextSchema(MAX_PATH_LEN).optional().catch(void 0),
|
|
92
|
+
siwe_token_expires_at: safeTextSchema(MAX_SECRET_LEN).optional().catch(void 0)
|
|
93
|
+
}).strip();
|
|
94
|
+
function sanitizeConfig(input) {
|
|
95
|
+
const parsed = configSchema.safeParse(input);
|
|
96
|
+
if (!parsed.success) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
return parsed.data;
|
|
100
|
+
}
|
|
101
|
+
function getHome() {
|
|
102
|
+
return process.env.HOME || homedir();
|
|
103
|
+
}
|
|
104
|
+
function configPath() {
|
|
105
|
+
if (process.env.ALCHEMY_CONFIG) return process.env.ALCHEMY_CONFIG;
|
|
106
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(getHome(), ".config");
|
|
107
|
+
return join(configHome, "alchemy", "config.json");
|
|
108
|
+
}
|
|
109
|
+
function configDir() {
|
|
110
|
+
return dirname(configPath());
|
|
111
|
+
}
|
|
112
|
+
function load() {
|
|
113
|
+
const p = configPath();
|
|
114
|
+
if (!existsSync(p)) return {};
|
|
115
|
+
try {
|
|
116
|
+
const data = readFileSync(p, "utf-8");
|
|
117
|
+
return sanitizeConfig(JSON.parse(data));
|
|
118
|
+
} catch {
|
|
119
|
+
console.error(`warning: could not parse config file at ${p} \u2014 using defaults`);
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function save(cfg) {
|
|
124
|
+
const p = configPath();
|
|
125
|
+
const sanitized = sanitizeConfig(cfg);
|
|
126
|
+
mkdirSync(dirname(p), { recursive: true, mode: 493 });
|
|
127
|
+
writeFileSync(p, JSON.stringify(sanitized, null, 2) + "\n", {
|
|
128
|
+
mode: 384
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function get(cfg, key) {
|
|
132
|
+
if (key === "app") {
|
|
133
|
+
if (!cfg.app) return void 0;
|
|
134
|
+
return `${cfg.app.name} (${cfg.app.id})`;
|
|
135
|
+
}
|
|
136
|
+
const mapped = KEY_MAP[key];
|
|
137
|
+
if (!mapped) return void 0;
|
|
138
|
+
const value = cfg[mapped];
|
|
139
|
+
if (value === void 0) return void 0;
|
|
140
|
+
if (typeof value === "boolean") return String(value);
|
|
141
|
+
if (typeof value === "string") return value;
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
function toMap(cfg) {
|
|
145
|
+
const m = {};
|
|
146
|
+
if (cfg.api_key) m["api-key"] = maskIf(cfg.api_key);
|
|
147
|
+
if (cfg.access_key) m["access-key"] = maskIf(cfg.access_key);
|
|
148
|
+
if (cfg.webhook_api_key) m["webhook-api-key"] = maskIf(cfg.webhook_api_key);
|
|
149
|
+
if (cfg.app) m["app"] = `${cfg.app.name} (${cfg.app.id})`;
|
|
150
|
+
if (cfg.network) m["network"] = cfg.network;
|
|
151
|
+
if (cfg.verbose !== void 0) m["verbose"] = String(cfg.verbose);
|
|
152
|
+
if (cfg.wallet_key_file) m["wallet-key-file"] = cfg.wallet_key_file;
|
|
153
|
+
if (cfg.wallet_address) m["wallet-address"] = cfg.wallet_address;
|
|
154
|
+
if (cfg.x402 !== void 0) m["x402"] = String(cfg.x402);
|
|
155
|
+
if (cfg.auth_token) m["auth-token"] = maskIf(cfg.auth_token);
|
|
156
|
+
if (cfg.auth_token_expires_at) m["auth-token-expires-at"] = cfg.auth_token_expires_at;
|
|
157
|
+
return m;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/lib/terminal-ui.ts
|
|
161
|
+
import * as readline from "readline";
|
|
162
|
+
import { stdin, stdout } from "process";
|
|
163
|
+
var ansi = {
|
|
164
|
+
cyan: esc("36"),
|
|
165
|
+
dim: esc("2"),
|
|
166
|
+
green: esc("32"),
|
|
167
|
+
red: esc("31"),
|
|
168
|
+
purple: rgb(180, 160, 255)
|
|
169
|
+
};
|
|
170
|
+
var FLOW_PIPE = "\u2502";
|
|
171
|
+
function optionLabel(option) {
|
|
172
|
+
return option.label ?? String(option.value);
|
|
173
|
+
}
|
|
174
|
+
function printCancel(message) {
|
|
175
|
+
if (!message) return;
|
|
176
|
+
console.log(` ${ansi.dim(FLOW_PIPE)}`);
|
|
177
|
+
console.log(` ${ansi.dim(message)}`);
|
|
178
|
+
}
|
|
179
|
+
function clearRenderedLines(lines) {
|
|
180
|
+
for (let i = 0; i < lines; i += 1) {
|
|
181
|
+
readline.clearLine(stdout, 0);
|
|
182
|
+
readline.cursorTo(stdout, 0);
|
|
183
|
+
if (i < lines - 1) {
|
|
184
|
+
readline.moveCursor(stdout, 0, -1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function suspendStdinKeypressListeners() {
|
|
189
|
+
const listeners = stdin.listeners("keypress");
|
|
190
|
+
for (const listener of listeners) {
|
|
191
|
+
stdin.removeListener("keypress", listener);
|
|
192
|
+
}
|
|
193
|
+
return () => {
|
|
194
|
+
for (const listener of listeners) {
|
|
195
|
+
stdin.on("keypress", listener);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function runListPrompt(opts) {
|
|
200
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
201
|
+
const initial = opts.initialValue ?? opts.options.find((o) => !o.disabled)?.value ?? null;
|
|
202
|
+
return { value: initial, cancelled: false };
|
|
203
|
+
}
|
|
204
|
+
readline.emitKeypressEvents(stdin);
|
|
205
|
+
const restoreKeypressListeners = suspendStdinKeypressListeners();
|
|
206
|
+
const previousRawMode = stdin.isRaw;
|
|
207
|
+
stdin.resume();
|
|
208
|
+
stdin.setRawMode(true);
|
|
209
|
+
let query = "";
|
|
210
|
+
let cursor = Math.max(
|
|
211
|
+
0,
|
|
212
|
+
opts.options.findIndex((o) => o.value === opts.initialValue && !o.disabled)
|
|
213
|
+
);
|
|
214
|
+
const selected = /* @__PURE__ */ new Set();
|
|
215
|
+
const maxVisible = 8;
|
|
216
|
+
let renderedLines = 0;
|
|
217
|
+
const getFiltered = () => {
|
|
218
|
+
if (!opts.filterable || !query.trim()) return opts.options;
|
|
219
|
+
const q = query.toLowerCase();
|
|
220
|
+
return opts.options.filter((option) => {
|
|
221
|
+
const label = optionLabel(option).toLowerCase();
|
|
222
|
+
const hint = (option.hint ?? "").toLowerCase();
|
|
223
|
+
const value = String(option.value).toLowerCase();
|
|
224
|
+
return label.includes(q) || hint.includes(q) || value.includes(q);
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
const normalizeCursor = (filtered) => {
|
|
228
|
+
if (filtered.length === 0) {
|
|
229
|
+
cursor = 0;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (cursor >= filtered.length) cursor = filtered.length - 1;
|
|
233
|
+
if (cursor < 0) cursor = 0;
|
|
234
|
+
if (filtered[cursor]?.disabled) {
|
|
235
|
+
const next = filtered.findIndex((o) => !o.disabled);
|
|
236
|
+
cursor = next >= 0 ? next : 0;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const render = () => {
|
|
240
|
+
const filtered = getFiltered();
|
|
241
|
+
normalizeCursor(filtered);
|
|
242
|
+
if (renderedLines > 0) clearRenderedLines(renderedLines);
|
|
243
|
+
const lines = [];
|
|
244
|
+
const suffix = opts.filterable && query ? ` ${ansi.dim(`(${query})`)}` : "";
|
|
245
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)}`);
|
|
246
|
+
lines.push(` ${ansi.cyan("\u25C6")} ${opts.message}${suffix}`);
|
|
247
|
+
if (filtered.length === 0) {
|
|
248
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${ansi.dim("No matches found")}`);
|
|
249
|
+
} else {
|
|
250
|
+
const start = Math.max(0, Math.min(cursor - 3, Math.max(0, filtered.length - maxVisible)));
|
|
251
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
252
|
+
for (let i = 0; i < visible.length; i += 1) {
|
|
253
|
+
const option = visible[i];
|
|
254
|
+
const active = start + i === cursor;
|
|
255
|
+
const disabled = option.disabled === true;
|
|
256
|
+
const selectedMark = opts.allowMultiple ? selected.has(option.value) ? ansi.green("\u25C6") : ansi.dim("\u25C7") : active ? ansi.cyan("\u25C6") : ansi.dim("\u25C7");
|
|
257
|
+
const label = optionLabel(option);
|
|
258
|
+
const value = disabled ? ansi.dim(label) : label;
|
|
259
|
+
const hint = option.hint ? ` ${ansi.dim(`\u2014 ${option.hint}`)}` : "";
|
|
260
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${selectedMark} ${value}${hint}`);
|
|
261
|
+
}
|
|
262
|
+
if (filtered.length > maxVisible) {
|
|
263
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${ansi.dim(`${filtered.length} options`)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (opts.filterable && query.length === 0 && opts.placeholder) {
|
|
267
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${ansi.dim(opts.placeholder)}`);
|
|
268
|
+
} else if (opts.allowMultiple) {
|
|
269
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${ansi.dim("Space to toggle, Enter to confirm")}`);
|
|
270
|
+
} else {
|
|
271
|
+
lines.push(` ${ansi.dim(FLOW_PIPE)} ${ansi.dim("Use arrows and press Enter")}`);
|
|
272
|
+
}
|
|
273
|
+
stdout.write(lines.join("\n"));
|
|
274
|
+
renderedLines = lines.length;
|
|
275
|
+
};
|
|
276
|
+
const cleanup = () => {
|
|
277
|
+
if (renderedLines > 0) clearRenderedLines(renderedLines);
|
|
278
|
+
stdin.setRawMode(previousRawMode);
|
|
279
|
+
stdin.removeListener("keypress", onKeypress);
|
|
280
|
+
restoreKeypressListeners();
|
|
281
|
+
if (!previousRawMode) {
|
|
282
|
+
stdin.pause();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const commitSingleLine = (text) => {
|
|
286
|
+
if (opts.commitLabel === null) return;
|
|
287
|
+
const label = opts.commitLabel ?? opts.message;
|
|
288
|
+
console.log(` ${ansi.green("\u25C6")} ${label}: ${text}`);
|
|
289
|
+
};
|
|
290
|
+
const onKeypress = (str, key) => {
|
|
291
|
+
const filtered = getFiltered();
|
|
292
|
+
const current = filtered[cursor];
|
|
293
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
294
|
+
cleanup();
|
|
295
|
+
resolver({ value: null, cancelled: true });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (key.name === "return") {
|
|
299
|
+
if (opts.allowMultiple) {
|
|
300
|
+
if (opts.required && selected.size === 0) return;
|
|
301
|
+
cleanup();
|
|
302
|
+
const values = Array.from(selected);
|
|
303
|
+
const labels = opts.options.filter((o) => values.includes(o.value)).map((o) => optionLabel(o)).join(", ");
|
|
304
|
+
commitSingleLine(labels || "none");
|
|
305
|
+
resolver({ value: values, cancelled: false });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!current || current.disabled) return;
|
|
309
|
+
cleanup();
|
|
310
|
+
commitSingleLine(optionLabel(current));
|
|
311
|
+
resolver({ value: current.value, cancelled: false });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (opts.allowMultiple && key.name === "space") {
|
|
315
|
+
if (!current || current.disabled) return;
|
|
316
|
+
if (selected.has(current.value)) selected.delete(current.value);
|
|
317
|
+
else selected.add(current.value);
|
|
318
|
+
render();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (key.name === "up") {
|
|
322
|
+
if (filtered.length === 0) return;
|
|
323
|
+
let next = cursor - 1;
|
|
324
|
+
while (next >= 0 && filtered[next]?.disabled) next -= 1;
|
|
325
|
+
if (next >= 0) cursor = next;
|
|
326
|
+
render();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (key.name === "down") {
|
|
330
|
+
if (filtered.length === 0) return;
|
|
331
|
+
let next = cursor + 1;
|
|
332
|
+
while (next < filtered.length && filtered[next]?.disabled) next += 1;
|
|
333
|
+
if (next < filtered.length) cursor = next;
|
|
334
|
+
render();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (opts.filterable && key.name === "backspace") {
|
|
338
|
+
if (query.length > 0) {
|
|
339
|
+
query = query.slice(0, -1);
|
|
340
|
+
render();
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (opts.filterable && str && !key.ctrl && !key.meta && str >= " " && str !== "\x7F") {
|
|
345
|
+
query += str;
|
|
346
|
+
cursor = 0;
|
|
347
|
+
render();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
let resolver;
|
|
351
|
+
const done = new Promise((resolve) => {
|
|
352
|
+
resolver = resolve;
|
|
353
|
+
});
|
|
354
|
+
stdin.on("keypress", onKeypress);
|
|
355
|
+
render();
|
|
356
|
+
return done;
|
|
357
|
+
}
|
|
358
|
+
async function promptText(opts) {
|
|
359
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
360
|
+
return opts.defaultValue ?? opts.initialValue ?? "";
|
|
361
|
+
}
|
|
362
|
+
stdin.resume();
|
|
363
|
+
stdin.ref?.();
|
|
364
|
+
const restoreKeypressListeners = suspendStdinKeypressListeners();
|
|
365
|
+
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
366
|
+
console.log(` ${ansi.dim(FLOW_PIPE)}`);
|
|
367
|
+
const question = ` ${ansi.cyan("\u25C6")} ${opts.message}${opts.placeholder ? ` ${ansi.dim(`(${opts.placeholder})`)}` : ""}: `;
|
|
368
|
+
const previousRawMode = stdin.isRaw;
|
|
369
|
+
if (previousRawMode) stdin.setRawMode(false);
|
|
370
|
+
const value = await new Promise((resolve) => {
|
|
371
|
+
rl.on("SIGINT", () => resolve(null));
|
|
372
|
+
rl.question(question, (answer) => resolve(answer));
|
|
373
|
+
});
|
|
374
|
+
rl.close();
|
|
375
|
+
restoreKeypressListeners();
|
|
376
|
+
if (previousRawMode) stdin.setRawMode(true);
|
|
377
|
+
if (opts.clearAfterSubmit) {
|
|
378
|
+
clearRenderedLines(2);
|
|
379
|
+
}
|
|
380
|
+
if (value === null) {
|
|
381
|
+
printCancel(opts.cancelMessage);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
if (!value.trim() && opts.defaultValue !== void 0) return opts.defaultValue;
|
|
385
|
+
if (!value.trim() && opts.initialValue !== void 0) return opts.initialValue;
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
async function promptConfirm(opts) {
|
|
389
|
+
const defaultYes = opts.initialValue ?? true;
|
|
390
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
391
|
+
const answer = await promptText({
|
|
392
|
+
message: `${opts.message} ${suffix}`,
|
|
393
|
+
cancelMessage: opts.cancelMessage
|
|
394
|
+
});
|
|
395
|
+
if (answer === null) return null;
|
|
396
|
+
const normalized = answer.trim().toLowerCase();
|
|
397
|
+
if (!normalized) return defaultYes;
|
|
398
|
+
if (normalized === "y" || normalized === "yes") return true;
|
|
399
|
+
if (normalized === "n" || normalized === "no") return false;
|
|
400
|
+
return defaultYes;
|
|
401
|
+
}
|
|
402
|
+
async function promptSelect(opts) {
|
|
403
|
+
const result = await runListPrompt({
|
|
404
|
+
message: opts.message,
|
|
405
|
+
options: opts.options,
|
|
406
|
+
initialValue: opts.initialValue,
|
|
407
|
+
commitLabel: opts.commitLabel ?? "Selected"
|
|
408
|
+
});
|
|
409
|
+
if (result.cancelled) {
|
|
410
|
+
printCancel(opts.cancelMessage);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return result.value;
|
|
414
|
+
}
|
|
415
|
+
async function promptAutocomplete(opts) {
|
|
416
|
+
const result = await runListPrompt({
|
|
417
|
+
message: opts.message,
|
|
418
|
+
options: opts.options,
|
|
419
|
+
initialValue: opts.initialValue,
|
|
420
|
+
placeholder: opts.placeholder,
|
|
421
|
+
filterable: true,
|
|
422
|
+
commitLabel: opts.commitLabel ?? "Selected"
|
|
423
|
+
});
|
|
424
|
+
if (result.cancelled) {
|
|
425
|
+
printCancel(opts.cancelMessage);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return result.value;
|
|
429
|
+
}
|
|
430
|
+
async function promptMultiselect(opts) {
|
|
431
|
+
const result = await runListPrompt({
|
|
432
|
+
message: opts.message,
|
|
433
|
+
options: opts.options,
|
|
434
|
+
allowMultiple: true,
|
|
435
|
+
required: opts.required,
|
|
436
|
+
commitLabel: "Selected"
|
|
437
|
+
});
|
|
438
|
+
if (result.cancelled) {
|
|
439
|
+
printCancel(opts.cancelMessage);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return result.value;
|
|
443
|
+
}
|
|
444
|
+
async function runWithSpinner(label, doneLabel, fn) {
|
|
445
|
+
if (!stdout.isTTY) return fn();
|
|
446
|
+
const spinFrames = ["\u2B16", "\u2B18", "\u2B17", "\u2B19"];
|
|
447
|
+
let tick = 0;
|
|
448
|
+
const render = () => {
|
|
449
|
+
readline.clearLine(stdout, 0);
|
|
450
|
+
readline.cursorTo(stdout, 0);
|
|
451
|
+
const spin = spinFrames[tick % spinFrames.length];
|
|
452
|
+
stdout.write(` ${ansi.dim(FLOW_PIPE)} ${ansi.purple(spin)} ${label}`);
|
|
453
|
+
tick += 1;
|
|
454
|
+
};
|
|
455
|
+
render();
|
|
456
|
+
const timer = setInterval(render, 160);
|
|
457
|
+
try {
|
|
458
|
+
const result = await fn();
|
|
459
|
+
clearInterval(timer);
|
|
460
|
+
readline.clearLine(stdout, 0);
|
|
461
|
+
readline.cursorTo(stdout, 0);
|
|
462
|
+
stdout.write(` ${ansi.green("\u25C6")} ${doneLabel}
|
|
463
|
+
`);
|
|
464
|
+
return result;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
clearInterval(timer);
|
|
467
|
+
readline.clearLine(stdout, 0);
|
|
468
|
+
readline.cursorTo(stdout, 0);
|
|
469
|
+
stdout.write(` ${ansi.red("\u2717")} ${label}
|
|
470
|
+
`);
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/lib/ui.ts
|
|
476
|
+
import Table from "cli-table3";
|
|
477
|
+
var ansi2 = {
|
|
478
|
+
green: esc("32"),
|
|
479
|
+
red: esc("31"),
|
|
480
|
+
dim: esc("2"),
|
|
481
|
+
cyan: esc("36"),
|
|
482
|
+
bold: esc("1"),
|
|
483
|
+
yellow: esc("33"),
|
|
484
|
+
// Alchemy brand colors
|
|
485
|
+
brand: rgb(54, 63, 249),
|
|
486
|
+
// Primary #363FF9
|
|
487
|
+
brandSecondary: rgb(139, 92, 246)
|
|
488
|
+
// Secondary #8B5CF6
|
|
489
|
+
};
|
|
490
|
+
var stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
491
|
+
function wrap(fn) {
|
|
492
|
+
return (s) => isJSONMode() ? s : fn(s);
|
|
493
|
+
}
|
|
494
|
+
var green = wrap(ansi2.green);
|
|
495
|
+
var red = wrap(ansi2.red);
|
|
496
|
+
var dim = wrap(ansi2.dim);
|
|
497
|
+
var cyan = wrap(ansi2.cyan);
|
|
498
|
+
var bold = wrap(ansi2.bold);
|
|
499
|
+
var yellow = wrap(ansi2.yellow);
|
|
500
|
+
var brand = wrap(ansi2.brand);
|
|
501
|
+
var suppressBrandedHelp = false;
|
|
502
|
+
function setBrandedHelpSuppressed(suppressed) {
|
|
503
|
+
suppressBrandedHelp = suppressed;
|
|
504
|
+
}
|
|
505
|
+
function successBadge() {
|
|
506
|
+
return isJSONMode() ? "Success" : ansi2.green("\u2713");
|
|
507
|
+
}
|
|
508
|
+
function failBadge() {
|
|
509
|
+
return isJSONMode() ? "Failed" : ansi2.red("\u2717");
|
|
510
|
+
}
|
|
511
|
+
async function withSpinner(label, doneLabel, fn) {
|
|
512
|
+
if (isJSONMode() || quiet) return fn();
|
|
513
|
+
return runWithSpinner(label, doneLabel, fn);
|
|
514
|
+
}
|
|
515
|
+
function printKeyValueBox(pairs) {
|
|
516
|
+
if (isJSONMode()) return;
|
|
517
|
+
if (pairs.length === 0) {
|
|
518
|
+
console.log(` ${ansi2.brand("\u250C\u2500\u2500\u2510")}`);
|
|
519
|
+
console.log(` ${ansi2.brand("\u2514\u2500\u2500\u2518")}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const keyWidth = Math.max(...pairs.map(([k]) => stripAnsi(k).length));
|
|
523
|
+
const contentRows = pairs.map(([key, value]) => {
|
|
524
|
+
const visibleKeyLen = stripAnsi(key).length;
|
|
525
|
+
const paddedKey = key + " ".repeat(Math.max(0, keyWidth - visibleKeyLen));
|
|
526
|
+
return `${ansi2.dim(paddedKey)} ${value}`;
|
|
527
|
+
});
|
|
528
|
+
const contentWidth = Math.max(...contentRows.map((row) => stripAnsi(row).length));
|
|
529
|
+
const top = `\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`;
|
|
530
|
+
const bottom = `\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`;
|
|
531
|
+
console.log(` ${ansi2.dim(top)}`);
|
|
532
|
+
for (const row of contentRows) {
|
|
533
|
+
const visibleLen = stripAnsi(row).length;
|
|
534
|
+
const padded = row + " ".repeat(Math.max(0, contentWidth - visibleLen));
|
|
535
|
+
console.log(` ${ansi2.dim("\u2502")} ${padded} ${ansi2.dim("\u2502")}`);
|
|
536
|
+
}
|
|
537
|
+
console.log(` ${ansi2.dim(bottom)}`);
|
|
538
|
+
}
|
|
539
|
+
function emptyState(message) {
|
|
540
|
+
if (isJSONMode()) return;
|
|
541
|
+
console.log(`
|
|
542
|
+
${ansi2.dim(`\u25CB ${message}`)}`);
|
|
543
|
+
}
|
|
544
|
+
function printSyntaxJSON(obj) {
|
|
545
|
+
if (isJSONMode()) {
|
|
546
|
+
console.log(JSON.stringify(obj));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
550
|
+
const highlighted = raw.replace(
|
|
551
|
+
/("(?:\\.|[^"\\])*")\s*(:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)/g,
|
|
552
|
+
(match, str, colon, num, lit) => {
|
|
553
|
+
if (str && colon) return ansi2.brand(str) + colon;
|
|
554
|
+
if (str) return ansi2.green(str);
|
|
555
|
+
if (num) return ansi2.cyan(num);
|
|
556
|
+
if (lit) return ansi2.yellow(lit);
|
|
557
|
+
return match;
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
console.log(highlighted);
|
|
561
|
+
}
|
|
562
|
+
function printTable(headers, rows) {
|
|
563
|
+
if (isJSONMode()) {
|
|
564
|
+
const objects = rows.map(
|
|
565
|
+
(row) => Object.fromEntries(headers.map((h, i) => [h, row[i] ?? null]))
|
|
566
|
+
);
|
|
567
|
+
console.log(JSON.stringify(objects, null, 2));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const table = new Table({
|
|
571
|
+
head: headers.map((h) => ansi2.brand(ansi2.bold(h))),
|
|
572
|
+
chars: {
|
|
573
|
+
top: "\u2500",
|
|
574
|
+
"top-mid": "\u252C",
|
|
575
|
+
"top-left": "\u250C",
|
|
576
|
+
"top-right": "\u2510",
|
|
577
|
+
bottom: "\u2500",
|
|
578
|
+
"bottom-mid": "\u2534",
|
|
579
|
+
"bottom-left": "\u2514",
|
|
580
|
+
"bottom-right": "\u2518",
|
|
581
|
+
left: "\u2502",
|
|
582
|
+
"left-mid": "\u251C",
|
|
583
|
+
mid: "\u2500",
|
|
584
|
+
"mid-mid": "\u253C",
|
|
585
|
+
right: "\u2502",
|
|
586
|
+
"right-mid": "\u2524",
|
|
587
|
+
middle: "\u2502"
|
|
588
|
+
},
|
|
589
|
+
style: {
|
|
590
|
+
head: [],
|
|
591
|
+
border: [],
|
|
592
|
+
"padding-left": 1,
|
|
593
|
+
"padding-right": 1
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
for (let i = 0; i < rows.length; i++) {
|
|
597
|
+
const row = i % 2 === 1 ? rows[i].map((cell) => ansi2.dim(cell)) : rows[i];
|
|
598
|
+
table.push(row);
|
|
599
|
+
}
|
|
600
|
+
const output = table.toString();
|
|
601
|
+
const dimBorders = output.replace(
|
|
602
|
+
/[┌┐└┘┬┴├┤┼─│]/g,
|
|
603
|
+
(ch) => ansi2.dim(ch)
|
|
604
|
+
);
|
|
605
|
+
console.log(dimBorders);
|
|
606
|
+
}
|
|
607
|
+
function weiToEth(wei) {
|
|
608
|
+
const divisor = 10n ** 18n;
|
|
609
|
+
const whole = wei / divisor;
|
|
610
|
+
const remainder = wei % divisor;
|
|
611
|
+
if (remainder === 0n) return `${whole}.0`;
|
|
612
|
+
const remStr = remainder.toString().padStart(18, "0").replace(/0+$/, "");
|
|
613
|
+
return `${whole}.${remStr}`;
|
|
614
|
+
}
|
|
615
|
+
function timeAgo(hexTimestamp) {
|
|
616
|
+
const seconds = parseInt(hexTimestamp, 16);
|
|
617
|
+
if (isNaN(seconds)) return hexTimestamp;
|
|
618
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
619
|
+
const diff = now - seconds;
|
|
620
|
+
if (diff < 0) return "in the future";
|
|
621
|
+
if (diff < 60) return `${diff} seconds ago`;
|
|
622
|
+
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
|
623
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
|
624
|
+
if (diff < 2592e3) return `${Math.floor(diff / 86400)} days ago`;
|
|
625
|
+
return `${Math.floor(diff / 2592e3)} months ago`;
|
|
626
|
+
}
|
|
627
|
+
var EXPLORER_MAP = {
|
|
628
|
+
"eth-mainnet": "https://etherscan.io",
|
|
629
|
+
"eth-sepolia": "https://sepolia.etherscan.io",
|
|
630
|
+
"eth-holesky": "https://holesky.etherscan.io",
|
|
631
|
+
"polygon-mainnet": "https://polygonscan.com",
|
|
632
|
+
"polygon-amoy": "https://amoy.polygonscan.com",
|
|
633
|
+
"arb-mainnet": "https://arbiscan.io",
|
|
634
|
+
"arb-sepolia": "https://sepolia.arbiscan.io",
|
|
635
|
+
"opt-mainnet": "https://optimistic.etherscan.io",
|
|
636
|
+
"opt-sepolia": "https://sepolia-optimism.etherscan.io",
|
|
637
|
+
"base-mainnet": "https://basescan.org",
|
|
638
|
+
"base-sepolia": "https://sepolia.basescan.org"
|
|
639
|
+
};
|
|
640
|
+
function etherscanTxURL(hash, network) {
|
|
641
|
+
const base = EXPLORER_MAP[network];
|
|
642
|
+
if (!base) return void 0;
|
|
643
|
+
return `${base}/tx/${hash}`;
|
|
644
|
+
}
|
|
645
|
+
function brandedHelp(options) {
|
|
646
|
+
if (isJSONMode() || quiet) return "";
|
|
647
|
+
if (suppressBrandedHelp && !options?.force) return "";
|
|
648
|
+
const lerp = (a, b, t) => Math.round(a + (b - a) * t);
|
|
649
|
+
const gradientAt = (t) => {
|
|
650
|
+
const c0 = { r: 5, g: 213, b: 255 };
|
|
651
|
+
const c1 = { r: 54, g: 63, b: 249 };
|
|
652
|
+
const c2 = { r: 85, g: 51, b: 255 };
|
|
653
|
+
if (t <= 0.723958) {
|
|
654
|
+
const p2 = t / 0.723958;
|
|
655
|
+
return rgb(
|
|
656
|
+
lerp(c0.r, c1.r, p2),
|
|
657
|
+
lerp(c0.g, c1.g, p2),
|
|
658
|
+
lerp(c0.b, c1.b, p2)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const p = (t - 0.723958) / (1 - 0.723958);
|
|
662
|
+
return rgb(
|
|
663
|
+
lerp(c1.r, c2.r, p),
|
|
664
|
+
lerp(c1.g, c2.g, p),
|
|
665
|
+
lerp(c1.b, c2.b, p)
|
|
666
|
+
);
|
|
667
|
+
};
|
|
668
|
+
const markLines = [
|
|
669
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
670
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
|
|
671
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D ",
|
|
672
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D ",
|
|
673
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 ",
|
|
674
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
|
|
675
|
+
];
|
|
676
|
+
const logo = markLines.map((line, i) => gradientAt(i / (markLines.length - 1))(line)).join("\n");
|
|
677
|
+
return "\n" + logo + "\n";
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/lib/admin-client.ts
|
|
681
|
+
var AdminClient = class _AdminClient {
|
|
682
|
+
static get ADMIN_API_HOST() {
|
|
683
|
+
return `admin-api.${getBaseDomain()}`;
|
|
684
|
+
}
|
|
685
|
+
// Test/debug only: used by mock E2E to route admin requests locally.
|
|
686
|
+
static ADMIN_API_BASE_URL_ENV = "ALCHEMY_ADMIN_API_BASE_URL";
|
|
687
|
+
credential;
|
|
688
|
+
constructor(credential) {
|
|
689
|
+
if (typeof credential === "string") {
|
|
690
|
+
this.validateAccessKey(credential);
|
|
691
|
+
this.credential = { type: "access_key", key: credential };
|
|
692
|
+
} else {
|
|
693
|
+
if (credential.type === "access_key") {
|
|
694
|
+
this.validateAccessKey(credential.key);
|
|
695
|
+
} else if (!credential.token.trim()) {
|
|
696
|
+
throw errAuthRequired();
|
|
697
|
+
}
|
|
698
|
+
this.credential = credential;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
baseURL() {
|
|
702
|
+
const override = this.baseURLOverride();
|
|
703
|
+
if (override) return override.toString().replace(/\/$/, "");
|
|
704
|
+
return `https://admin-api.${getBaseDomain()}`;
|
|
705
|
+
}
|
|
706
|
+
allowedHosts() {
|
|
707
|
+
const hosts = /* @__PURE__ */ new Set([_AdminClient.ADMIN_API_HOST]);
|
|
708
|
+
const override = this.baseURLOverride();
|
|
709
|
+
if (override) hosts.add(override.hostname);
|
|
710
|
+
return hosts;
|
|
711
|
+
}
|
|
712
|
+
allowInsecureTransport(hostname) {
|
|
713
|
+
return isLocalhost(hostname);
|
|
714
|
+
}
|
|
715
|
+
baseURLOverride() {
|
|
716
|
+
return parseBaseURLOverride(_AdminClient.ADMIN_API_BASE_URL_ENV);
|
|
717
|
+
}
|
|
718
|
+
validateAccessKey(accessKey) {
|
|
719
|
+
if (!accessKey.trim() || /\s/.test(accessKey)) {
|
|
720
|
+
throw errInvalidAccessKey();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
assertSafeRequestTarget(url) {
|
|
724
|
+
let parsed;
|
|
725
|
+
try {
|
|
726
|
+
parsed = new URL(url);
|
|
727
|
+
} catch {
|
|
728
|
+
throw errInvalidArgs("Invalid admin API URL.");
|
|
729
|
+
}
|
|
730
|
+
if (!this.allowedHosts().has(parsed.hostname)) {
|
|
731
|
+
throw errInvalidArgs(`Refusing to send credentials to unexpected host: ${parsed.hostname}`);
|
|
732
|
+
}
|
|
733
|
+
if (parsed.protocol !== "https:" && !this.allowInsecureTransport(parsed.hostname)) {
|
|
734
|
+
throw errInvalidArgs("Refusing to send credentials over non-HTTPS connection.");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async request(method, path, body) {
|
|
738
|
+
const url = `${this.baseURL()}${path}`;
|
|
739
|
+
debug(`${method} ${url}`);
|
|
740
|
+
this.assertSafeRequestTarget(url);
|
|
741
|
+
const resp = await fetchWithTimeout(url, {
|
|
742
|
+
method,
|
|
743
|
+
redirect: "error",
|
|
744
|
+
headers: {
|
|
745
|
+
Authorization: `Bearer ${this.credential.type === "access_key" ? this.credential.key : this.credential.token}`,
|
|
746
|
+
"Content-Type": "application/json",
|
|
747
|
+
Accept: "application/json"
|
|
748
|
+
},
|
|
749
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
750
|
+
});
|
|
751
|
+
if (resp.status === 401) {
|
|
752
|
+
debug(`401 Unauthorized from ${url}`);
|
|
753
|
+
throw errInvalidAccessKey();
|
|
754
|
+
}
|
|
755
|
+
if (resp.status === 403) {
|
|
756
|
+
const detail = await resp.text().catch(() => "");
|
|
757
|
+
let reason;
|
|
758
|
+
try {
|
|
759
|
+
const parsed = JSON.parse(detail);
|
|
760
|
+
reason = parsed?.message || parsed?.error?.message || parsed?.error || void 0;
|
|
761
|
+
} catch {
|
|
762
|
+
reason = detail || void 0;
|
|
763
|
+
}
|
|
764
|
+
throw errAccessDenied(typeof reason === "string" ? reason : void 0);
|
|
765
|
+
}
|
|
766
|
+
if (resp.status === 404) {
|
|
767
|
+
const text = await resp.text().catch(() => "");
|
|
768
|
+
throw errNotFound(text || path);
|
|
769
|
+
}
|
|
770
|
+
if (resp.status === 429) throw errRateLimited();
|
|
771
|
+
if (!resp.ok) {
|
|
772
|
+
const text = await resp.text().catch(() => "");
|
|
773
|
+
throw errAdminAPI(resp.status, text);
|
|
774
|
+
}
|
|
775
|
+
return resp.json();
|
|
776
|
+
}
|
|
777
|
+
async listChains() {
|
|
778
|
+
const result = await this.request("GET", "/v1/chains");
|
|
779
|
+
const chains = (Array.isArray(result.data) ? result.data : void 0) ?? (!Array.isArray(result.data) ? result.data?.networks : void 0) ?? (!Array.isArray(result.data) ? result.data?.chains : void 0) ?? result.networks ?? result.chains;
|
|
780
|
+
if (!Array.isArray(chains)) {
|
|
781
|
+
throw errAdminAPI(200, "Unexpected response shape for /v1/chains.");
|
|
782
|
+
}
|
|
783
|
+
return chains;
|
|
784
|
+
}
|
|
785
|
+
async listApps(opts) {
|
|
786
|
+
const params = new URLSearchParams();
|
|
787
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
788
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
789
|
+
const qs = params.toString();
|
|
790
|
+
const resp = await this.request(
|
|
791
|
+
"GET",
|
|
792
|
+
`/v1/apps${qs ? `?${qs}` : ""}`
|
|
793
|
+
);
|
|
794
|
+
return resp.data;
|
|
795
|
+
}
|
|
796
|
+
async listAllApps(opts) {
|
|
797
|
+
const apps = [];
|
|
798
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
799
|
+
let cursor;
|
|
800
|
+
let pages = 0;
|
|
801
|
+
do {
|
|
802
|
+
const page = await this.listApps({
|
|
803
|
+
...cursor && { cursor },
|
|
804
|
+
...opts?.limit !== void 0 && { limit: opts.limit }
|
|
805
|
+
});
|
|
806
|
+
pages += 1;
|
|
807
|
+
apps.push(...page.apps);
|
|
808
|
+
cursor = page.cursor;
|
|
809
|
+
if (cursor && seenCursors.has(cursor)) break;
|
|
810
|
+
if (cursor) seenCursors.add(cursor);
|
|
811
|
+
} while (cursor);
|
|
812
|
+
return { apps, pages };
|
|
813
|
+
}
|
|
814
|
+
async getApp(id) {
|
|
815
|
+
const resp = await this.request("GET", `/v1/apps/${id}`);
|
|
816
|
+
return resp.data;
|
|
817
|
+
}
|
|
818
|
+
async createApp(opts) {
|
|
819
|
+
const resp = await this.request("POST", "/v1/apps", {
|
|
820
|
+
name: opts.name,
|
|
821
|
+
chainNetworks: opts.networks,
|
|
822
|
+
...opts.description && { description: opts.description },
|
|
823
|
+
...opts.products && { products: opts.products }
|
|
824
|
+
});
|
|
825
|
+
return resp.data;
|
|
826
|
+
}
|
|
827
|
+
async deleteApp(id) {
|
|
828
|
+
await this.request("DELETE", `/v1/apps/${id}`);
|
|
829
|
+
}
|
|
830
|
+
async updateApp(id, opts) {
|
|
831
|
+
const resp = await this.request("PATCH", `/v1/apps/${id}`, opts);
|
|
832
|
+
return resp.data;
|
|
833
|
+
}
|
|
834
|
+
async updateNetworkAllowlist(id, networks) {
|
|
835
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/networks`, {
|
|
836
|
+
chainNetworks: networks
|
|
837
|
+
});
|
|
838
|
+
return resp.data;
|
|
839
|
+
}
|
|
840
|
+
async updateAddressAllowlist(id, addresses) {
|
|
841
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/address-allowlist`, {
|
|
842
|
+
addressAllowlist: addresses
|
|
843
|
+
});
|
|
844
|
+
return resp.data;
|
|
845
|
+
}
|
|
846
|
+
async updateOriginAllowlist(id, origins) {
|
|
847
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/origin-allowlist`, {
|
|
848
|
+
originAllowlist: origins
|
|
849
|
+
});
|
|
850
|
+
return resp.data;
|
|
851
|
+
}
|
|
852
|
+
async updateIpAllowlist(id, ips) {
|
|
853
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/ip-allowlist`, {
|
|
854
|
+
ipAllowlist: ips
|
|
855
|
+
});
|
|
856
|
+
return resp.data;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
// src/lib/interaction.ts
|
|
861
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
862
|
+
function isTruthy(value) {
|
|
863
|
+
if (!value) return false;
|
|
864
|
+
const normalized = value.trim().toLowerCase();
|
|
865
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
866
|
+
}
|
|
867
|
+
function isNonInteractiveEnv() {
|
|
868
|
+
return isTruthy(process.env.ALCHEMY_NON_INTERACTIVE);
|
|
869
|
+
}
|
|
870
|
+
function isInteractiveAllowed(program) {
|
|
871
|
+
if (!stdin2.isTTY || !stdout2.isTTY) return false;
|
|
872
|
+
if (isJSONMode()) return false;
|
|
873
|
+
if (isNonInteractiveEnv()) return false;
|
|
874
|
+
if (program && program.opts().interactive === false) return false;
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/lib/resolve.ts
|
|
879
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
880
|
+
|
|
881
|
+
// src/lib/client.ts
|
|
882
|
+
var Client = class _Client {
|
|
883
|
+
apiKey;
|
|
884
|
+
network;
|
|
885
|
+
// Test/debug only: used by mock E2E to route CLI requests locally.
|
|
886
|
+
static RPC_BASE_URL_ENV = "ALCHEMY_RPC_BASE_URL";
|
|
887
|
+
constructor(apiKey, network) {
|
|
888
|
+
this.apiKey = apiKey;
|
|
889
|
+
this.network = network;
|
|
890
|
+
this.validateNetwork(network);
|
|
891
|
+
}
|
|
892
|
+
validateNetwork(network) {
|
|
893
|
+
if (this.rpcBaseURLOverride()) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const domain = getBaseDomain();
|
|
897
|
+
const hostname = `${network}.g.${domain}`;
|
|
898
|
+
let parsed;
|
|
899
|
+
try {
|
|
900
|
+
parsed = new URL(`https://${hostname}`);
|
|
901
|
+
} catch {
|
|
902
|
+
throw errInvalidArgs(
|
|
903
|
+
`Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (!parsed.hostname.endsWith(`.g.${domain}`)) {
|
|
907
|
+
throw errInvalidArgs(
|
|
908
|
+
`Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
rpcBaseURLOverride() {
|
|
913
|
+
return parseBaseURLOverride(_Client.RPC_BASE_URL_ENV);
|
|
914
|
+
}
|
|
915
|
+
rpcBaseURL() {
|
|
916
|
+
const override = this.rpcBaseURLOverride();
|
|
917
|
+
if (override) return override;
|
|
918
|
+
return new URL(`https://${this.network}.g.${getBaseDomain()}`);
|
|
919
|
+
}
|
|
920
|
+
rpcURL() {
|
|
921
|
+
return new URL(`/v2/${this.apiKey}`, this.rpcBaseURL()).toString();
|
|
922
|
+
}
|
|
923
|
+
enhancedURL() {
|
|
924
|
+
return new URL(`/nft/v3/${this.apiKey}`, this.rpcBaseURL()).toString();
|
|
925
|
+
}
|
|
926
|
+
parseNetworkNotEnabledError(detail) {
|
|
927
|
+
const match = detail.match(
|
|
928
|
+
/([A-Z0-9_]+)\s+is not enabled for this app\.\s+Visit this page to enable the network:\s+(https?:\/\/\S+)/i
|
|
929
|
+
);
|
|
930
|
+
if (!match) return null;
|
|
931
|
+
return errNetworkNotEnabled(match[1], detail);
|
|
932
|
+
}
|
|
933
|
+
authErrorFromResponseBody(detail) {
|
|
934
|
+
const networkNotEnabled = this.parseNetworkNotEnabledError(detail);
|
|
935
|
+
if (networkNotEnabled) return networkNotEnabled;
|
|
936
|
+
return errInvalidAPIKey(detail || void 0);
|
|
937
|
+
}
|
|
938
|
+
tryParseRPCError(text) {
|
|
939
|
+
try {
|
|
940
|
+
const parsed = JSON.parse(text);
|
|
941
|
+
if (parsed?.error?.code !== void 0 && parsed?.error?.message !== void 0) {
|
|
942
|
+
return errRPC(parsed.error.code, parsed.error.message);
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
verboseLog(message) {
|
|
949
|
+
if (verbose) {
|
|
950
|
+
process.stderr.write(`[verbose] ${message}
|
|
951
|
+
`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async doFetch(url, init) {
|
|
955
|
+
return fetchWithTimeout(url, init);
|
|
956
|
+
}
|
|
957
|
+
async call(method, params = []) {
|
|
958
|
+
const body = {
|
|
959
|
+
jsonrpc: "2.0",
|
|
960
|
+
method,
|
|
961
|
+
params,
|
|
962
|
+
id: 1
|
|
963
|
+
};
|
|
964
|
+
const redactedURL = redactSensitiveText(this.rpcURL());
|
|
965
|
+
this.verboseLog(`\u2192 POST ${redactedURL}`);
|
|
966
|
+
this.verboseLog(` method: ${method}`);
|
|
967
|
+
const hasParams = Array.isArray(params) ? params.length > 0 : Object.keys(params).length > 0;
|
|
968
|
+
if (hasParams) {
|
|
969
|
+
this.verboseLog(` params: ${JSON.stringify(params)}`);
|
|
970
|
+
}
|
|
971
|
+
const startTime = Date.now();
|
|
972
|
+
const resp = await this.doFetch(this.rpcURL(), {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: {
|
|
975
|
+
"Content-Type": "application/json",
|
|
976
|
+
Accept: "application/json"
|
|
977
|
+
},
|
|
978
|
+
body: JSON.stringify(body)
|
|
979
|
+
});
|
|
980
|
+
this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
|
|
981
|
+
if (resp.status === 429) throw errRateLimited();
|
|
982
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
983
|
+
const detail = await resp.text().catch(() => "");
|
|
984
|
+
throw this.authErrorFromResponseBody(detail);
|
|
985
|
+
}
|
|
986
|
+
if (!resp.ok) {
|
|
987
|
+
const text = await resp.text().catch(() => "");
|
|
988
|
+
const rpcError = this.tryParseRPCError(text);
|
|
989
|
+
if (rpcError) throw rpcError;
|
|
990
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
991
|
+
}
|
|
992
|
+
const rpcResp = await resp.json();
|
|
993
|
+
if (rpcResp.error) {
|
|
994
|
+
throw errRPC(rpcResp.error.code, rpcResp.error.message);
|
|
995
|
+
}
|
|
996
|
+
return rpcResp.result;
|
|
997
|
+
}
|
|
998
|
+
async callEnhanced(path, params) {
|
|
999
|
+
const url = new URL(`${this.enhancedURL()}/${path}`);
|
|
1000
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1001
|
+
url.searchParams.set(k, v);
|
|
1002
|
+
}
|
|
1003
|
+
const redactedURL = redactSensitiveText(url.toString());
|
|
1004
|
+
this.verboseLog(`\u2192 GET ${redactedURL}`);
|
|
1005
|
+
const startTime = Date.now();
|
|
1006
|
+
const resp = await this.doFetch(url.toString(), {
|
|
1007
|
+
headers: { Accept: "application/json" }
|
|
1008
|
+
});
|
|
1009
|
+
this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
|
|
1010
|
+
if (resp.status === 429) throw errRateLimited();
|
|
1011
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
1012
|
+
const detail = await resp.text().catch(() => "");
|
|
1013
|
+
throw this.authErrorFromResponseBody(detail);
|
|
1014
|
+
}
|
|
1015
|
+
if (!resp.ok) {
|
|
1016
|
+
const text = await resp.text().catch(() => "");
|
|
1017
|
+
const rpcError = this.tryParseRPCError(text);
|
|
1018
|
+
if (rpcError) throw rpcError;
|
|
1019
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
1020
|
+
}
|
|
1021
|
+
return resp.json();
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// src/lib/x402-client.ts
|
|
1026
|
+
import { signSiwe, createPayment } from "@alchemy/x402";
|
|
1027
|
+
var X402Client = class _X402Client {
|
|
1028
|
+
network;
|
|
1029
|
+
privateKey;
|
|
1030
|
+
siweToken = null;
|
|
1031
|
+
static X402_BASE_URL_ENV = "ALCHEMY_X402_BASE_URL";
|
|
1032
|
+
static get DEFAULT_BASE() {
|
|
1033
|
+
return `https://x402.${getBaseDomain()}`;
|
|
1034
|
+
}
|
|
1035
|
+
constructor(privateKey, network) {
|
|
1036
|
+
this.privateKey = privateKey;
|
|
1037
|
+
this.network = network;
|
|
1038
|
+
this.validateNetwork(network);
|
|
1039
|
+
}
|
|
1040
|
+
validateNetwork(network) {
|
|
1041
|
+
if (this.baseURLOverride()) return;
|
|
1042
|
+
if (!/^[A-Za-z0-9:_-]{1,128}$/.test(network)) {
|
|
1043
|
+
throw errInvalidArgs(`Invalid network: ${network}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
baseURLOverride() {
|
|
1047
|
+
return parseBaseURLOverride(_X402Client.X402_BASE_URL_ENV);
|
|
1048
|
+
}
|
|
1049
|
+
baseURL() {
|
|
1050
|
+
const override = this.baseURLOverride();
|
|
1051
|
+
if (override) return override;
|
|
1052
|
+
return new URL(_X402Client.DEFAULT_BASE);
|
|
1053
|
+
}
|
|
1054
|
+
rpcURL() {
|
|
1055
|
+
return new URL(`/${this.network}/v2`, this.baseURL()).toString();
|
|
1056
|
+
}
|
|
1057
|
+
enhancedURL() {
|
|
1058
|
+
return new URL(`/${this.network}/nft/v3`, this.baseURL()).toString();
|
|
1059
|
+
}
|
|
1060
|
+
static SIWE_TTL = "1h";
|
|
1061
|
+
static SIWE_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
1062
|
+
// refresh 5min before expiry
|
|
1063
|
+
async ensureSiweToken() {
|
|
1064
|
+
if (this.siweToken) return this.siweToken;
|
|
1065
|
+
const cfg = load();
|
|
1066
|
+
if (cfg.siwe_token && cfg.siwe_token_expires_at) {
|
|
1067
|
+
const expiry = new Date(cfg.siwe_token_expires_at);
|
|
1068
|
+
const remaining = expiry.getTime() - Date.now();
|
|
1069
|
+
debug(`SIWE: found cached token (length=${cfg.siwe_token.length}, remaining=${Math.round(remaining / 1e3)}s)`);
|
|
1070
|
+
if (!Number.isNaN(expiry.getTime()) && remaining > _X402Client.SIWE_EXPIRY_BUFFER_MS) {
|
|
1071
|
+
this.siweToken = cfg.siwe_token;
|
|
1072
|
+
return this.siweToken;
|
|
1073
|
+
}
|
|
1074
|
+
debug("SIWE: cached token expired or expiring soon");
|
|
1075
|
+
} else {
|
|
1076
|
+
debug("SIWE: no cached token in config");
|
|
1077
|
+
}
|
|
1078
|
+
debug("SIWE: generating fresh token");
|
|
1079
|
+
this.siweToken = await signSiwe({
|
|
1080
|
+
privateKey: this.privateKey,
|
|
1081
|
+
expiresAfter: _X402Client.SIWE_TTL
|
|
1082
|
+
});
|
|
1083
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1e3).toISOString();
|
|
1084
|
+
debug(`SIWE: saving token to config (length=${this.siweToken.length}, expires=${expiresAt})`);
|
|
1085
|
+
save({ ...load(), siwe_token: this.siweToken, siwe_token_expires_at: expiresAt });
|
|
1086
|
+
return this.siweToken;
|
|
1087
|
+
}
|
|
1088
|
+
refreshSiweToken() {
|
|
1089
|
+
this.siweToken = null;
|
|
1090
|
+
const cfg = load();
|
|
1091
|
+
save({ ...cfg, siwe_token: void 0, siwe_token_expires_at: void 0 });
|
|
1092
|
+
}
|
|
1093
|
+
async call(method, params = []) {
|
|
1094
|
+
const body = { jsonrpc: "2.0", method, params, id: 1 };
|
|
1095
|
+
const jsonBody = JSON.stringify(body);
|
|
1096
|
+
const buildInit = (extra) => ({
|
|
1097
|
+
method: "POST",
|
|
1098
|
+
headers: {
|
|
1099
|
+
"Content-Type": "application/json",
|
|
1100
|
+
Accept: "application/json",
|
|
1101
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
1102
|
+
...extra
|
|
1103
|
+
},
|
|
1104
|
+
body: jsonBody
|
|
1105
|
+
});
|
|
1106
|
+
await this.ensureSiweToken();
|
|
1107
|
+
let resp = await this.doFetch(this.rpcURL(), buildInit());
|
|
1108
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
1109
|
+
authRetry: async () => {
|
|
1110
|
+
this.refreshSiweToken();
|
|
1111
|
+
await this.ensureSiweToken();
|
|
1112
|
+
return this.doFetch(this.rpcURL(), buildInit());
|
|
1113
|
+
},
|
|
1114
|
+
paymentRetry: async (paymentSig) => this.doFetch(this.rpcURL(), buildInit({ "Payment-Signature": paymentSig }))
|
|
1115
|
+
});
|
|
1116
|
+
if (resp.status === 429) throw errRateLimited();
|
|
1117
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
1118
|
+
if (!resp.ok) {
|
|
1119
|
+
const text = await resp.text().catch(() => "");
|
|
1120
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
1121
|
+
}
|
|
1122
|
+
const rpcResp = await resp.json();
|
|
1123
|
+
if (rpcResp.error) {
|
|
1124
|
+
throw errRPC(rpcResp.error.code, rpcResp.error.message);
|
|
1125
|
+
}
|
|
1126
|
+
return rpcResp.result;
|
|
1127
|
+
}
|
|
1128
|
+
async callEnhanced(path, params) {
|
|
1129
|
+
const url = new URL(`${this.enhancedURL()}/${path}`);
|
|
1130
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1131
|
+
url.searchParams.set(k, v);
|
|
1132
|
+
}
|
|
1133
|
+
const urlStr = url.toString();
|
|
1134
|
+
const buildInit = (extra) => ({
|
|
1135
|
+
headers: {
|
|
1136
|
+
Accept: "application/json",
|
|
1137
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
1138
|
+
...extra
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
await this.ensureSiweToken();
|
|
1142
|
+
let resp = await this.doFetch(urlStr, buildInit());
|
|
1143
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
1144
|
+
authRetry: async () => {
|
|
1145
|
+
this.refreshSiweToken();
|
|
1146
|
+
await this.ensureSiweToken();
|
|
1147
|
+
return this.doFetch(urlStr, buildInit());
|
|
1148
|
+
},
|
|
1149
|
+
paymentRetry: async (paymentSig) => this.doFetch(urlStr, buildInit({ "Payment-Signature": paymentSig }))
|
|
1150
|
+
});
|
|
1151
|
+
if (resp.status === 429) throw errRateLimited();
|
|
1152
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
1153
|
+
if (!resp.ok) {
|
|
1154
|
+
const text = await resp.text().catch(() => "");
|
|
1155
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
1156
|
+
}
|
|
1157
|
+
return resp.json();
|
|
1158
|
+
}
|
|
1159
|
+
async callRest(path, options = {}) {
|
|
1160
|
+
const base = new URL(`/${path.replace(/^\//, "")}`, this.baseURL());
|
|
1161
|
+
if (options.query) {
|
|
1162
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
1163
|
+
if (v !== void 0 && v !== "") base.searchParams.set(k, v);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const urlStr = base.toString();
|
|
1167
|
+
const method = options.method ?? "GET";
|
|
1168
|
+
const buildInit = (extra) => ({
|
|
1169
|
+
method,
|
|
1170
|
+
headers: {
|
|
1171
|
+
"Content-Type": "application/json",
|
|
1172
|
+
Accept: "application/json",
|
|
1173
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
1174
|
+
...extra
|
|
1175
|
+
},
|
|
1176
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
1177
|
+
});
|
|
1178
|
+
await this.ensureSiweToken();
|
|
1179
|
+
let resp = await this.doFetch(urlStr, buildInit());
|
|
1180
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
1181
|
+
authRetry: async () => {
|
|
1182
|
+
this.refreshSiweToken();
|
|
1183
|
+
await this.ensureSiweToken();
|
|
1184
|
+
return this.doFetch(urlStr, buildInit());
|
|
1185
|
+
},
|
|
1186
|
+
paymentRetry: async (paymentSig) => this.doFetch(urlStr, buildInit({ "Payment-Signature": paymentSig }))
|
|
1187
|
+
});
|
|
1188
|
+
if (resp.status === 429) throw errRateLimited();
|
|
1189
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
1190
|
+
if (!resp.ok) {
|
|
1191
|
+
const text = await resp.text().catch(() => "");
|
|
1192
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
1193
|
+
}
|
|
1194
|
+
return resp.json();
|
|
1195
|
+
}
|
|
1196
|
+
async doFetch(url, init) {
|
|
1197
|
+
return fetchWithTimeout(url, init);
|
|
1198
|
+
}
|
|
1199
|
+
async parsePaymentError(resp) {
|
|
1200
|
+
const text = await resp.text().catch(() => "");
|
|
1201
|
+
try {
|
|
1202
|
+
const body = JSON.parse(text);
|
|
1203
|
+
const reason = body?.extensions?.paymentError?.info?.reason;
|
|
1204
|
+
const message = body?.extensions?.paymentError?.info?.message;
|
|
1205
|
+
const payer = body?.extensions?.paymentError?.info?.payer;
|
|
1206
|
+
if (reason === "insufficient_funds") {
|
|
1207
|
+
const network = body?.accepts?.[0]?.network;
|
|
1208
|
+
const asset = body?.accepts?.[0]?.extra?.name ?? "USDC";
|
|
1209
|
+
const networkLabel = network === "eip155:8453" ? "Base" : network ?? "the payment network";
|
|
1210
|
+
return new CLIError(
|
|
1211
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
1212
|
+
`Insufficient ${asset} balance on ${networkLabel}. ${message ?? ""}`.trim(),
|
|
1213
|
+
`Fund wallet ${payer ?? ""} with ${asset} on ${networkLabel} to use x402.`.trim()
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
return new CLIError(
|
|
1217
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
1218
|
+
`x402 payment failed: ${message || body?.error || text}`
|
|
1219
|
+
);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return new CLIError(
|
|
1222
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
1223
|
+
`x402 payment failed: ${text}`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async handleAuthAndPayment(resp, retries) {
|
|
1228
|
+
if (resp.status === 401) {
|
|
1229
|
+
const detail = await resp.text().catch(() => "");
|
|
1230
|
+
if (detail.includes("MESSAGE_EXPIRED")) {
|
|
1231
|
+
return retries.authRetry();
|
|
1232
|
+
}
|
|
1233
|
+
throw new CLIError(
|
|
1234
|
+
ErrorCode.AUTH_REQUIRED,
|
|
1235
|
+
`x402 authentication failed: ${detail || "unauthorized"}`,
|
|
1236
|
+
"Check your wallet key and try again."
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (resp.status === 402) {
|
|
1240
|
+
const paymentRequiredHeader = resp.headers.get("payment-required");
|
|
1241
|
+
if (!paymentRequiredHeader) {
|
|
1242
|
+
throw new CLIError(
|
|
1243
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
1244
|
+
"x402 payment required but no Payment-Required header received."
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const paymentSignature = await createPayment({
|
|
1248
|
+
privateKey: this.privateKey,
|
|
1249
|
+
paymentRequiredHeader
|
|
1250
|
+
});
|
|
1251
|
+
return retries.paymentRetry(paymentSignature);
|
|
1252
|
+
}
|
|
1253
|
+
return resp;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/lib/resolve.ts
|
|
1258
|
+
function resolveAPIKey(program, cfg) {
|
|
1259
|
+
const opts = program.opts();
|
|
1260
|
+
if (opts.apiKey) return opts.apiKey;
|
|
1261
|
+
if (process.env.ALCHEMY_API_KEY) return process.env.ALCHEMY_API_KEY;
|
|
1262
|
+
const config = cfg ?? load();
|
|
1263
|
+
if (config.api_key) return config.api_key;
|
|
1264
|
+
if (config.app?.apiKey) return config.app.apiKey;
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
function resolveAccessKey(program, cfg) {
|
|
1268
|
+
const opts = program.opts();
|
|
1269
|
+
if (opts.accessKey) return opts.accessKey;
|
|
1270
|
+
if (process.env.ALCHEMY_ACCESS_KEY) return process.env.ALCHEMY_ACCESS_KEY;
|
|
1271
|
+
const config = cfg ?? load();
|
|
1272
|
+
if (config.access_key) return config.access_key;
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
function resolveNetwork(program, cfg, defaultNetwork) {
|
|
1276
|
+
const opts = program.opts();
|
|
1277
|
+
if (opts.network) return opts.network;
|
|
1278
|
+
if (process.env.ALCHEMY_NETWORK) return process.env.ALCHEMY_NETWORK;
|
|
1279
|
+
const config = cfg ?? load();
|
|
1280
|
+
if (config.network) return config.network;
|
|
1281
|
+
return defaultNetwork ?? "eth-mainnet";
|
|
1282
|
+
}
|
|
1283
|
+
function resolveAppId(program, cfg) {
|
|
1284
|
+
const opts = program.opts();
|
|
1285
|
+
if (opts.appId) return opts.appId;
|
|
1286
|
+
const config = cfg ?? load();
|
|
1287
|
+
if (config.app?.id) return config.app.id;
|
|
1288
|
+
return void 0;
|
|
1289
|
+
}
|
|
1290
|
+
function resolveAuthToken(cfg) {
|
|
1291
|
+
const config = cfg ?? load();
|
|
1292
|
+
if (!config.auth_token?.trim()) return void 0;
|
|
1293
|
+
if (config.auth_token_expires_at) {
|
|
1294
|
+
const expiry = new Date(config.auth_token_expires_at);
|
|
1295
|
+
if (!Number.isNaN(expiry.getTime()) && expiry <= /* @__PURE__ */ new Date()) {
|
|
1296
|
+
return void 0;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return config.auth_token;
|
|
1300
|
+
}
|
|
1301
|
+
function adminClientFromFlags(program) {
|
|
1302
|
+
const cfg = load();
|
|
1303
|
+
const accessKey = resolveAccessKey(program, cfg);
|
|
1304
|
+
if (accessKey) return new AdminClient(accessKey);
|
|
1305
|
+
const authToken = resolveAuthToken(cfg);
|
|
1306
|
+
if (authToken) return new AdminClient({ type: "auth_token", token: authToken });
|
|
1307
|
+
throw errAccessKeyRequired();
|
|
1308
|
+
}
|
|
1309
|
+
function resolveX402(program, cfg) {
|
|
1310
|
+
const opts = program.opts();
|
|
1311
|
+
if (opts.x402) return true;
|
|
1312
|
+
const config = cfg ?? load();
|
|
1313
|
+
return config.x402 === true;
|
|
1314
|
+
}
|
|
1315
|
+
function resolveX402Client(program) {
|
|
1316
|
+
const cfg = load();
|
|
1317
|
+
if (!resolveX402(program, cfg)) return null;
|
|
1318
|
+
const walletKey = resolveWalletKey(program, cfg);
|
|
1319
|
+
if (!walletKey) return null;
|
|
1320
|
+
return new X402Client(walletKey, resolveNetwork(program, cfg));
|
|
1321
|
+
}
|
|
1322
|
+
function resolveWalletKey(program, cfg) {
|
|
1323
|
+
const opts = program.opts();
|
|
1324
|
+
if (opts.walletKeyFile) {
|
|
1325
|
+
return readFileSync2(opts.walletKeyFile, "utf-8").trim();
|
|
1326
|
+
}
|
|
1327
|
+
if (process.env.ALCHEMY_WALLET_KEY) {
|
|
1328
|
+
return process.env.ALCHEMY_WALLET_KEY;
|
|
1329
|
+
}
|
|
1330
|
+
const config = cfg ?? load();
|
|
1331
|
+
if (config.wallet_key_file) {
|
|
1332
|
+
return readFileSync2(config.wallet_key_file, "utf-8").trim();
|
|
1333
|
+
}
|
|
1334
|
+
return void 0;
|
|
1335
|
+
}
|
|
1336
|
+
function clientFromFlags(program, opts) {
|
|
1337
|
+
const cfg = load();
|
|
1338
|
+
const network = resolveNetwork(program, cfg, opts?.defaultNetwork);
|
|
1339
|
+
debug(`using network=${network}`);
|
|
1340
|
+
const programOpts = program.opts();
|
|
1341
|
+
if (programOpts.accessKey) {
|
|
1342
|
+
throw errInvalidArgs(
|
|
1343
|
+
"--access-key is for admin commands (apps, chains, webhooks). Use --api-key for RPC commands."
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (resolveX402(program, cfg)) {
|
|
1347
|
+
const walletKey = resolveWalletKey(program, cfg);
|
|
1348
|
+
if (!walletKey) throw errWalletKeyRequired();
|
|
1349
|
+
return new X402Client(walletKey, network);
|
|
1350
|
+
}
|
|
1351
|
+
const apiKey = resolveAPIKey(program, cfg);
|
|
1352
|
+
if (!apiKey) throw errAuthRequired();
|
|
1353
|
+
return new Client(apiKey, network);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
export {
|
|
1357
|
+
maskIf,
|
|
1358
|
+
KEY_MAP,
|
|
1359
|
+
configPath,
|
|
1360
|
+
configDir,
|
|
1361
|
+
load,
|
|
1362
|
+
save,
|
|
1363
|
+
get,
|
|
1364
|
+
toMap,
|
|
1365
|
+
promptText,
|
|
1366
|
+
promptConfirm,
|
|
1367
|
+
promptSelect,
|
|
1368
|
+
promptAutocomplete,
|
|
1369
|
+
promptMultiselect,
|
|
1370
|
+
green,
|
|
1371
|
+
red,
|
|
1372
|
+
dim,
|
|
1373
|
+
bold,
|
|
1374
|
+
yellow,
|
|
1375
|
+
brand,
|
|
1376
|
+
setBrandedHelpSuppressed,
|
|
1377
|
+
successBadge,
|
|
1378
|
+
failBadge,
|
|
1379
|
+
withSpinner,
|
|
1380
|
+
printKeyValueBox,
|
|
1381
|
+
emptyState,
|
|
1382
|
+
printSyntaxJSON,
|
|
1383
|
+
printTable,
|
|
1384
|
+
weiToEth,
|
|
1385
|
+
timeAgo,
|
|
1386
|
+
etherscanTxURL,
|
|
1387
|
+
brandedHelp,
|
|
1388
|
+
AdminClient,
|
|
1389
|
+
isInteractiveAllowed,
|
|
1390
|
+
resolveAPIKey,
|
|
1391
|
+
resolveNetwork,
|
|
1392
|
+
resolveAppId,
|
|
1393
|
+
resolveAuthToken,
|
|
1394
|
+
adminClientFromFlags,
|
|
1395
|
+
resolveX402Client,
|
|
1396
|
+
resolveWalletKey,
|
|
1397
|
+
clientFromFlags
|
|
1398
|
+
};
|