get your claude code tokens here
1#!/usr/bin/env bun 2 3import { serve } from "bun"; 4 5const PORT = Number(Bun.env.PORT || 8787); 6const ROOT = new URL("../", import.meta.url).pathname; 7const PUBLIC_DIR = `${ROOT}public`; 8 9function notFound() { 10 return new Response("Not found", { status: 404 }); 11} 12 13async function serveStatic(pathname: string) { 14 const filePath = PUBLIC_DIR + (pathname === "/" ? "/index.html" : pathname); 15 try { 16 const file = Bun.file(filePath); 17 if (!(await file.exists())) return null; 18 return new Response(file); 19 } catch { 20 return null; 21 } 22} 23 24function json(data: unknown, init: ResponseInit = {}) { 25 return new Response(JSON.stringify(data), { 26 headers: { "content-type": "application/json", ...(init.headers || {}) }, 27 ...init, 28 }); 29} 30 31function authorizeUrl(verifier: string, challenge: string) { 32 const u = new URL("https://claude.ai/oauth/authorize"); 33 u.searchParams.set("response_type", "code"); 34 u.searchParams.set("client_id", CLIENT_ID); 35 u.searchParams.set( 36 "redirect_uri", 37 "https://console.anthropic.com/oauth/code/callback", 38 ); 39 u.searchParams.set("scope", "org:create_api_key user:profile user:inference"); 40 u.searchParams.set("code_challenge", challenge); 41 u.searchParams.set("code_challenge_method", "S256"); 42 u.searchParams.set("state", verifier); 43 return u.toString(); 44} 45 46function base64url(input: ArrayBuffer | Uint8Array) { 47 const buf = input instanceof Uint8Array ? input : new Uint8Array(input); 48 return Buffer.from(buf) 49 .toString("base64") 50 .replace(/=/g, "") 51 .replace(/\+/g, "-") 52 .replace(/\//g, "_"); 53} 54 55async function pkcePair() { 56 const bytes = crypto.getRandomValues(new Uint8Array(32)); 57 const verifier = base64url(bytes); 58 const digest = await crypto.subtle.digest( 59 "SHA-256", 60 new TextEncoder().encode(verifier), 61 ); 62 const challenge = base64url(digest as ArrayBuffer); 63 return { verifier, challenge }; 64} 65 66const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; 67 68async function exchangeRefreshToken(refreshToken: string) { 69 const res = await fetch("https://console.anthropic.com/v1/oauth/token", { 70 method: "POST", 71 headers: { 72 "content-type": "application/json", 73 "user-agent": "CRUSH/1.0", 74 }, 75 body: JSON.stringify({ 76 grant_type: "refresh_token", 77 refresh_token: refreshToken, 78 client_id: CLIENT_ID, 79 }), 80 }); 81 if (!res.ok) throw new Error(`refresh failed: ${res.status}`); 82 return (await res.json()) as { 83 access_token: string; 84 refresh_token?: string; 85 expires_in: number; 86 }; 87} 88 89function cleanPastedCode(input: string) { 90 let v = input.trim(); 91 v = v.replace(/^code\s*[:=]\s*/i, ""); 92 v = v.replace(/^["'`]/, "").replace(/["'`]$/, ""); 93 const m = v.match(/[A-Za-z0-9._~-]+(?:#[A-Za-z0-9._~-]+)?/); 94 if (m) return m[0]; 95 return v; 96} 97 98async function exchangeAuthorizationCode(code: string, verifier: string) { 99 const cleaned = cleanPastedCode(code); 100 const [pure, state = ""] = cleaned.split("#"); 101 const body = { 102 code: pure ?? "", 103 state: state ?? "", 104 grant_type: "authorization_code", 105 client_id: CLIENT_ID, 106 redirect_uri: "https://console.anthropic.com/oauth/code/callback", 107 code_verifier: verifier, 108 } satisfies Record<string, string>; 109 const res = await fetch("https://console.anthropic.com/v1/oauth/token", { 110 method: "POST", 111 headers: { 112 "content-type": "application/json", 113 "user-agent": "CRUSH/1.0", 114 }, 115 body: JSON.stringify(body), 116 }); 117 if (!res.ok) throw new Error(`code exchange failed: ${res.status}`); 118 return (await res.json()) as { 119 access_token: string; 120 refresh_token: string; 121 expires_in: number; 122 }; 123} 124 125const memory = new Map< 126 string, 127 { accessToken: string; refreshToken: string; expiresAt: number } 128>(); 129 130const HOME = Bun.env.HOME || Bun.env.USERPROFILE || "."; 131const CACHE_DIR = `${HOME}/.config/crush/anthropic`; 132const BEARER_FILE = `${CACHE_DIR}/bearer_token`; 133const REFRESH_FILE = `${CACHE_DIR}/refresh_token`; 134const EXPIRES_FILE = `${CACHE_DIR}/bearer_token.expires`; 135 136async function ensureDir() { 137 await Bun.$`mkdir -p ${CACHE_DIR}`; 138} 139 140async function writeSecret(path: string, data: string) { 141 await Bun.write(path, data); 142 await Bun.$`chmod 600 ${path}`; 143} 144 145async function readText(path: string) { 146 const f = Bun.file(path); 147 if (!(await f.exists())) return undefined; 148 return await f.text(); 149} 150 151async function loadFromDisk() { 152 const [bearer, refresh, expires] = await Promise.all([ 153 readText(BEARER_FILE), 154 readText(REFRESH_FILE), 155 readText(EXPIRES_FILE), 156 ]); 157 if (!bearer || !refresh || !expires) return undefined; 158 const exp = Number.parseInt(expires, 10) || 0; 159 return { 160 accessToken: bearer.trim(), 161 refreshToken: refresh.trim(), 162 expiresAt: exp, 163 }; 164} 165 166async function saveToDisk(entry: { 167 accessToken: string; 168 refreshToken: string; 169 expiresAt: number; 170}) { 171 await ensureDir(); 172 await writeSecret(BEARER_FILE, `${entry.accessToken}\n`); 173 await writeSecret(REFRESH_FILE, `${entry.refreshToken}\n`); 174 await writeSecret(EXPIRES_FILE, `${String(entry.expiresAt)}\n`); 175} 176 177let serverStarted = false; 178 179async function bootstrapFromDisk() { 180 const entry = await loadFromDisk(); 181 if (!entry) return false; 182 const now = Math.floor(Date.now() / 1000); 183 if (now < entry.expiresAt - 60) { 184 Bun.write(Bun.stdout, `${entry.accessToken}\n`); 185 setTimeout(() => process.exit(0), 50); 186 memory.set("tokens", entry); 187 return true; 188 } 189 try { 190 const refreshed = await exchangeRefreshToken(entry.refreshToken); 191 entry.accessToken = refreshed.access_token; 192 entry.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in; 193 if (refreshed.refresh_token) entry.refreshToken = refreshed.refresh_token; 194 await saveToDisk(entry); 195 memory.set("tokens", entry); 196 Bun.write(Bun.stdout, `${entry.accessToken}\n`); 197 setTimeout(() => process.exit(0), 50); 198 return true; 199 } catch { 200 return false; 201 } 202} 203 204await bootstrapFromDisk(); 205 206const argv = process.argv.slice(2); 207if (argv.includes("-h") || argv.includes("--help")) { 208 Bun.write(Bun.stdout, `Usage: anthropic\n\n`); 209 Bun.write(Bun.stdout, ` anthropic Start UI and flow; prints token on success and exits.\n`); 210 Bun.write(Bun.stdout, ` PORT=xxxx anthropic Override port (default 8787).\n`); 211 Bun.write(Bun.stdout, `\nTokens are cached at ~/.config/crush/anthropic and reused on later runs.\n`); 212 process.exit(0); 213} 214 215serve({ 216 port: PORT, 217 development: { console: false }, 218 async fetch(req) { 219 const url = new URL(req.url); 220 221 if (url.pathname.startsWith("/api/")) { 222 if (url.pathname === "/api/ping") 223 return json({ ok: true, ts: Date.now() }); 224 225 if (url.pathname === "/api/auth/start" && req.method === "POST") { 226 const { verifier, challenge } = await pkcePair(); 227 const authUrl = authorizeUrl(verifier, challenge); 228 return json({ authUrl, verifier }); 229 } 230 231 if (url.pathname === "/api/auth/complete" && req.method === "POST") { 232 const body = (await req.json().catch(() => ({}))) as { 233 code?: string; 234 verifier?: string; 235 }; 236 const code = String(body.code ?? ""); 237 const verifier = String(body.verifier ?? ""); 238 if (!code || !verifier) 239 return json({ error: "missing code or verifier" }, { status: 400 }); 240 const tokens = await exchangeAuthorizationCode(code, verifier); 241 const expiresAt = 242 Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0); 243 const entry = { 244 accessToken: tokens.access_token, 245 refreshToken: tokens.refresh_token, 246 expiresAt, 247 }; 248 memory.set("tokens", entry); 249 await saveToDisk(entry); 250 Bun.write(Bun.stdout, `${entry.accessToken}\n`); 251 setTimeout(() => process.exit(0), 100); 252 return json({ ok: true }); 253 } 254 255 if (url.pathname === "/api/token" && req.method === "GET") { 256 let entry = memory.get("tokens"); 257 if (!entry) { 258 const disk = await loadFromDisk(); 259 if (disk) { 260 entry = disk; 261 memory.set("tokens", entry); 262 } 263 } 264 if (!entry) 265 return json({ error: "not_authenticated" }, { status: 401 }); 266 const now = Math.floor(Date.now() / 1000); 267 if (now >= entry.expiresAt - 60) { 268 const refreshed = await exchangeRefreshToken(entry.refreshToken); 269 entry.accessToken = refreshed.access_token; 270 entry.expiresAt = 271 Math.floor(Date.now() / 1000) + refreshed.expires_in; 272 if (refreshed.refresh_token) 273 entry.refreshToken = refreshed.refresh_token; 274 memory.set("tokens", entry); 275 await saveToDisk(entry); 276 } 277 return json({ 278 accessToken: entry.accessToken, 279 expiresAt: entry.expiresAt, 280 }); 281 } 282 283 return notFound(); 284 } 285 286 const staticResp = await serveStatic(url.pathname); 287 if (staticResp) return staticResp; 288 289 return notFound(); 290 }, 291 error() {}, 292}); 293 294if (!serverStarted) { 295 serverStarted = true; 296 const url = `http://localhost:${PORT}`; 297 const tryRun = async (cmd: string, ...args: string[]) => { 298 try { 299 await Bun.$`${[cmd, ...args]}`.quiet(); 300 return true; 301 } catch { 302 return false; 303 } 304 }; 305 (async () => { 306 if (process.platform === "darwin") { 307 if (await tryRun("open", url)) return; 308 } else if (process.platform === "win32") { 309 if (await tryRun("cmd", "/c", "start", "", url)) return; 310 } else { 311 if (await tryRun("xdg-open", url)) return; 312 } 313 })(); 314}