get your claude code tokens here
at main 12 kB view raw
1#!/usr/bin/env node 2 3import { createServer } from "node:http"; 4import express from "express"; 5import fetch from "node-fetch"; 6import open from "open"; 7import { 8 bootstrapFromDisk, 9 exchangeRefreshToken, 10 loadFromDisk, 11 saveToDisk, 12} from "./lib/token"; 13 14const PORT = Number(process.env.PORT || 8787); 15 16function json(res: express.Response, data: unknown, status = 200) { 17 res.status(status).json(data); 18} 19 20const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; 21 22function authorizeUrl(verifier: string, challenge: string) { 23 const u = new URL("https://claude.ai/oauth/authorize"); 24 u.searchParams.set("response_type", "code"); 25 u.searchParams.set("client_id", CLIENT_ID); 26 u.searchParams.set( 27 "redirect_uri", 28 "https://console.anthropic.com/oauth/code/callback", 29 ); 30 u.searchParams.set("scope", "org:create_api_key user:profile user:inference"); 31 u.searchParams.set("code_challenge", challenge); 32 u.searchParams.set("code_challenge_method", "S256"); 33 u.searchParams.set("state", verifier); 34 return u.toString(); 35} 36 37function base64url(input: ArrayBuffer | Uint8Array) { 38 const buf = input instanceof Uint8Array ? input : new Uint8Array(input); 39 return Buffer.from(buf) 40 .toString("base64") 41 .replace(/=/g, "") 42 .replace(/\+/g, "-") 43 .replace(/\//g, "_"); 44} 45 46async function pkcePair() { 47 const bytes = crypto.getRandomValues(new Uint8Array(32)); 48 const verifier = base64url(bytes); 49 const digest = await crypto.subtle.digest( 50 "SHA-256", 51 new TextEncoder().encode(verifier), 52 ); 53 const challenge = base64url(digest as ArrayBuffer); 54 return { verifier, challenge }; 55} 56 57function cleanPastedCode(input: string) { 58 let v = input.trim(); 59 v = v.replace(/^code\s*[:=]\s*/i, ""); 60 v = v.replace(/^["'`]/, "").replace(/["'`]$/, ""); 61 const m = v.match(/[A-Za-z0-9._~-]+(?:#[A-Za-z0-9._~-]+)?/); 62 if (m) return m[0]; 63 return v; 64} 65 66async function exchangeAuthorizationCode(code: string, verifier: string) { 67 const cleaned = cleanPastedCode(code); 68 const [pure, state = ""] = cleaned.split("#"); 69 const body = { 70 code: pure ?? "", 71 state: state ?? "", 72 grant_type: "authorization_code", 73 client_id: CLIENT_ID, 74 redirect_uri: "https://console.anthropic.com/oauth/code/callback", 75 code_verifier: verifier, 76 } as Record<string, string>; 77 const res = await fetch("https://console.anthropic.com/v1/oauth/token", { 78 method: "POST", 79 headers: { 80 "content-type": "application/json", 81 "user-agent": "CRUSH/1.0", 82 }, 83 body: JSON.stringify(body), 84 }); 85 if (!res.ok) throw new Error(`code exchange failed: ${res.status}`); 86 return (await res.json()) as { 87 access_token: string; 88 refresh_token: string; 89 expires_in: number; 90 }; 91} 92 93// Try to bootstrap from disk and exit if successful 94const didBootstrap = await bootstrapFromDisk(); 95 96const argv = process.argv.slice(2); 97if (argv.includes("-h") || argv.includes("--help")) { 98 console.log(`Usage: anthropic\n`); 99 console.log( 100 ` anthropic Start UI and flow; prints token on success and exits.`, 101 ); 102 console.log(` PORT=xxxx anthropic Override port (default 8787).`); 103 console.log( 104 `\nTokens are cached at ~/.config/crush/anthropic and reused on later runs.\n`, 105 ); 106 process.exit(0); 107} 108 109const indexHtml = ` 110 <!doctype html> 111 <html> 112 <head> 113 <meta charset="utf-8" /> 114 <meta name="viewport" content="width=device-width, initial-scale=1" /> 115 <title>Anthropic Auth</title> 116 <style> 117 body { 118 font-family: 119 system-ui, 120 -apple-system, 121 Segoe UI, 122 Roboto, 123 Ubuntu, 124 Cantarell, 125 Noto Sans, 126 sans-serif; 127 background: #0f0f10; 128 color: #fff; 129 margin: 0; 130 display: flex; 131 min-height: 100vh; 132 align-items: center; 133 justify-content: center; 134 } 135 .card { 136 background: #1a1a1b; 137 border: 1px solid #2b2b2c; 138 border-radius: 14px; 139 padding: 28px; 140 max-width: 560px; 141 width: 100%; 142 } 143 h1 { 144 margin: 0 0 8px; 145 } 146 p { 147 color: #9aa0a6; 148 } 149 button, 150 a.button { 151 background: linear-gradient(135deg, #ff6b35, #ff8e53); 152 color: #fff; 153 border: none; 154 border-radius: 10px; 155 padding: 12px 16px; 156 font-weight: 600; 157 cursor: pointer; 158 text-decoration: none; 159 display: inline-block; 160 } 161 textarea { 162 width: 100%; 163 min-height: 120px; 164 background: #111; 165 border: 1px solid #2b2b2c; 166 border-radius: 10px; 167 color: #fff; 168 padding: 10px; 169 } 170 .row { 171 margin: 16px 0; 172 } 173 .muted { 174 color: #9aa0a6; 175 } 176 .status { 177 margin-top: 8px; 178 font-size: 14px; 179 } 180 </style> 181 <script type="module" crossorigin src="../anthropic-api-key/index-9f070n0a.js"></script></head> 182 <body> 183 <div class="card"> 184 <h1>Anthropic Authentication</h1> 185 <p class="muted"> 186 Start the OAuth flow, authorize in the new tab, then paste the 187 returned token here. 188 </p> 189 190 <div class="row"> 191 <a 192 id="authlink" 193 class="button" 194 href="#" 195 target="_blank" 196 style="display: none" 197 >Open Anthropic Authorization</a 198 > 199 </div> 200 201 <div class="row"> 202 <label for="code">Authorization code</label> 203 <textarea 204 id="code" 205 placeholder="Paste the exact code shown by Anthropic (not a URL). If it includes a #, keep the part after it too." 206 ></textarea> 207 </div> 208 209 <div class="row"> 210 <button id="complete">Complete Authentication</button> 211 </div> 212 213 <div id="status" class="status"></div> 214 </div> 215 216 <script> 217 let verifier = ""; 218 const statusEl = document.getElementById("status"); 219 220 function setStatus(msg, ok) { 221 statusEl.textContent = msg; 222 statusEl.style.color = ok ? "#34a853" : "#ea4335"; 223 } 224 225 (async () => { 226 setStatus("Preparing authorization...", true); 227 const res = await fetch("/api/auth/start", { method: "POST" }); 228 if (!res.ok) { 229 setStatus("Failed to prepare auth", false); 230 return; 231 } 232 const data = await res.json(); 233 verifier = data.verifier; 234 const a = document.getElementById("authlink"); 235 a.href = data.authUrl; 236 a.style.display = "inline-block"; 237 setStatus( 238 'Ready. Click "Open Authorization" to continue.', 239 true, 240 ); 241 })(); 242 243 const completeBtn = document.getElementById("complete"); 244 document 245 .getElementById("complete") 246 .addEventListener("click", async () => { 247 if (completeBtn.disabled) return; 248 completeBtn.disabled = true; 249 const code = document.getElementById("code").value.trim(); 250 if (!code || !verifier) { 251 setStatus( 252 "Missing code or verifier. Click Start first.", 253 false, 254 ); 255 completeBtn.disabled = false; 256 return; 257 } 258 const res = await fetch("/api/auth/complete", { 259 method: "POST", 260 headers: { "content-type": "application/json" }, 261 body: JSON.stringify({ code, verifier }), 262 }); 263 if (!res.ok) { 264 setStatus("Code exchange failed", false); 265 completeBtn.disabled = false; 266 return; 267 } 268 setStatus("Authenticated! Fetching token...", true); 269 const t = await fetch("/api/token"); 270 if (!t.ok) { 271 setStatus("Could not fetch token", false); 272 completeBtn.disabled = false; 273 return; 274 } 275 const tok = await t.json(); 276 setStatus( 277 "Access token acquired (expires " + 278 new Date(tok.expiresAt * 1000).toLocaleString() + 279 ")", 280 true, 281 ); 282 setTimeout(() => { 283 try { 284 window.close(); 285 } catch {} 286 }, 500); 287 }); 288 </script> 289 </body> 290 </html> 291`; 292 293if (!didBootstrap) { 294 // Only start the server and open the browser if we didn't bootstrap from disk 295 const memory = new Map< 296 string, 297 { accessToken: string; refreshToken: string; expiresAt: number } 298 >(); 299 const app = express(); 300 app.use(express.json()); 301 302 app.post("/api/auth/start", async (_req, res) => { 303 const { verifier, challenge } = await pkcePair(); 304 const authUrl = authorizeUrl(verifier, challenge); 305 json(res, { authUrl, verifier }); 306 }); 307 308 app.post("/api/auth/complete", async (req, res) => { 309 const body = req.body as { code?: string; verifier?: string }; 310 const code = String(body.code ?? ""); 311 const verifier = String(body.verifier ?? ""); 312 if (!code || !verifier) 313 return json(res, { error: "missing code or verifier" }, 400); 314 const tokens = await exchangeAuthorizationCode(code, verifier); 315 const expiresAt = Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0); 316 const entry = { 317 accessToken: tokens.access_token, 318 refreshToken: tokens.refresh_token, 319 expiresAt, 320 }; 321 memory.set("tokens", entry); 322 await saveToDisk(entry); 323 console.log(`${entry.accessToken}\n`); 324 setTimeout(() => process.exit(0), 100); 325 json(res, { ok: true }); 326 }); 327 328 app.get("/api/token", async (_req, res) => { 329 let entry = memory.get("tokens"); 330 if (!entry) { 331 const disk = await loadFromDisk(); 332 if (disk) { 333 entry = disk; 334 memory.set("tokens", entry); 335 } 336 } 337 if (!entry) return json(res, { error: "not_authenticated" }, 401); 338 const now = Math.floor(Date.now() / 1000); 339 if (now >= entry.expiresAt - 60) { 340 const refreshed = await exchangeRefreshToken(entry.refreshToken); 341 entry.accessToken = refreshed.access_token; 342 entry.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in; 343 if (refreshed.refresh_token) entry.refreshToken = refreshed.refresh_token; 344 memory.set("tokens", entry); 345 await saveToDisk(entry); 346 } 347 json(res, { 348 accessToken: entry.accessToken, 349 expiresAt: entry.expiresAt, 350 }); 351 }); 352 353 app.get("/", (_req, res) => { 354 res.setHeader("content-type", "text/html; charset=utf-8"); 355 res.send(indexHtml); 356 }); 357 358 app.use((_req, res) => { 359 res.status(404).send("something went wrong and your request fell through"); 360 }); 361 362 const server = createServer(app); 363 server.listen(PORT, async () => { 364 const url = `http://localhost:${PORT}`; 365 await open(url); 366 }); 367}