get your claude code tokens here
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}