@alchemy/cli 0.5.1 → 0.6.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/README.md +25 -74
- package/dist/{auth-7E33EMAI.js → auth-QB3BA7AN.js} +7 -3
- package/dist/{auth-E26YCAJV.js → auth-S4DTOWW3.js} +7 -5
- package/dist/{chunk-Z7J64GJJ.js → chunk-3W4ICF67.js} +2 -2
- package/dist/chunk-ATX65U7J.js +737 -0
- package/dist/chunk-BAAQ7ELR.js +143 -0
- package/dist/{chunk-IGD4NIK7.js → chunk-FFMNT74F.js} +54 -36
- package/dist/chunk-JQRGILIS.js +53 -0
- package/dist/chunk-KDMIWPZH.js +27 -0
- package/dist/chunk-NBDWF4ZQ.js +554 -0
- package/dist/{chunk-5X6YRTPU.js → chunk-T5Z2GJUX.js} +7 -5
- package/dist/{chunk-LYUW7O6X.js → chunk-UMKDYHMO.js} +113 -37
- package/dist/credential-storage-T6FFW7DG.js +14 -0
- package/dist/index.js +726 -44
- package/dist/{interactive-G4ON47AR.js → interactive-OM476LBG.js} +11 -6
- package/dist/onboarding-S3GAP4OV.js +61 -0
- package/dist/resolve-HXKHDOJZ.js +31 -0
- package/package.json +2 -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,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
|
+
};
|
|
@@ -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: #
|
|
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: #
|
|
38
|
+
color: #fff;
|
|
39
39
|
font-size: 0.875rem;
|
|
40
40
|
}
|
|
41
41
|
`;
|
|
@@ -130,7 +130,7 @@ ${SHARED_STYLE}
|
|
|
130
130
|
// src/lib/auth.ts
|
|
131
131
|
var AUTH_PORT = 16424;
|
|
132
132
|
var AUTH_CALLBACK_PATH = "/callback";
|
|
133
|
-
var
|
|
133
|
+
var OAUTH_CLIENT_ID = "alchemy-cli";
|
|
134
134
|
function getAuthBaseUrl() {
|
|
135
135
|
return process.env.ALCHEMY_AUTH_URL || `https://auth.${getBaseDomain()}`;
|
|
136
136
|
}
|
|
@@ -140,14 +140,29 @@ function generateCodeVerifier() {
|
|
|
140
140
|
function deriveCodeChallenge(verifier) {
|
|
141
141
|
return createHash("sha256").update(verifier).digest("base64url");
|
|
142
142
|
}
|
|
143
|
-
function
|
|
143
|
+
function generateState() {
|
|
144
|
+
return randomBytes(32).toString("base64url");
|
|
145
|
+
}
|
|
146
|
+
function getAuthorizeUrl(port, codeChallenge, state) {
|
|
144
147
|
const base = getAuthBaseUrl();
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
const url = new URL(`${base}/oauth/authorize`);
|
|
149
|
+
url.searchParams.set("response_type", "code");
|
|
150
|
+
url.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
151
|
+
url.searchParams.set("redirect_uri", `http://localhost:${port}${AUTH_CALLBACK_PATH}`);
|
|
152
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
153
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
154
|
+
url.searchParams.set("state", state);
|
|
155
|
+
return url.toString();
|
|
156
|
+
}
|
|
157
|
+
function prepareBrowserLogin(port = AUTH_PORT) {
|
|
158
|
+
const codeVerifier = generateCodeVerifier();
|
|
159
|
+
const codeChallenge = deriveCodeChallenge(codeVerifier);
|
|
160
|
+
const state = generateState();
|
|
161
|
+
return {
|
|
162
|
+
authorizeUrl: getAuthorizeUrl(port, codeChallenge, state),
|
|
163
|
+
codeVerifier,
|
|
164
|
+
state
|
|
165
|
+
};
|
|
151
166
|
}
|
|
152
167
|
function openBrowser(url) {
|
|
153
168
|
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
@@ -188,6 +203,7 @@ function waitForCallback(port, timeoutMs = 12e4) {
|
|
|
188
203
|
clearTimeout(timer);
|
|
189
204
|
resolve({
|
|
190
205
|
code,
|
|
206
|
+
state: url.searchParams.get("state"),
|
|
191
207
|
sendSuccess: () => {
|
|
192
208
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
193
209
|
res.end(AUTH_SUCCESS_HTML);
|
|
@@ -220,43 +236,44 @@ function waitForCallback(port, timeoutMs = 12e4) {
|
|
|
220
236
|
async function exchangeCodeForToken(code, port, options) {
|
|
221
237
|
const baseUrl = getAuthBaseUrl();
|
|
222
238
|
const redirectUri = `http://localhost:${port}${AUTH_CALLBACK_PATH}`;
|
|
223
|
-
const body = {
|
|
239
|
+
const body = new URLSearchParams({
|
|
240
|
+
grant_type: "authorization_code",
|
|
224
241
|
code,
|
|
225
|
-
redirect_uri: redirectUri
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
if (options?.codeVerifier) {
|
|
231
|
-
body.code_verifier = options.codeVerifier;
|
|
232
|
-
}
|
|
233
|
-
const response = await fetch(`${baseUrl}/api/cli/token`, {
|
|
242
|
+
redirect_uri: redirectUri,
|
|
243
|
+
client_id: OAUTH_CLIENT_ID,
|
|
244
|
+
code_verifier: options.codeVerifier
|
|
245
|
+
});
|
|
246
|
+
const response = await fetch(`${baseUrl}/oauth/token`, {
|
|
234
247
|
method: "POST",
|
|
235
|
-
headers: { "Content-Type": "application/
|
|
236
|
-
body:
|
|
248
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
249
|
+
body: body.toString()
|
|
237
250
|
});
|
|
238
251
|
if (!response.ok) {
|
|
239
252
|
const errBody = await response.json().catch(() => ({}));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
253
|
+
const errMsg = errBody.error_description || errBody.error || `Token exchange failed (HTTP ${response.status})`;
|
|
254
|
+
throw new Error(errMsg);
|
|
243
255
|
}
|
|
244
256
|
const data = await response.json();
|
|
245
|
-
if (!data.
|
|
246
|
-
throw new Error("Token exchange response missing
|
|
257
|
+
if (!data.access_token) {
|
|
258
|
+
throw new Error("Token exchange response missing access_token");
|
|
247
259
|
}
|
|
248
|
-
|
|
260
|
+
const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
261
|
+
return { token: data.access_token, expiresAt };
|
|
249
262
|
}
|
|
250
|
-
async function performBrowserLogin(
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const loginUrl = getLoginUrl(port, codeChallenge);
|
|
263
|
+
async function performBrowserLogin(prepared, options) {
|
|
264
|
+
const port = options?.port ?? AUTH_PORT;
|
|
265
|
+
const { authorizeUrl, codeVerifier, state } = prepared ?? prepareBrowserLogin(port);
|
|
254
266
|
const callbackPromise = waitForCallback(port);
|
|
255
|
-
|
|
267
|
+
if (!options?.skipBrowserOpen) {
|
|
268
|
+
openBrowser(authorizeUrl);
|
|
269
|
+
}
|
|
256
270
|
const callback = await callbackPromise;
|
|
271
|
+
if (callback.state !== state) {
|
|
272
|
+
callback.sendError("State mismatch \u2014 possible CSRF attack.");
|
|
273
|
+
throw new Error("OAuth state mismatch. Authentication aborted.");
|
|
274
|
+
}
|
|
257
275
|
try {
|
|
258
276
|
const result = await exchangeCodeForToken(callback.code, port, {
|
|
259
|
-
expiresInSeconds: options?.expiresInSeconds ?? DEFAULT_EXPIRES_IN_SECONDS,
|
|
260
277
|
codeVerifier
|
|
261
278
|
});
|
|
262
279
|
callback.sendSuccess();
|
|
@@ -290,8 +307,9 @@ async function revokeToken(token) {
|
|
|
290
307
|
|
|
291
308
|
export {
|
|
292
309
|
AUTH_PORT,
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
OAUTH_CLIENT_ID,
|
|
311
|
+
getAuthorizeUrl,
|
|
312
|
+
prepareBrowserLogin,
|
|
295
313
|
openBrowser,
|
|
296
314
|
waitForCallback,
|
|
297
315
|
exchangeCodeForToken,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
|
|
3
|
+
|
|
4
|
+
// src/lib/credential-storage.ts
|
|
5
|
+
import {
|
|
6
|
+
getPassword,
|
|
7
|
+
setPassword,
|
|
8
|
+
deletePassword,
|
|
9
|
+
getKeyring,
|
|
10
|
+
PasswordDeleteError
|
|
11
|
+
} from "cross-keychain";
|
|
12
|
+
var SERVICE = "alchemy-cli";
|
|
13
|
+
var ACCOUNT = "oauth-credentials";
|
|
14
|
+
async function getCredentials() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await getPassword(SERVICE, ACCOUNT);
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (parsed && typeof parsed.auth_token === "string" && typeof parsed.auth_token_expires_at === "string") {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function saveCredentials(creds) {
|
|
28
|
+
await setPassword(SERVICE, ACCOUNT, JSON.stringify(creds));
|
|
29
|
+
}
|
|
30
|
+
async function deleteCredentials() {
|
|
31
|
+
try {
|
|
32
|
+
await deletePassword(SERVICE, ACCOUNT);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (!(err instanceof PasswordDeleteError)) {
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function getStorageBackend() {
|
|
40
|
+
try {
|
|
41
|
+
const keyring = await getKeyring();
|
|
42
|
+
return keyring.name;
|
|
43
|
+
} catch {
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
getCredentials,
|
|
50
|
+
saveCredentials,
|
|
51
|
+
deleteCredentials,
|
|
52
|
+
getStorageBackend
|
|
53
|
+
};
|
|
@@ -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
|
+
};
|