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}