@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 +22 -71
- package/dist/{auth-E26YCAJV.js → auth-76PDHQ3U.js} +1 -1
- package/dist/{auth-7E33EMAI.js → auth-PYH5WEC3.js} +6 -3
- package/dist/{chunk-IGD4NIK7.js → chunk-5ZAK2VSS.js} +2 -2
- package/dist/chunk-BAAQ7ELR.js +143 -0
- package/dist/{chunk-LYUW7O6X.js → chunk-DBTRDS35.js} +30 -12
- package/dist/{chunk-5X6YRTPU.js → chunk-FM7GQX6U.js} +4 -2
- package/dist/chunk-KDMIWPZH.js +27 -0
- package/dist/chunk-NBDWF4ZQ.js +554 -0
- package/dist/chunk-NM25MEJZ.js +724 -0
- package/dist/{chunk-Z7J64GJJ.js → chunk-NSG4ZKZI.js} +2 -2
- package/dist/index.js +699 -30
- package/dist/{interactive-G4ON47AR.js → interactive-CGEVIPC2.js} +9 -5
- package/dist/onboarding-DNEXVUUH.js +64 -0
- package/dist/resolve-CLDYJ27A.js +30 -0
- package/package.json +1 -1
- package/dist/chunk-44OGGLN4.js +0 -681
- package/dist/chunk-T2XSNZE3.js +0 -1398
- package/dist/onboarding-CWCVWSUG.js +0 -227
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
|
|
3
|
+
import {
|
|
4
|
+
load,
|
|
5
|
+
save
|
|
6
|
+
} from "./chunk-BAAQ7ELR.js";
|
|
7
|
+
import {
|
|
8
|
+
CLIError,
|
|
9
|
+
ErrorCode,
|
|
10
|
+
debug,
|
|
11
|
+
errAccessDenied,
|
|
12
|
+
errAccessKeyRequired,
|
|
13
|
+
errAdminAPI,
|
|
14
|
+
errAppRequired,
|
|
15
|
+
errAuthRequired,
|
|
16
|
+
errInvalidAPIKey,
|
|
17
|
+
errInvalidAccessKey,
|
|
18
|
+
errInvalidArgs,
|
|
19
|
+
errNetwork,
|
|
20
|
+
errNetworkNotEnabled,
|
|
21
|
+
errNotFound,
|
|
22
|
+
errRPC,
|
|
23
|
+
errRateLimited,
|
|
24
|
+
errWalletKeyRequired,
|
|
25
|
+
fetchWithTimeout,
|
|
26
|
+
getBaseDomain,
|
|
27
|
+
isLocalhost,
|
|
28
|
+
parseBaseURLOverride,
|
|
29
|
+
redactSensitiveText,
|
|
30
|
+
verbose
|
|
31
|
+
} from "./chunk-56ZVYB4G.js";
|
|
32
|
+
|
|
33
|
+
// src/lib/resolve.ts
|
|
34
|
+
import { readFileSync } from "fs";
|
|
35
|
+
|
|
36
|
+
// src/lib/client.ts
|
|
37
|
+
var Client = class _Client {
|
|
38
|
+
apiKey;
|
|
39
|
+
network;
|
|
40
|
+
// Test/debug only: used by mock E2E to route CLI requests locally.
|
|
41
|
+
static RPC_BASE_URL_ENV = "ALCHEMY_RPC_BASE_URL";
|
|
42
|
+
constructor(apiKey, network) {
|
|
43
|
+
this.apiKey = apiKey;
|
|
44
|
+
this.network = network;
|
|
45
|
+
this.validateNetwork(network);
|
|
46
|
+
}
|
|
47
|
+
validateNetwork(network) {
|
|
48
|
+
if (this.rpcBaseURLOverride()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const domain = getBaseDomain();
|
|
52
|
+
const hostname = `${network}.g.${domain}`;
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(`https://${hostname}`);
|
|
56
|
+
} catch {
|
|
57
|
+
throw errInvalidArgs(
|
|
58
|
+
`Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!parsed.hostname.endsWith(`.g.${domain}`)) {
|
|
62
|
+
throw errInvalidArgs(
|
|
63
|
+
`Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
rpcBaseURLOverride() {
|
|
68
|
+
return parseBaseURLOverride(_Client.RPC_BASE_URL_ENV);
|
|
69
|
+
}
|
|
70
|
+
rpcBaseURL() {
|
|
71
|
+
const override = this.rpcBaseURLOverride();
|
|
72
|
+
if (override) return override;
|
|
73
|
+
return new URL(`https://${this.network}.g.${getBaseDomain()}`);
|
|
74
|
+
}
|
|
75
|
+
rpcURL() {
|
|
76
|
+
return new URL(`/v2/${this.apiKey}`, this.rpcBaseURL()).toString();
|
|
77
|
+
}
|
|
78
|
+
enhancedURL() {
|
|
79
|
+
return new URL(`/nft/v3/${this.apiKey}`, this.rpcBaseURL()).toString();
|
|
80
|
+
}
|
|
81
|
+
parseNetworkNotEnabledError(detail) {
|
|
82
|
+
const match = detail.match(
|
|
83
|
+
/([A-Z0-9_]+)\s+is not enabled for this app\.\s+Visit this page to enable the network:\s+(https?:\/\/\S+)/i
|
|
84
|
+
);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
return errNetworkNotEnabled(match[1], detail);
|
|
87
|
+
}
|
|
88
|
+
authErrorFromResponseBody(detail) {
|
|
89
|
+
const networkNotEnabled = this.parseNetworkNotEnabledError(detail);
|
|
90
|
+
if (networkNotEnabled) return networkNotEnabled;
|
|
91
|
+
return errInvalidAPIKey(detail || void 0);
|
|
92
|
+
}
|
|
93
|
+
tryParseRPCError(text) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(text);
|
|
96
|
+
if (parsed?.error?.code !== void 0 && parsed?.error?.message !== void 0) {
|
|
97
|
+
return errRPC(parsed.error.code, parsed.error.message);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
verboseLog(message) {
|
|
104
|
+
if (verbose) {
|
|
105
|
+
process.stderr.write(`[verbose] ${message}
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async doFetch(url, init) {
|
|
110
|
+
return fetchWithTimeout(url, init);
|
|
111
|
+
}
|
|
112
|
+
async call(method, params = []) {
|
|
113
|
+
const body = {
|
|
114
|
+
jsonrpc: "2.0",
|
|
115
|
+
method,
|
|
116
|
+
params,
|
|
117
|
+
id: 1
|
|
118
|
+
};
|
|
119
|
+
const redactedURL = redactSensitiveText(this.rpcURL());
|
|
120
|
+
this.verboseLog(`\u2192 POST ${redactedURL}`);
|
|
121
|
+
this.verboseLog(` method: ${method}`);
|
|
122
|
+
const hasParams = Array.isArray(params) ? params.length > 0 : Object.keys(params).length > 0;
|
|
123
|
+
if (hasParams) {
|
|
124
|
+
this.verboseLog(` params: ${JSON.stringify(params)}`);
|
|
125
|
+
}
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
const resp = await this.doFetch(this.rpcURL(), {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
Accept: "application/json"
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(body)
|
|
134
|
+
});
|
|
135
|
+
this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
|
|
136
|
+
if (resp.status === 429) throw errRateLimited();
|
|
137
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
138
|
+
const detail = await resp.text().catch(() => "");
|
|
139
|
+
throw this.authErrorFromResponseBody(detail);
|
|
140
|
+
}
|
|
141
|
+
if (!resp.ok) {
|
|
142
|
+
const text = await resp.text().catch(() => "");
|
|
143
|
+
const rpcError = this.tryParseRPCError(text);
|
|
144
|
+
if (rpcError) throw rpcError;
|
|
145
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
146
|
+
}
|
|
147
|
+
const rpcResp = await resp.json();
|
|
148
|
+
if (rpcResp.error) {
|
|
149
|
+
throw errRPC(rpcResp.error.code, rpcResp.error.message);
|
|
150
|
+
}
|
|
151
|
+
return rpcResp.result;
|
|
152
|
+
}
|
|
153
|
+
async callEnhanced(path, params) {
|
|
154
|
+
const url = new URL(`${this.enhancedURL()}/${path}`);
|
|
155
|
+
for (const [k, v] of Object.entries(params)) {
|
|
156
|
+
url.searchParams.set(k, v);
|
|
157
|
+
}
|
|
158
|
+
const redactedURL = redactSensitiveText(url.toString());
|
|
159
|
+
this.verboseLog(`\u2192 GET ${redactedURL}`);
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const resp = await this.doFetch(url.toString(), {
|
|
162
|
+
headers: { Accept: "application/json" }
|
|
163
|
+
});
|
|
164
|
+
this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
|
|
165
|
+
if (resp.status === 429) throw errRateLimited();
|
|
166
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
167
|
+
const detail = await resp.text().catch(() => "");
|
|
168
|
+
throw this.authErrorFromResponseBody(detail);
|
|
169
|
+
}
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
const text = await resp.text().catch(() => "");
|
|
172
|
+
const rpcError = this.tryParseRPCError(text);
|
|
173
|
+
if (rpcError) throw rpcError;
|
|
174
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
175
|
+
}
|
|
176
|
+
return resp.json();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/lib/x402-client.ts
|
|
181
|
+
import { signSiwe, createPayment } from "@alchemy/x402";
|
|
182
|
+
var X402Client = class _X402Client {
|
|
183
|
+
network;
|
|
184
|
+
privateKey;
|
|
185
|
+
siweToken = null;
|
|
186
|
+
static X402_BASE_URL_ENV = "ALCHEMY_X402_BASE_URL";
|
|
187
|
+
static get DEFAULT_BASE() {
|
|
188
|
+
return `https://x402.${getBaseDomain()}`;
|
|
189
|
+
}
|
|
190
|
+
constructor(privateKey, network) {
|
|
191
|
+
this.privateKey = privateKey;
|
|
192
|
+
this.network = network;
|
|
193
|
+
this.validateNetwork(network);
|
|
194
|
+
}
|
|
195
|
+
validateNetwork(network) {
|
|
196
|
+
if (this.baseURLOverride()) return;
|
|
197
|
+
if (!/^[A-Za-z0-9:_-]{1,128}$/.test(network)) {
|
|
198
|
+
throw errInvalidArgs(`Invalid network: ${network}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
baseURLOverride() {
|
|
202
|
+
return parseBaseURLOverride(_X402Client.X402_BASE_URL_ENV);
|
|
203
|
+
}
|
|
204
|
+
baseURL() {
|
|
205
|
+
const override = this.baseURLOverride();
|
|
206
|
+
if (override) return override;
|
|
207
|
+
return new URL(_X402Client.DEFAULT_BASE);
|
|
208
|
+
}
|
|
209
|
+
rpcURL() {
|
|
210
|
+
return new URL(`/${this.network}/v2`, this.baseURL()).toString();
|
|
211
|
+
}
|
|
212
|
+
enhancedURL() {
|
|
213
|
+
return new URL(`/${this.network}/nft/v3`, this.baseURL()).toString();
|
|
214
|
+
}
|
|
215
|
+
static SIWE_TTL = "1h";
|
|
216
|
+
static SIWE_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
217
|
+
// refresh 5min before expiry
|
|
218
|
+
async ensureSiweToken() {
|
|
219
|
+
if (this.siweToken) return this.siweToken;
|
|
220
|
+
const cfg = load();
|
|
221
|
+
if (cfg.siwe_token && cfg.siwe_token_expires_at) {
|
|
222
|
+
const expiry = new Date(cfg.siwe_token_expires_at);
|
|
223
|
+
const remaining = expiry.getTime() - Date.now();
|
|
224
|
+
debug(`SIWE: found cached token (length=${cfg.siwe_token.length}, remaining=${Math.round(remaining / 1e3)}s)`);
|
|
225
|
+
if (!Number.isNaN(expiry.getTime()) && remaining > _X402Client.SIWE_EXPIRY_BUFFER_MS) {
|
|
226
|
+
this.siweToken = cfg.siwe_token;
|
|
227
|
+
return this.siweToken;
|
|
228
|
+
}
|
|
229
|
+
debug("SIWE: cached token expired or expiring soon");
|
|
230
|
+
} else {
|
|
231
|
+
debug("SIWE: no cached token in config");
|
|
232
|
+
}
|
|
233
|
+
debug("SIWE: generating fresh token");
|
|
234
|
+
this.siweToken = await signSiwe({
|
|
235
|
+
privateKey: this.privateKey,
|
|
236
|
+
expiresAfter: _X402Client.SIWE_TTL
|
|
237
|
+
});
|
|
238
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1e3).toISOString();
|
|
239
|
+
debug(`SIWE: saving token to config (length=${this.siweToken.length}, expires=${expiresAt})`);
|
|
240
|
+
save({ ...load(), siwe_token: this.siweToken, siwe_token_expires_at: expiresAt });
|
|
241
|
+
return this.siweToken;
|
|
242
|
+
}
|
|
243
|
+
refreshSiweToken() {
|
|
244
|
+
this.siweToken = null;
|
|
245
|
+
const cfg = load();
|
|
246
|
+
save({ ...cfg, siwe_token: void 0, siwe_token_expires_at: void 0 });
|
|
247
|
+
}
|
|
248
|
+
async call(method, params = []) {
|
|
249
|
+
const body = { jsonrpc: "2.0", method, params, id: 1 };
|
|
250
|
+
const jsonBody = JSON.stringify(body);
|
|
251
|
+
const buildInit = (extra) => ({
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": "application/json",
|
|
255
|
+
Accept: "application/json",
|
|
256
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
257
|
+
...extra
|
|
258
|
+
},
|
|
259
|
+
body: jsonBody
|
|
260
|
+
});
|
|
261
|
+
await this.ensureSiweToken();
|
|
262
|
+
let resp = await this.doFetch(this.rpcURL(), buildInit());
|
|
263
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
264
|
+
authRetry: async () => {
|
|
265
|
+
this.refreshSiweToken();
|
|
266
|
+
await this.ensureSiweToken();
|
|
267
|
+
return this.doFetch(this.rpcURL(), buildInit());
|
|
268
|
+
},
|
|
269
|
+
paymentRetry: async (paymentSig) => this.doFetch(this.rpcURL(), buildInit({ "Payment-Signature": paymentSig }))
|
|
270
|
+
});
|
|
271
|
+
if (resp.status === 429) throw errRateLimited();
|
|
272
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
273
|
+
if (!resp.ok) {
|
|
274
|
+
const text = await resp.text().catch(() => "");
|
|
275
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
276
|
+
}
|
|
277
|
+
const rpcResp = await resp.json();
|
|
278
|
+
if (rpcResp.error) {
|
|
279
|
+
throw errRPC(rpcResp.error.code, rpcResp.error.message);
|
|
280
|
+
}
|
|
281
|
+
return rpcResp.result;
|
|
282
|
+
}
|
|
283
|
+
async callEnhanced(path, params) {
|
|
284
|
+
const url = new URL(`${this.enhancedURL()}/${path}`);
|
|
285
|
+
for (const [k, v] of Object.entries(params)) {
|
|
286
|
+
url.searchParams.set(k, v);
|
|
287
|
+
}
|
|
288
|
+
const urlStr = url.toString();
|
|
289
|
+
const buildInit = (extra) => ({
|
|
290
|
+
headers: {
|
|
291
|
+
Accept: "application/json",
|
|
292
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
293
|
+
...extra
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
await this.ensureSiweToken();
|
|
297
|
+
let resp = await this.doFetch(urlStr, buildInit());
|
|
298
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
299
|
+
authRetry: async () => {
|
|
300
|
+
this.refreshSiweToken();
|
|
301
|
+
await this.ensureSiweToken();
|
|
302
|
+
return this.doFetch(urlStr, buildInit());
|
|
303
|
+
},
|
|
304
|
+
paymentRetry: async (paymentSig) => this.doFetch(urlStr, buildInit({ "Payment-Signature": paymentSig }))
|
|
305
|
+
});
|
|
306
|
+
if (resp.status === 429) throw errRateLimited();
|
|
307
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
308
|
+
if (!resp.ok) {
|
|
309
|
+
const text = await resp.text().catch(() => "");
|
|
310
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
311
|
+
}
|
|
312
|
+
return resp.json();
|
|
313
|
+
}
|
|
314
|
+
async callRest(path, options = {}) {
|
|
315
|
+
const base = new URL(`/${path.replace(/^\//, "")}`, this.baseURL());
|
|
316
|
+
if (options.query) {
|
|
317
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
318
|
+
if (v !== void 0 && v !== "") base.searchParams.set(k, v);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const urlStr = base.toString();
|
|
322
|
+
const method = options.method ?? "GET";
|
|
323
|
+
const buildInit = (extra) => ({
|
|
324
|
+
method,
|
|
325
|
+
headers: {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
Accept: "application/json",
|
|
328
|
+
Authorization: `SIWE ${this.siweToken}`,
|
|
329
|
+
...extra
|
|
330
|
+
},
|
|
331
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
332
|
+
});
|
|
333
|
+
await this.ensureSiweToken();
|
|
334
|
+
let resp = await this.doFetch(urlStr, buildInit());
|
|
335
|
+
resp = await this.handleAuthAndPayment(resp, {
|
|
336
|
+
authRetry: async () => {
|
|
337
|
+
this.refreshSiweToken();
|
|
338
|
+
await this.ensureSiweToken();
|
|
339
|
+
return this.doFetch(urlStr, buildInit());
|
|
340
|
+
},
|
|
341
|
+
paymentRetry: async (paymentSig) => this.doFetch(urlStr, buildInit({ "Payment-Signature": paymentSig }))
|
|
342
|
+
});
|
|
343
|
+
if (resp.status === 429) throw errRateLimited();
|
|
344
|
+
if (resp.status === 402) throw await this.parsePaymentError(resp);
|
|
345
|
+
if (!resp.ok) {
|
|
346
|
+
const text = await resp.text().catch(() => "");
|
|
347
|
+
throw errNetwork(`HTTP ${resp.status}: ${text}`);
|
|
348
|
+
}
|
|
349
|
+
return resp.json();
|
|
350
|
+
}
|
|
351
|
+
async doFetch(url, init) {
|
|
352
|
+
return fetchWithTimeout(url, init);
|
|
353
|
+
}
|
|
354
|
+
async parsePaymentError(resp) {
|
|
355
|
+
const text = await resp.text().catch(() => "");
|
|
356
|
+
try {
|
|
357
|
+
const body = JSON.parse(text);
|
|
358
|
+
const reason = body?.extensions?.paymentError?.info?.reason;
|
|
359
|
+
const message = body?.extensions?.paymentError?.info?.message;
|
|
360
|
+
const payer = body?.extensions?.paymentError?.info?.payer;
|
|
361
|
+
if (reason === "insufficient_funds") {
|
|
362
|
+
const network = body?.accepts?.[0]?.network;
|
|
363
|
+
const asset = body?.accepts?.[0]?.extra?.name ?? "USDC";
|
|
364
|
+
const networkLabel = network === "eip155:8453" ? "Base" : network ?? "the payment network";
|
|
365
|
+
return new CLIError(
|
|
366
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
367
|
+
`Insufficient ${asset} balance on ${networkLabel}. ${message ?? ""}`.trim(),
|
|
368
|
+
`Fund wallet ${payer ?? ""} with ${asset} on ${networkLabel} to use x402.`.trim()
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return new CLIError(
|
|
372
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
373
|
+
`x402 payment failed: ${message || body?.error || text}`
|
|
374
|
+
);
|
|
375
|
+
} catch {
|
|
376
|
+
return new CLIError(
|
|
377
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
378
|
+
`x402 payment failed: ${text}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async handleAuthAndPayment(resp, retries) {
|
|
383
|
+
if (resp.status === 401) {
|
|
384
|
+
const detail = await resp.text().catch(() => "");
|
|
385
|
+
if (detail.includes("MESSAGE_EXPIRED")) {
|
|
386
|
+
return retries.authRetry();
|
|
387
|
+
}
|
|
388
|
+
throw new CLIError(
|
|
389
|
+
ErrorCode.AUTH_REQUIRED,
|
|
390
|
+
`x402 authentication failed: ${detail || "unauthorized"}`,
|
|
391
|
+
"Check your wallet key and try again."
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (resp.status === 402) {
|
|
395
|
+
const paymentRequiredHeader = resp.headers.get("payment-required");
|
|
396
|
+
if (!paymentRequiredHeader) {
|
|
397
|
+
throw new CLIError(
|
|
398
|
+
ErrorCode.PAYMENT_REQUIRED,
|
|
399
|
+
"x402 payment required but no Payment-Required header received."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const paymentSignature = await createPayment({
|
|
403
|
+
privateKey: this.privateKey,
|
|
404
|
+
paymentRequiredHeader
|
|
405
|
+
});
|
|
406
|
+
return retries.paymentRetry(paymentSignature);
|
|
407
|
+
}
|
|
408
|
+
return resp;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/lib/admin-client.ts
|
|
413
|
+
var AdminClient = class _AdminClient {
|
|
414
|
+
static get ADMIN_API_HOST() {
|
|
415
|
+
return `admin-api.${getBaseDomain()}`;
|
|
416
|
+
}
|
|
417
|
+
// Test/debug only: used by mock E2E to route admin requests locally.
|
|
418
|
+
static ADMIN_API_BASE_URL_ENV = "ALCHEMY_ADMIN_API_BASE_URL";
|
|
419
|
+
credential;
|
|
420
|
+
constructor(credential) {
|
|
421
|
+
if (typeof credential === "string") {
|
|
422
|
+
this.validateAccessKey(credential);
|
|
423
|
+
this.credential = { type: "access_key", key: credential };
|
|
424
|
+
} else {
|
|
425
|
+
if (credential.type === "access_key") {
|
|
426
|
+
this.validateAccessKey(credential.key);
|
|
427
|
+
} else if (!credential.token.trim()) {
|
|
428
|
+
throw errAuthRequired();
|
|
429
|
+
}
|
|
430
|
+
this.credential = credential;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
baseURL() {
|
|
434
|
+
const override = this.baseURLOverride();
|
|
435
|
+
if (override) return override.toString().replace(/\/$/, "");
|
|
436
|
+
return `https://admin-api.${getBaseDomain()}`;
|
|
437
|
+
}
|
|
438
|
+
allowedHosts() {
|
|
439
|
+
const hosts = /* @__PURE__ */ new Set([_AdminClient.ADMIN_API_HOST]);
|
|
440
|
+
const override = this.baseURLOverride();
|
|
441
|
+
if (override) hosts.add(override.hostname);
|
|
442
|
+
return hosts;
|
|
443
|
+
}
|
|
444
|
+
allowInsecureTransport(hostname) {
|
|
445
|
+
return isLocalhost(hostname);
|
|
446
|
+
}
|
|
447
|
+
baseURLOverride() {
|
|
448
|
+
return parseBaseURLOverride(_AdminClient.ADMIN_API_BASE_URL_ENV);
|
|
449
|
+
}
|
|
450
|
+
validateAccessKey(accessKey) {
|
|
451
|
+
if (!accessKey.trim() || /\s/.test(accessKey)) {
|
|
452
|
+
throw errInvalidAccessKey();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
assertSafeRequestTarget(url) {
|
|
456
|
+
let parsed;
|
|
457
|
+
try {
|
|
458
|
+
parsed = new URL(url);
|
|
459
|
+
} catch {
|
|
460
|
+
throw errInvalidArgs("Invalid admin API URL.");
|
|
461
|
+
}
|
|
462
|
+
if (!this.allowedHosts().has(parsed.hostname)) {
|
|
463
|
+
throw errInvalidArgs(`Refusing to send credentials to unexpected host: ${parsed.hostname}`);
|
|
464
|
+
}
|
|
465
|
+
if (parsed.protocol !== "https:" && !this.allowInsecureTransport(parsed.hostname)) {
|
|
466
|
+
throw errInvalidArgs("Refusing to send credentials over non-HTTPS connection.");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async request(method, path, body) {
|
|
470
|
+
const url = `${this.baseURL()}${path}`;
|
|
471
|
+
debug(`${method} ${url}`);
|
|
472
|
+
this.assertSafeRequestTarget(url);
|
|
473
|
+
const resp = await fetchWithTimeout(url, {
|
|
474
|
+
method,
|
|
475
|
+
redirect: "error",
|
|
476
|
+
headers: {
|
|
477
|
+
Authorization: `Bearer ${this.credential.type === "access_key" ? this.credential.key : this.credential.token}`,
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
Accept: "application/json"
|
|
480
|
+
},
|
|
481
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
482
|
+
});
|
|
483
|
+
if (resp.status === 401) {
|
|
484
|
+
debug(`401 Unauthorized from ${url}`);
|
|
485
|
+
throw errInvalidAccessKey();
|
|
486
|
+
}
|
|
487
|
+
if (resp.status === 403) {
|
|
488
|
+
const detail = await resp.text().catch(() => "");
|
|
489
|
+
let reason;
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(detail);
|
|
492
|
+
reason = parsed?.message || parsed?.error?.message || parsed?.error || void 0;
|
|
493
|
+
} catch {
|
|
494
|
+
reason = detail || void 0;
|
|
495
|
+
}
|
|
496
|
+
throw errAccessDenied(typeof reason === "string" ? reason : void 0);
|
|
497
|
+
}
|
|
498
|
+
if (resp.status === 404) {
|
|
499
|
+
const text = await resp.text().catch(() => "");
|
|
500
|
+
throw errNotFound(text || path);
|
|
501
|
+
}
|
|
502
|
+
if (resp.status === 429) throw errRateLimited();
|
|
503
|
+
if (!resp.ok) {
|
|
504
|
+
const text = await resp.text().catch(() => "");
|
|
505
|
+
throw errAdminAPI(resp.status, text);
|
|
506
|
+
}
|
|
507
|
+
return resp.json();
|
|
508
|
+
}
|
|
509
|
+
async listChains() {
|
|
510
|
+
const result = await this.request("GET", "/v1/chains");
|
|
511
|
+
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;
|
|
512
|
+
if (!Array.isArray(chains)) {
|
|
513
|
+
throw errAdminAPI(200, "Unexpected response shape for /v1/chains.");
|
|
514
|
+
}
|
|
515
|
+
return chains;
|
|
516
|
+
}
|
|
517
|
+
async listApps(opts) {
|
|
518
|
+
const params = new URLSearchParams();
|
|
519
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
520
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
521
|
+
const qs = params.toString();
|
|
522
|
+
const resp = await this.request(
|
|
523
|
+
"GET",
|
|
524
|
+
`/v1/apps${qs ? `?${qs}` : ""}`
|
|
525
|
+
);
|
|
526
|
+
return resp.data;
|
|
527
|
+
}
|
|
528
|
+
async listAllApps(opts) {
|
|
529
|
+
const apps = [];
|
|
530
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
531
|
+
let cursor;
|
|
532
|
+
let pages = 0;
|
|
533
|
+
do {
|
|
534
|
+
const page = await this.listApps({
|
|
535
|
+
...cursor && { cursor },
|
|
536
|
+
...opts?.limit !== void 0 && { limit: opts.limit }
|
|
537
|
+
});
|
|
538
|
+
pages += 1;
|
|
539
|
+
apps.push(...page.apps);
|
|
540
|
+
cursor = page.cursor;
|
|
541
|
+
if (cursor && seenCursors.has(cursor)) break;
|
|
542
|
+
if (cursor) seenCursors.add(cursor);
|
|
543
|
+
} while (cursor);
|
|
544
|
+
return { apps, pages };
|
|
545
|
+
}
|
|
546
|
+
async getApp(id) {
|
|
547
|
+
const resp = await this.request("GET", `/v1/apps/${id}`);
|
|
548
|
+
return resp.data;
|
|
549
|
+
}
|
|
550
|
+
async createApp(opts) {
|
|
551
|
+
const resp = await this.request("POST", "/v1/apps", {
|
|
552
|
+
name: opts.name,
|
|
553
|
+
chainNetworks: opts.networks,
|
|
554
|
+
...opts.description && { description: opts.description },
|
|
555
|
+
...opts.products && { products: opts.products }
|
|
556
|
+
});
|
|
557
|
+
return resp.data;
|
|
558
|
+
}
|
|
559
|
+
async deleteApp(id) {
|
|
560
|
+
await this.request("DELETE", `/v1/apps/${id}`);
|
|
561
|
+
}
|
|
562
|
+
async updateApp(id, opts) {
|
|
563
|
+
const resp = await this.request("PATCH", `/v1/apps/${id}`, opts);
|
|
564
|
+
return resp.data;
|
|
565
|
+
}
|
|
566
|
+
async updateNetworkAllowlist(id, networks) {
|
|
567
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/networks`, {
|
|
568
|
+
chainNetworks: networks
|
|
569
|
+
});
|
|
570
|
+
return resp.data;
|
|
571
|
+
}
|
|
572
|
+
async updateAddressAllowlist(id, addresses) {
|
|
573
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/address-allowlist`, {
|
|
574
|
+
addressAllowlist: addresses
|
|
575
|
+
});
|
|
576
|
+
return resp.data;
|
|
577
|
+
}
|
|
578
|
+
async updateOriginAllowlist(id, origins) {
|
|
579
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/origin-allowlist`, {
|
|
580
|
+
originAllowlist: origins
|
|
581
|
+
});
|
|
582
|
+
return resp.data;
|
|
583
|
+
}
|
|
584
|
+
async updateIpAllowlist(id, ips) {
|
|
585
|
+
const resp = await this.request("PUT", `/v1/apps/${id}/ip-allowlist`, {
|
|
586
|
+
ipAllowlist: ips
|
|
587
|
+
});
|
|
588
|
+
return resp.data;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/lib/resolve.ts
|
|
593
|
+
function resolveAPIKey(program, cfg) {
|
|
594
|
+
const opts = program.opts();
|
|
595
|
+
if (opts.apiKey) return opts.apiKey;
|
|
596
|
+
if (process.env.ALCHEMY_API_KEY) return process.env.ALCHEMY_API_KEY;
|
|
597
|
+
const config = cfg ?? load();
|
|
598
|
+
if (config.api_key) return config.api_key;
|
|
599
|
+
if (config.app?.apiKey) return config.app.apiKey;
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
function resolveAccessKey(program, cfg) {
|
|
603
|
+
const opts = program.opts();
|
|
604
|
+
if (opts.accessKey) return opts.accessKey;
|
|
605
|
+
if (process.env.ALCHEMY_ACCESS_KEY) return process.env.ALCHEMY_ACCESS_KEY;
|
|
606
|
+
const config = cfg ?? load();
|
|
607
|
+
if (config.access_key) return config.access_key;
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
function resolveNetwork(program, cfg, defaultNetwork) {
|
|
611
|
+
const opts = program.opts();
|
|
612
|
+
if (opts.network) return opts.network;
|
|
613
|
+
if (process.env.ALCHEMY_NETWORK) return process.env.ALCHEMY_NETWORK;
|
|
614
|
+
const config = cfg ?? load();
|
|
615
|
+
if (config.network) return config.network;
|
|
616
|
+
return defaultNetwork ?? "eth-mainnet";
|
|
617
|
+
}
|
|
618
|
+
function resolveAppId(program, cfg) {
|
|
619
|
+
const opts = program.opts();
|
|
620
|
+
if (opts.appId) return opts.appId;
|
|
621
|
+
const config = cfg ?? load();
|
|
622
|
+
if (config.app?.id) return config.app.id;
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
function resolveAuthToken(cfg) {
|
|
626
|
+
const config = cfg ?? load();
|
|
627
|
+
if (!config.auth_token?.trim()) return void 0;
|
|
628
|
+
if (config.auth_token_expires_at) {
|
|
629
|
+
const expiry = new Date(config.auth_token_expires_at);
|
|
630
|
+
if (!Number.isNaN(expiry.getTime()) && expiry <= /* @__PURE__ */ new Date()) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return config.auth_token;
|
|
635
|
+
}
|
|
636
|
+
function adminClientFromFlags(program) {
|
|
637
|
+
const cfg = load();
|
|
638
|
+
const accessKey = resolveAccessKey(program, cfg);
|
|
639
|
+
if (accessKey) return new AdminClient(accessKey);
|
|
640
|
+
const authToken = resolveAuthToken(cfg);
|
|
641
|
+
if (authToken) return new AdminClient({ type: "auth_token", token: authToken });
|
|
642
|
+
throw errAccessKeyRequired();
|
|
643
|
+
}
|
|
644
|
+
function resolveX402(program, cfg) {
|
|
645
|
+
const opts = program.opts();
|
|
646
|
+
if (opts.x402) return true;
|
|
647
|
+
const config = cfg ?? load();
|
|
648
|
+
return config.x402 === true;
|
|
649
|
+
}
|
|
650
|
+
function resolveX402Client(program) {
|
|
651
|
+
const cfg = load();
|
|
652
|
+
if (!resolveX402(program, cfg)) return null;
|
|
653
|
+
const walletKey = resolveWalletKey(program, cfg);
|
|
654
|
+
if (!walletKey) return null;
|
|
655
|
+
return new X402Client(walletKey, resolveNetwork(program, cfg));
|
|
656
|
+
}
|
|
657
|
+
function resolveWalletKey(program, cfg) {
|
|
658
|
+
const opts = program.opts();
|
|
659
|
+
if (opts.walletKeyFile) {
|
|
660
|
+
return readFileSync(opts.walletKeyFile, "utf-8").trim();
|
|
661
|
+
}
|
|
662
|
+
if (process.env.ALCHEMY_WALLET_KEY) {
|
|
663
|
+
return process.env.ALCHEMY_WALLET_KEY;
|
|
664
|
+
}
|
|
665
|
+
const config = cfg ?? load();
|
|
666
|
+
if (config.wallet_key_file) {
|
|
667
|
+
return readFileSync(config.wallet_key_file, "utf-8").trim();
|
|
668
|
+
}
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
function clientFromFlags(program, opts) {
|
|
672
|
+
const cfg = load();
|
|
673
|
+
const network = resolveNetwork(program, cfg, opts?.defaultNetwork);
|
|
674
|
+
debug(`using network=${network}`);
|
|
675
|
+
const programOpts = program.opts();
|
|
676
|
+
if (programOpts.accessKey) {
|
|
677
|
+
throw errInvalidArgs(
|
|
678
|
+
"--access-key is for admin commands (apps, chains, webhooks). Use --api-key for RPC commands."
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
if (resolveX402(program, cfg)) {
|
|
682
|
+
const walletKey = resolveWalletKey(program, cfg);
|
|
683
|
+
if (!walletKey) throw errWalletKeyRequired();
|
|
684
|
+
return new X402Client(walletKey, network);
|
|
685
|
+
}
|
|
686
|
+
const apiKey = resolveAPIKey(program, cfg);
|
|
687
|
+
if (!apiKey) throw errAuthRequired();
|
|
688
|
+
return new Client(apiKey, network);
|
|
689
|
+
}
|
|
690
|
+
function appNetworkToSlug(rpcUrl) {
|
|
691
|
+
let parsed;
|
|
692
|
+
try {
|
|
693
|
+
parsed = new URL(rpcUrl);
|
|
694
|
+
} catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
const suffix = `.g.${getBaseDomain()}`;
|
|
698
|
+
if (!parsed.hostname.endsWith(suffix)) return null;
|
|
699
|
+
const slug = parsed.hostname.slice(0, -suffix.length);
|
|
700
|
+
return slug || null;
|
|
701
|
+
}
|
|
702
|
+
async function resolveConfiguredNetworkSlugs(program, appIdOverride) {
|
|
703
|
+
const appId = appIdOverride || resolveAppId(program);
|
|
704
|
+
if (!appId) throw errAppRequired();
|
|
705
|
+
const admin = adminClientFromFlags(program);
|
|
706
|
+
const app = await admin.getApp(appId);
|
|
707
|
+
const slugs = app.chainNetworks.map((network) => appNetworkToSlug(network.rpcUrl)).filter((slug) => Boolean(slug));
|
|
708
|
+
return Array.from(new Set(slugs)).sort((a, b) => a.localeCompare(b));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export {
|
|
712
|
+
AdminClient,
|
|
713
|
+
resolveAPIKey,
|
|
714
|
+
resolveAccessKey,
|
|
715
|
+
resolveNetwork,
|
|
716
|
+
resolveAppId,
|
|
717
|
+
resolveAuthToken,
|
|
718
|
+
adminClientFromFlags,
|
|
719
|
+
resolveX402,
|
|
720
|
+
resolveX402Client,
|
|
721
|
+
resolveWalletKey,
|
|
722
|
+
clientFromFlags,
|
|
723
|
+
resolveConfiguredNetworkSlugs
|
|
724
|
+
};
|