|
| 1 | +/** |
| 2 | + * Interactive auth-method picker shared by `zerion init` and `zerion login`. |
| 3 | + * |
| 4 | + * Presents the currently available ways to authenticate — browser login first |
| 5 | + * (the modern default, like Claude Code), then pasting an existing API key, |
| 6 | + * then a pointer to pay-per-call (x402 / MPP) which needs no API key. Requires |
| 7 | + * a raw-mode TTY; callers guard on `process.stdin.isTTY` before invoking. |
| 8 | + */ |
| 9 | + |
| 10 | +import { selectOne, BACK } from "../common/select.js"; |
| 11 | +import { readSecret } from "../common/prompt.js"; |
| 12 | +import { setConfigValue } from "../config.js"; |
| 13 | +import { DASHBOARD_URL } from "../common/constants.js"; |
| 14 | +import { authenticateWithBrowser } from "./oauth.js"; |
| 15 | + |
| 16 | +const METHODS = [ |
| 17 | + { id: "oauth", label: "Authenticate with the Zerion dashboard (opens browser)" }, |
| 18 | + { id: "paste", label: "Paste an existing API key" }, |
| 19 | + { id: "payg", label: "Use pay-per-call instead (x402 / MPP — no API key)" }, |
| 20 | +]; |
| 21 | + |
| 22 | +/** |
| 23 | + * Show the auth-method menu. Returns the chosen method id, or null if the user |
| 24 | + * backed out (Esc). |
| 25 | + * @param {{ includePayg?: boolean }} [opts] |
| 26 | + * @returns {Promise<"oauth" | "paste" | "payg" | null>} |
| 27 | + */ |
| 28 | +export async function selectAuthMethod({ includePayg = true } = {}) { |
| 29 | + const methods = includePayg ? METHODS : METHODS.filter((m) => m.id !== "payg"); |
| 30 | + const idx = await selectOne( |
| 31 | + "How would you like to authenticate?", |
| 32 | + methods.map((m) => m.label), |
| 33 | + { defaultIndex: 0 } |
| 34 | + ); |
| 35 | + if (idx === BACK) return null; |
| 36 | + return methods[idx].id; |
| 37 | +} |
| 38 | + |
| 39 | +function printPaygGuidance(log) { |
| 40 | + log(""); |
| 41 | + log(" Pay-per-call needs no API key — set a private key and pass --x402 or --mpp:"); |
| 42 | + log(" export WALLET_PRIVATE_KEY=0x... # x402 on Base (EVM) or MPP on Tempo"); |
| 43 | + log(" export WALLET_PRIVATE_KEY=<base58> # x402 on Solana"); |
| 44 | + log(" zerion portfolio <address> --x402 # or --mpp"); |
| 45 | + log(" Note: pay-per-call covers analytics only; trading needs an API key."); |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * Run the interactive auth setup: pick a method and execute it, persisting the |
| 50 | + * API key to config on success. Never throws for expected outcomes (denied / |
| 51 | + * timeout / cancel / skip) — returns a structured result so callers decide how |
| 52 | + * loud to be. |
| 53 | + * |
| 54 | + * @param {{ |
| 55 | + * log?: (line?: string) => void, |
| 56 | + * open?: boolean, |
| 57 | + * dashboardUrl?: string, |
| 58 | + * includePayg?: boolean, |
| 59 | + * }} [opts] |
| 60 | + * @returns {Promise<{ ok: boolean, method?: string, skipped?: boolean, reason?: string, message?: string }>} |
| 61 | + */ |
| 62 | +export async function runInteractiveAuth({ |
| 63 | + log = (line = "") => process.stderr.write(line + "\n"), |
| 64 | + open = true, |
| 65 | + dashboardUrl = DASHBOARD_URL, |
| 66 | + includePayg = true, |
| 67 | +} = {}) { |
| 68 | + const method = await selectAuthMethod({ includePayg }); |
| 69 | + |
| 70 | + if (method === null) { |
| 71 | + log(" ! Cancelled — no changes made."); |
| 72 | + return { ok: true, skipped: true, reason: "user_cancelled" }; |
| 73 | + } |
| 74 | + |
| 75 | + if (method === "oauth") { |
| 76 | + try { |
| 77 | + const { apiKey } = await authenticateWithBrowser({ dashboardUrl, open, log }); |
| 78 | + setConfigValue("apiKey", apiKey); |
| 79 | + log(" ✓ Authenticated — API key saved to config"); |
| 80 | + return { ok: true, method: "oauth" }; |
| 81 | + } catch (err) { |
| 82 | + log(` ! Browser authorization failed: ${err.message}`); |
| 83 | + return { ok: false, method: "oauth", reason: err.code || "oauth_failed", message: err.message }; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + if (method === "paste") { |
| 88 | + const key = await readSecret(" Paste your API key (or press Enter to skip): ", { mask: true }); |
| 89 | + if (!key) { |
| 90 | + log(" ! Skipped — set later with: zerion config set apiKey <your-key>"); |
| 91 | + return { ok: true, skipped: true, method: "paste", reason: "user_skipped" }; |
| 92 | + } |
| 93 | + if (!key.startsWith("zk_")) { |
| 94 | + log(` ! Warning: keys typically start with "zk_". Saving anyway.`); |
| 95 | + } |
| 96 | + setConfigValue("apiKey", key); |
| 97 | + log(" ✓ API key saved to config"); |
| 98 | + return { ok: true, method: "paste" }; |
| 99 | + } |
| 100 | + |
| 101 | + // payg — informational only; nothing is persisted. |
| 102 | + printPaygGuidance(log); |
| 103 | + return { ok: true, skipped: true, method: "payg", reason: "pay_per_call" }; |
| 104 | +} |
0 commit comments