this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: simplify the serving and make it more reliable

+212 -221
+1 -1
package.json
··· 1 1 { 2 2 "name": "anthropic-api-key", 3 - "version": "0.1.6", 3 + "version": "0.1.8", 4 4 "description": "CLI to fetch Anthropic API access tokens via OAuth with PKCE using Bun.", 5 5 "type": "module", 6 6 "private": false,
-181
public/index.html
··· 1 - <!doctype html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 - <title>Anthropic Auth</title> 7 - <style> 8 - body { 9 - font-family: 10 - system-ui, 11 - -apple-system, 12 - Segoe UI, 13 - Roboto, 14 - Ubuntu, 15 - Cantarell, 16 - Noto Sans, 17 - sans-serif; 18 - background: #0f0f10; 19 - color: #fff; 20 - margin: 0; 21 - display: flex; 22 - min-height: 100vh; 23 - align-items: center; 24 - justify-content: center; 25 - } 26 - .card { 27 - background: #1a1a1b; 28 - border: 1px solid #2b2b2c; 29 - border-radius: 14px; 30 - padding: 28px; 31 - max-width: 560px; 32 - width: 100%; 33 - } 34 - h1 { 35 - margin: 0 0 8px; 36 - } 37 - p { 38 - color: #9aa0a6; 39 - } 40 - button, 41 - a.button { 42 - background: linear-gradient(135deg, #ff6b35, #ff8e53); 43 - color: #fff; 44 - border: none; 45 - border-radius: 10px; 46 - padding: 12px 16px; 47 - font-weight: 600; 48 - cursor: pointer; 49 - text-decoration: none; 50 - display: inline-block; 51 - } 52 - textarea { 53 - width: 100%; 54 - min-height: 120px; 55 - background: #111; 56 - border: 1px solid #2b2b2c; 57 - border-radius: 10px; 58 - color: #fff; 59 - padding: 10px; 60 - } 61 - .row { 62 - margin: 16px 0; 63 - } 64 - .muted { 65 - color: #9aa0a6; 66 - } 67 - .status { 68 - margin-top: 8px; 69 - font-size: 14px; 70 - } 71 - </style> 72 - </head> 73 - <body> 74 - <div class="card"> 75 - <h1>Anthropic Authentication</h1> 76 - <p class="muted"> 77 - Start the OAuth flow, authorize in the new tab, then paste the 78 - returned token here. 79 - </p> 80 - 81 - <div class="row"> 82 - <a 83 - id="authlink" 84 - class="button" 85 - href="#" 86 - target="_blank" 87 - style="display: none" 88 - >Open Anthropic Authorization</a 89 - > 90 - </div> 91 - 92 - <div class="row"> 93 - <label for="code">Authorization code</label> 94 - <textarea 95 - id="code" 96 - placeholder="Paste the exact code shown by Anthropic (not a URL). If it includes a #, keep the part after it too." 97 - ></textarea> 98 - </div> 99 - 100 - <div class="row"> 101 - <button id="complete">Complete Authentication</button> 102 - </div> 103 - 104 - <div id="status" class="status"></div> 105 - </div> 106 - 107 - <script> 108 - let verifier = ""; 109 - const statusEl = document.getElementById("status"); 110 - 111 - function setStatus(msg, ok) { 112 - statusEl.textContent = msg; 113 - statusEl.style.color = ok ? "#34a853" : "#ea4335"; 114 - } 115 - 116 - (async () => { 117 - setStatus("Preparing authorization...", true); 118 - const res = await fetch("/api/auth/start", { method: "POST" }); 119 - if (!res.ok) { 120 - setStatus("Failed to prepare auth", false); 121 - return; 122 - } 123 - const data = await res.json(); 124 - verifier = data.verifier; 125 - const a = document.getElementById("authlink"); 126 - a.href = data.authUrl; 127 - a.style.display = "inline-block"; 128 - setStatus( 129 - 'Ready. Click "Open Authorization" to continue.', 130 - true, 131 - ); 132 - })(); 133 - 134 - const completeBtn = document.getElementById("complete"); 135 - document 136 - .getElementById("complete") 137 - .addEventListener("click", async () => { 138 - if (completeBtn.disabled) return; 139 - completeBtn.disabled = true; 140 - const code = document.getElementById("code").value.trim(); 141 - if (!code || !verifier) { 142 - setStatus( 143 - "Missing code or verifier. Click Start first.", 144 - false, 145 - ); 146 - completeBtn.disabled = false; 147 - return; 148 - } 149 - const res = await fetch("/api/auth/complete", { 150 - method: "POST", 151 - headers: { "content-type": "application/json" }, 152 - body: JSON.stringify({ code, verifier }), 153 - }); 154 - if (!res.ok) { 155 - setStatus("Code exchange failed", false); 156 - completeBtn.disabled = false; 157 - return; 158 - } 159 - setStatus("Authenticated! Fetching token...", true); 160 - const t = await fetch("/api/token"); 161 - if (!t.ok) { 162 - setStatus("Could not fetch token", false); 163 - completeBtn.disabled = false; 164 - return; 165 - } 166 - const tok = await t.json(); 167 - setStatus( 168 - "Access token acquired (expires " + 169 - new Date(tok.expiresAt * 1000).toLocaleString() + 170 - ")", 171 - true, 172 - ); 173 - setTimeout(() => { 174 - try { 175 - window.close(); 176 - } catch {} 177 - }, 500); 178 - }); 179 - </script> 180 - </body> 181 - </html>
+210 -38
src/index.ts
··· 17 17 } from "./lib/token"; 18 18 19 19 const PORT = Number(Bun.env.PORT || 8787); 20 - const ROOT = new URL("../", import.meta.url).pathname; 21 - const PUBLIC_DIR = `${ROOT}public`; 22 - 23 - function notFound() { 24 - return new Response("Not found", { status: 404 }); 25 - } 26 - 27 - async function serveStatic(pathname: string) { 28 - const filePath = PUBLIC_DIR + (pathname === "/" ? "/index.html" : pathname); 29 - try { 30 - const file = Bun.file(filePath); 31 - if (!(await file.exists())) return null; 32 - return new Response(file); 33 - } catch { 34 - return null; 35 - } 36 - } 37 20 38 21 function json(data: unknown, init: ResponseInit = {}) { 39 22 return new Response(JSON.stringify(data), { ··· 136 119 process.exit(0); 137 120 } 138 121 122 + const indexHtml = ` 123 + <!doctype html> 124 + <html> 125 + <head> 126 + <meta charset="utf-8" /> 127 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 128 + <title>Anthropic Auth</title> 129 + <style> 130 + body { 131 + font-family: 132 + system-ui, 133 + -apple-system, 134 + Segoe UI, 135 + Roboto, 136 + Ubuntu, 137 + Cantarell, 138 + Noto Sans, 139 + sans-serif; 140 + background: #0f0f10; 141 + color: #fff; 142 + margin: 0; 143 + display: flex; 144 + min-height: 100vh; 145 + align-items: center; 146 + justify-content: center; 147 + } 148 + .card { 149 + background: #1a1a1b; 150 + border: 1px solid #2b2b2c; 151 + border-radius: 14px; 152 + padding: 28px; 153 + max-width: 560px; 154 + width: 100%; 155 + } 156 + h1 { 157 + margin: 0 0 8px; 158 + } 159 + p { 160 + color: #9aa0a6; 161 + } 162 + button, 163 + a.button { 164 + background: linear-gradient(135deg, #ff6b35, #ff8e53); 165 + color: #fff; 166 + border: none; 167 + border-radius: 10px; 168 + padding: 12px 16px; 169 + font-weight: 600; 170 + cursor: pointer; 171 + text-decoration: none; 172 + display: inline-block; 173 + } 174 + textarea { 175 + width: 100%; 176 + min-height: 120px; 177 + background: #111; 178 + border: 1px solid #2b2b2c; 179 + border-radius: 10px; 180 + color: #fff; 181 + padding: 10px; 182 + } 183 + .row { 184 + margin: 16px 0; 185 + } 186 + .muted { 187 + color: #9aa0a6; 188 + } 189 + .status { 190 + margin-top: 8px; 191 + font-size: 14px; 192 + } 193 + </style> 194 + <script type="module" crossorigin src="../anthropic-api-key/index-9f070n0a.js"></script></head> 195 + <body> 196 + <div class="card"> 197 + <h1>Anthropic Authentication</h1> 198 + <p class="muted"> 199 + Start the OAuth flow, authorize in the new tab, then paste the 200 + returned token here. 201 + </p> 202 + 203 + <div class="row"> 204 + <a 205 + id="authlink" 206 + class="button" 207 + href="#" 208 + target="_blank" 209 + style="display: none" 210 + >Open Anthropic Authorization</a 211 + > 212 + </div> 213 + 214 + <div class="row"> 215 + <label for="code">Authorization code</label> 216 + <textarea 217 + id="code" 218 + placeholder="Paste the exact code shown by Anthropic (not a URL). If it includes a #, keep the part after it too." 219 + ></textarea> 220 + </div> 221 + 222 + <div class="row"> 223 + <button id="complete">Complete Authentication</button> 224 + </div> 225 + 226 + <div id="status" class="status"></div> 227 + </div> 228 + 229 + <script> 230 + let verifier = ""; 231 + const statusEl = document.getElementById("status"); 232 + 233 + function setStatus(msg, ok) { 234 + statusEl.textContent = msg; 235 + statusEl.style.color = ok ? "#34a853" : "#ea4335"; 236 + } 237 + 238 + (async () => { 239 + setStatus("Preparing authorization...", true); 240 + const res = await fetch("/api/auth/start", { method: "POST" }); 241 + if (!res.ok) { 242 + setStatus("Failed to prepare auth", false); 243 + return; 244 + } 245 + const data = await res.json(); 246 + verifier = data.verifier; 247 + const a = document.getElementById("authlink"); 248 + a.href = data.authUrl; 249 + a.style.display = "inline-block"; 250 + setStatus( 251 + 'Ready. Click "Open Authorization" to continue.', 252 + true, 253 + ); 254 + })(); 255 + 256 + const completeBtn = document.getElementById("complete"); 257 + document 258 + .getElementById("complete") 259 + .addEventListener("click", async () => { 260 + if (completeBtn.disabled) return; 261 + completeBtn.disabled = true; 262 + const code = document.getElementById("code").value.trim(); 263 + if (!code || !verifier) { 264 + setStatus( 265 + "Missing code or verifier. Click Start first.", 266 + false, 267 + ); 268 + completeBtn.disabled = false; 269 + return; 270 + } 271 + const res = await fetch("/api/auth/complete", { 272 + method: "POST", 273 + headers: { "content-type": "application/json" }, 274 + body: JSON.stringify({ code, verifier }), 275 + }); 276 + if (!res.ok) { 277 + setStatus("Code exchange failed", false); 278 + completeBtn.disabled = false; 279 + return; 280 + } 281 + setStatus("Authenticated! Fetching token...", true); 282 + const t = await fetch("/api/token"); 283 + if (!t.ok) { 284 + setStatus("Could not fetch token", false); 285 + completeBtn.disabled = false; 286 + return; 287 + } 288 + const tok = await t.json(); 289 + setStatus( 290 + "Access token acquired (expires " + 291 + new Date(tok.expiresAt * 1000).toLocaleString() + 292 + ")", 293 + true, 294 + ); 295 + setTimeout(() => { 296 + try { 297 + window.close(); 298 + } catch {} 299 + }, 500); 300 + }); 301 + </script> 302 + </body> 303 + </html> 304 + `; 305 + 139 306 if (!didBootstrap) { 140 307 // Only start the server and open the browser if we didn't bootstrap from disk 141 308 const memory = new Map< 142 309 string, 143 310 { accessToken: string; refreshToken: string; expiresAt: number } 144 311 >(); 145 - 146 312 serve({ 147 313 port: PORT, 148 314 development: { console: false }, 149 - async fetch(req) { 150 - const url = new URL(req.url); 151 - 152 - if (url.pathname.startsWith("/api/")) { 153 - if (url.pathname === "/api/ping") 154 - return json({ ok: true, ts: Date.now() }); 155 - 156 - if (url.pathname === "/api/auth/start" && req.method === "POST") { 315 + routes: { 316 + "/api/auth/start": { 317 + POST: async () => { 157 318 const { verifier, challenge } = await pkcePair(); 158 319 const authUrl = authorizeUrl(verifier, challenge); 159 320 return json({ authUrl, verifier }); 160 - } 321 + }, 322 + }, 161 323 162 - if (url.pathname === "/api/auth/complete" && req.method === "POST") { 324 + "/api/auth/complete": { 325 + POST: async (req) => { 163 326 const body = (await req.json().catch(() => ({}))) as { 164 327 code?: string; 165 328 verifier?: string; ··· 181 344 Bun.write(Bun.stdout, `${entry.accessToken}\n`); 182 345 setTimeout(() => process.exit(0), 100); 183 346 return json({ ok: true }); 184 - } 347 + }, 348 + }, 185 349 186 - if (url.pathname === "/api/token" && req.method === "GET") { 350 + // Get current token (refresh if needed) 351 + "/api/token": { 352 + GET: async () => { 187 353 let entry = memory.get("tokens"); 188 354 if (!entry) { 189 355 const disk = await loadFromDisk(); ··· 209 375 accessToken: entry.accessToken, 210 376 expiresAt: entry.expiresAt, 211 377 }); 212 - } 378 + }, 379 + }, 213 380 214 - return notFound(); 215 - } 381 + // Wildcard route for all other /api/ routes 382 + "/": () => 383 + new Response(indexHtml, { 384 + headers: { "content-type": "text/html; charset=utf-8" }, 385 + }), 386 + }, 216 387 217 - const staticResp = await serveStatic(url.pathname); 218 - if (staticResp) return staticResp; 219 - 220 - return notFound(); 388 + // Fallback for all other routes: serve static or 404 389 + fetch() { 390 + return new Response( 391 + "something went wrong and your request fell through", 392 + { status: 404 }, 393 + ); 221 394 }, 222 - error() {}, 223 395 }); 224 396 225 397 // Open browser
+1 -1
src/lib/token.ts
··· 54 54 method: "POST", 55 55 headers: { 56 56 "content-type": "application/json", 57 - "user-agent": "CRUSH/1.0", 57 + "user-agent": "anthropic", 58 58 }, 59 59 body: JSON.stringify({ 60 60 grant_type: "refresh_token",