get your claude code tokens here

Compare changes

Choose any two refs to compare.

+4 -13
README.md
···
simple cli to fetch an anthropic access token using oauth (pkce), built with bun
-
## install
-
-
```bash
-
bun install
-
```
-
## usage
```bash
-
# one-off via bunx (recommended)
-
bunx anthropic
-
-
# or via npx
-
npx anthropic
# override port
-
PORT=9999 bunx anthropic
# help
-
bunx anthropic --help
```
## what it does
···
simple cli to fetch an anthropic access token using oauth (pkce), built with bun
## usage
```bash
+
bunx anthropic-api-key
+
npx anthropic-api-key
# override port
+
PORT=9999 bunx anthropic-api-key
# help
+
bunx anthropic-api-key --help
```
## what it does
-531
anthropic.sh
···
-
#!/bin/sh
-
-
# Anthropic OAuth client ID
-
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
-
-
# Token cache file location
-
CACHE_DIR="${HOME}/.config/crush/anthropic"
-
CACHE_FILE="${CACHE_DIR}/bearer_token"
-
REFRESH_TOKEN_FILE="${CACHE_DIR}/refresh_token"
-
-
# Function to extract expiration from cached token file
-
extract_expiration() {
-
if [ -f "${CACHE_FILE}.expires" ]; then
-
cat "${CACHE_FILE}.expires"
-
fi
-
}
-
-
# Function to check if token is valid
-
is_token_valid() {
-
local expires="$1"
-
-
if [ -z "$expires" ]; then
-
return 1
-
fi
-
-
local current_time=$(date +%s)
-
# Add 60 second buffer before expiration
-
local buffer_time=$((expires - 60))
-
-
if [ "$current_time" -lt "$buffer_time" ]; then
-
return 0
-
else
-
return 1
-
fi
-
}
-
-
# Function to generate PKCE challenge (requires openssl)
-
generate_pkce() {
-
# Generate 32 random bytes, base64url encode
-
local verifier=$(openssl rand -base64 32 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n")
-
# Create SHA256 hash of verifier, base64url encode
-
local challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | openssl base64 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n")
-
-
echo "$verifier|$challenge"
-
}
-
-
# Function to exchange refresh token for new access token
-
exchange_refresh_token() {
-
local refresh_token="$1"
-
-
local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \
-
-H "Content-Type: application/json" \
-
-H "User-Agent: CRUSH/1.0" \
-
-d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"${refresh_token}\",\"client_id\":\"${CLIENT_ID}\"}")
-
-
# Parse JSON response - try jq first, fallback to sed
-
local access_token=""
-
local new_refresh_token=""
-
local expires_in=""
-
-
if command -v jq >/dev/null 2>&1; then
-
access_token=$(echo "$bearer_response" | jq -r '.access_token // empty')
-
new_refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty')
-
expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty')
-
else
-
# Fallback to sed parsing
-
access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
-
new_refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
-
expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
-
fi
-
-
if [ -n "$access_token" ] && [ -n "$expires_in" ]; then
-
# Calculate expiration timestamp
-
local current_time=$(date +%s)
-
local expires_timestamp=$((current_time + expires_in))
-
-
# Cache the new tokens
-
mkdir -p "$CACHE_DIR"
-
echo "$access_token" > "$CACHE_FILE"
-
chmod 600 "$CACHE_FILE"
-
-
if [ -n "$new_refresh_token" ]; then
-
echo "$new_refresh_token" > "$REFRESH_TOKEN_FILE"
-
chmod 600 "$REFRESH_TOKEN_FILE"
-
fi
-
-
# Store expiration for future reference
-
echo "$expires_timestamp" > "${CACHE_FILE}.expires"
-
chmod 600 "${CACHE_FILE}.expires"
-
-
echo "$access_token"
-
return 0
-
fi
-
-
return 1
-
}
-
-
# Function to exchange authorization code for tokens
-
exchange_authorization_code() {
-
local auth_code="$1"
-
local verifier="$2"
-
-
# Split code if it contains state (format: code#state)
-
local code=$(echo "$auth_code" | cut -d'#' -f1)
-
local state=""
-
if echo "$auth_code" | grep -q '#'; then
-
state=$(echo "$auth_code" | cut -d'#' -f2)
-
fi
-
-
# Use the working endpoint
-
local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \
-
-H "Content-Type: application/json" \
-
-H "User-Agent: CRUSH/1.0" \
-
-d "{\"code\":\"${code}\",\"state\":\"${state}\",\"grant_type\":\"authorization_code\",\"client_id\":\"${CLIENT_ID}\",\"redirect_uri\":\"https://console.anthropic.com/oauth/code/callback\",\"code_verifier\":\"${verifier}\"}")
-
-
# Parse JSON response - try jq first, fallback to sed
-
local access_token=""
-
local refresh_token=""
-
local expires_in=""
-
-
if command -v jq >/dev/null 2>&1; then
-
access_token=$(echo "$bearer_response" | jq -r '.access_token // empty')
-
refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty')
-
expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty')
-
else
-
# Fallback to sed parsing
-
access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
-
refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
-
expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
-
fi
-
-
if [ -n "$access_token" ] && [ -n "$refresh_token" ] && [ -n "$expires_in" ]; then
-
# Calculate expiration timestamp
-
local current_time=$(date +%s)
-
local expires_timestamp=$((current_time + expires_in))
-
-
# Cache the tokens
-
mkdir -p "$CACHE_DIR"
-
echo "$access_token" > "$CACHE_FILE"
-
echo "$refresh_token" > "$REFRESH_TOKEN_FILE"
-
echo "$expires_timestamp" > "${CACHE_FILE}.expires"
-
chmod 600 "$CACHE_FILE" "$REFRESH_TOKEN_FILE" "${CACHE_FILE}.expires"
-
-
echo "$access_token"
-
return 0
-
else
-
return 1
-
fi
-
}
-
-
# Check for cached bearer token
-
if [ -f "$CACHE_FILE" ] && [ -f "${CACHE_FILE}.expires" ]; then
-
CACHED_TOKEN=$(cat "$CACHE_FILE")
-
CACHED_EXPIRES=$(cat "${CACHE_FILE}.expires")
-
if is_token_valid "$CACHED_EXPIRES"; then
-
# Token is still valid, output and exit
-
echo "$CACHED_TOKEN"
-
exit 0
-
fi
-
fi
-
-
# Bearer token is expired/missing, try to use cached refresh token
-
if [ -f "$REFRESH_TOKEN_FILE" ]; then
-
REFRESH_TOKEN=$(cat "$REFRESH_TOKEN_FILE")
-
if [ -n "$REFRESH_TOKEN" ]; then
-
# Try to exchange refresh token for new bearer token
-
BEARER_TOKEN=$(exchange_refresh_token "$REFRESH_TOKEN")
-
if [ -n "$BEARER_TOKEN" ]; then
-
# Successfully got new bearer token, output and exit
-
echo "$BEARER_TOKEN"
-
exit 0
-
fi
-
fi
-
fi
-
-
# No valid tokens found, start OAuth flow
-
# Check if openssl is available for PKCE
-
if ! command -v openssl >/dev/null 2>&1; then
-
exit 1
-
fi
-
-
# Generate PKCE challenge
-
PKCE_DATA=$(generate_pkce)
-
VERIFIER=$(echo "$PKCE_DATA" | cut -d'|' -f1)
-
CHALLENGE=$(echo "$PKCE_DATA" | cut -d'|' -f2)
-
-
# Build OAuth URL
-
AUTH_URL="https://claude.ai/oauth/authorize"
-
AUTH_URL="${AUTH_URL}?response_type=code"
-
AUTH_URL="${AUTH_URL}&client_id=${CLIENT_ID}"
-
AUTH_URL="${AUTH_URL}&redirect_uri=https://console.anthropic.com/oauth/code/callback"
-
AUTH_URL="${AUTH_URL}&scope=org:create_api_key%20user:profile%20user:inference"
-
AUTH_URL="${AUTH_URL}&code_challenge=${CHALLENGE}"
-
AUTH_URL="${AUTH_URL}&code_challenge_method=S256"
-
AUTH_URL="${AUTH_URL}&state=${VERIFIER}"
-
-
# Create a temporary HTML file with the authentication form
-
TEMP_HTML="/tmp/anthropic_auth_$$.html"
-
cat > "$TEMP_HTML" << EOF
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<title>Anthropic Authentication</title>
-
<style>
-
* {
-
box-sizing: border-box;
-
margin: 0;
-
padding: 0;
-
}
-
-
body {
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
-
background: linear-gradient(135deg, #1a1a1a 0%, #2d1810 100%);
-
color: #ffffff;
-
min-height: 100vh;
-
display: flex;
-
align-items: center;
-
justify-content: center;
-
padding: 20px;
-
}
-
-
.container {
-
background: rgba(40, 40, 40, 0.95);
-
border: 1px solid #4a4a4a;
-
border-radius: 16px;
-
padding: 48px;
-
max-width: 480px;
-
width: 100%;
-
text-align: center;
-
backdrop-filter: blur(10px);
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
-
}
-
-
.logo {
-
width: 48px;
-
height: 48px;
-
margin: 0 auto 24px;
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
-
border-radius: 12px;
-
display: flex;
-
align-items: center;
-
justify-content: center;
-
font-weight: bold;
-
font-size: 24px;
-
color: white;
-
}
-
-
h1 {
-
font-size: 28px;
-
font-weight: 600;
-
margin-bottom: 12px;
-
color: #ffffff;
-
}
-
-
.subtitle {
-
color: #a0a0a0;
-
margin-bottom: 32px;
-
font-size: 16px;
-
line-height: 1.5;
-
}
-
-
.step {
-
margin-bottom: 32px;
-
text-align: left;
-
}
-
-
.step-number {
-
display: inline-flex;
-
align-items: center;
-
justify-content: center;
-
width: 24px;
-
height: 24px;
-
background: #ff6b35;
-
color: white;
-
border-radius: 50%;
-
font-size: 14px;
-
font-weight: 600;
-
margin-right: 12px;
-
}
-
-
.step-title {
-
font-weight: 600;
-
margin-bottom: 8px;
-
color: #ffffff;
-
}
-
-
.step-description {
-
color: #a0a0a0;
-
font-size: 14px;
-
margin-left: 36px;
-
}
-
-
.button {
-
display: inline-block;
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
-
color: white;
-
padding: 16px 32px;
-
text-decoration: none;
-
border-radius: 12px;
-
font-weight: 600;
-
font-size: 16px;
-
margin-bottom: 24px;
-
transition: all 0.2s ease;
-
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
-
}
-
-
.button:hover {
-
transform: translateY(-2px);
-
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
-
}
-
-
.input-group {
-
margin-bottom: 24px;
-
text-align: left;
-
}
-
-
label {
-
display: block;
-
margin-bottom: 8px;
-
font-weight: 500;
-
color: #ffffff;
-
}
-
-
textarea {
-
width: 100%;
-
background: #2a2a2a;
-
border: 2px solid #4a4a4a;
-
border-radius: 8px;
-
padding: 16px;
-
color: #ffffff;
-
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
-
font-size: 14px;
-
line-height: 1.4;
-
resize: vertical;
-
min-height: 120px;
-
transition: border-color 0.2s ease;
-
}
-
-
textarea:focus {
-
outline: none;
-
border-color: #ff6b35;
-
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
-
}
-
-
textarea::placeholder {
-
color: #666;
-
}
-
-
.submit-btn {
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
-
color: white;
-
border: none;
-
padding: 16px 32px;
-
border-radius: 12px;
-
font-weight: 600;
-
font-size: 16px;
-
cursor: pointer;
-
transition: all 0.2s ease;
-
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
-
width: 100%;
-
}
-
-
.submit-btn:hover {
-
transform: translateY(-2px);
-
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
-
}
-
-
.submit-btn:disabled {
-
opacity: 0.6;
-
cursor: not-allowed;
-
transform: none;
-
}
-
-
.status {
-
margin-top: 16px;
-
padding: 12px;
-
border-radius: 8px;
-
font-size: 14px;
-
display: none;
-
}
-
-
.status.success {
-
background: rgba(52, 168, 83, 0.1);
-
border: 1px solid rgba(52, 168, 83, 0.3);
-
color: #34a853;
-
}
-
-
.status.error {
-
background: rgba(234, 67, 53, 0.1);
-
border: 1px solid rgba(234, 67, 53, 0.3);
-
color: #ea4335;
-
}
-
</style>
-
</head>
-
<body>
-
<div class="container">
-
<div class="logo">A</div>
-
<h1>Anthropic Authentication</h1>
-
<p class="subtitle">Connect your Anthropic account to continue</p>
-
-
<div class="step">
-
<div class="step-title">
-
<span class="step-number">1</span>
-
Authorize with Anthropic
-
</div>
-
<div class="step-description">
-
Click the button below to open the Anthropic authorization page
-
</div>
-
</div>
-
-
<a href="$AUTH_URL" class="button" target="_blank">
-
Open Anthropic Authorization
-
</a>
-
-
<div class="step">
-
<div class="step-title">
-
<span class="step-number">2</span>
-
Paste your authorization token
-
</div>
-
<div class="step-description">
-
After authorizing, copy the token and paste it below
-
</div>
-
</div>
-
-
<form id="tokenForm">
-
<div class="input-group">
-
<label for="token">Authorization Token:</label>
-
<textarea
-
id="token"
-
name="token"
-
placeholder="Paste your token here..."
-
required
-
></textarea>
-
</div>
-
<button type="submit" class="submit-btn" id="submitBtn">
-
Complete Authentication
-
</button>
-
</form>
-
-
<div id="status" class="status"></div>
-
</div>
-
-
<script>
-
document.getElementById('tokenForm').addEventListener('submit', function(e) {
-
e.preventDefault();
-
-
const token = document.getElementById('token').value.trim();
-
if (!token) {
-
showStatus('Please paste your authorization token', 'error');
-
return;
-
}
-
-
// Ensure token has content before creating file
-
if (token.length > 0) {
-
// Save the token as a downloadable file
-
const blob = new Blob([token], { type: 'text/plain' });
-
const a = document.createElement('a');
-
a.href = URL.createObjectURL(blob);
-
a.download = "anthropic_token.txt";
-
document.body.appendChild(a); // Append to body to ensure it works in all browsers
-
a.click();
-
document.body.removeChild(a); // Clean up
-
-
// Verify file creation
-
console.log("Token file created with content length: " + token.length);
-
} else {
-
showStatus('Empty token detected, please provide a valid token', 'error');
-
return;
-
}
-
-
document.getElementById('submitBtn').disabled = true;
-
document.getElementById('submitBtn').textContent = "Token saved, you may close this tab.";
-
showStatus('Token file downloaded! You can close this window.', 'success');
-
-
// setTimeout(() => {
-
// window.close();
-
// }, 2000);
-
});
-
-
function showStatus(message, type) {
-
const status = document.getElementById('status');
-
status.textContent = message;
-
status.className = 'status ' + type;
-
status.style.display = 'block';
-
}
-
-
// Auto-close after 10 minutes
-
setTimeout(() => {
-
window.close();
-
}, 600000);
-
</script>
-
</body>
-
</html>
-
EOF
-
-
# Open the HTML file
-
if command -v xdg-open >/dev/null 2>&1; then
-
xdg-open "$TEMP_HTML" >/dev/null 2>&1 &
-
elif command -v open >/dev/null 2>&1; then
-
open "$TEMP_HTML" >/dev/null 2>&1 &
-
elif command -v start >/dev/null 2>&1; then
-
start "$TEMP_HTML" >/dev/null 2>&1 &
-
fi
-
-
# Wait for user to download the token file
-
TOKEN_FILE="$HOME/Downloads/anthropic_token.txt"
-
-
for i in $(seq 1 60); do
-
if [ -f "$TOKEN_FILE" ]; then
-
AUTH_CODE=$(cat "$TOKEN_FILE" | tr -d '\r\n')
-
rm -f "$TOKEN_FILE"
-
break
-
fi
-
sleep 2
-
done
-
-
# Clean up the temporary HTML file
-
rm -f "$TEMP_HTML"
-
-
if [ -z "$AUTH_CODE" ]; then
-
exit 1
-
fi
-
-
# Exchange code for tokens
-
ACCESS_TOKEN=$(exchange_authorization_code "$AUTH_CODE" "$VERIFIER")
-
if [ -n "$ACCESS_TOKEN" ]; then
-
echo "$ACCESS_TOKEN"
-
exit 0
-
else
-
exit 1
-
fi
···
-314
bin/anthropic.ts
···
-
#!/usr/bin/env bun
-
-
import { serve } from "bun";
-
-
const PORT = Number(Bun.env.PORT || 8787);
-
const ROOT = new URL("../", import.meta.url).pathname;
-
const PUBLIC_DIR = `${ROOT}public`;
-
-
function notFound() {
-
return new Response("Not found", { status: 404 });
-
}
-
-
async function serveStatic(pathname: string) {
-
const filePath = PUBLIC_DIR + (pathname === "/" ? "/index.html" : pathname);
-
try {
-
const file = Bun.file(filePath);
-
if (!(await file.exists())) return null;
-
return new Response(file);
-
} catch {
-
return null;
-
}
-
}
-
-
function json(data: unknown, init: ResponseInit = {}) {
-
return new Response(JSON.stringify(data), {
-
headers: { "content-type": "application/json", ...(init.headers || {}) },
-
...init,
-
});
-
}
-
-
function authorizeUrl(verifier: string, challenge: string) {
-
const u = new URL("https://claude.ai/oauth/authorize");
-
u.searchParams.set("response_type", "code");
-
u.searchParams.set("client_id", CLIENT_ID);
-
u.searchParams.set(
-
"redirect_uri",
-
"https://console.anthropic.com/oauth/code/callback",
-
);
-
u.searchParams.set("scope", "org:create_api_key user:profile user:inference");
-
u.searchParams.set("code_challenge", challenge);
-
u.searchParams.set("code_challenge_method", "S256");
-
u.searchParams.set("state", verifier);
-
return u.toString();
-
}
-
-
function base64url(input: ArrayBuffer | Uint8Array) {
-
const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
-
return Buffer.from(buf)
-
.toString("base64")
-
.replace(/=/g, "")
-
.replace(/\+/g, "-")
-
.replace(/\//g, "_");
-
}
-
-
async function pkcePair() {
-
const bytes = crypto.getRandomValues(new Uint8Array(32));
-
const verifier = base64url(bytes);
-
const digest = await crypto.subtle.digest(
-
"SHA-256",
-
new TextEncoder().encode(verifier),
-
);
-
const challenge = base64url(digest as ArrayBuffer);
-
return { verifier, challenge };
-
}
-
-
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
-
-
async function exchangeRefreshToken(refreshToken: string) {
-
const res = await fetch("https://console.anthropic.com/v1/oauth/token", {
-
method: "POST",
-
headers: {
-
"content-type": "application/json",
-
"user-agent": "CRUSH/1.0",
-
},
-
body: JSON.stringify({
-
grant_type: "refresh_token",
-
refresh_token: refreshToken,
-
client_id: CLIENT_ID,
-
}),
-
});
-
if (!res.ok) throw new Error(`refresh failed: ${res.status}`);
-
return (await res.json()) as {
-
access_token: string;
-
refresh_token?: string;
-
expires_in: number;
-
};
-
}
-
-
function cleanPastedCode(input: string) {
-
let v = input.trim();
-
v = v.replace(/^code\s*[:=]\s*/i, "");
-
v = v.replace(/^["'`]/, "").replace(/["'`]$/, "");
-
const m = v.match(/[A-Za-z0-9._~-]+(?:#[A-Za-z0-9._~-]+)?/);
-
if (m) return m[0];
-
return v;
-
}
-
-
async function exchangeAuthorizationCode(code: string, verifier: string) {
-
const cleaned = cleanPastedCode(code);
-
const [pure, state = ""] = cleaned.split("#");
-
const body = {
-
code: pure ?? "",
-
state: state ?? "",
-
grant_type: "authorization_code",
-
client_id: CLIENT_ID,
-
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
-
code_verifier: verifier,
-
} satisfies Record<string, string>;
-
const res = await fetch("https://console.anthropic.com/v1/oauth/token", {
-
method: "POST",
-
headers: {
-
"content-type": "application/json",
-
"user-agent": "CRUSH/1.0",
-
},
-
body: JSON.stringify(body),
-
});
-
if (!res.ok) throw new Error(`code exchange failed: ${res.status}`);
-
return (await res.json()) as {
-
access_token: string;
-
refresh_token: string;
-
expires_in: number;
-
};
-
}
-
-
const memory = new Map<
-
string,
-
{ accessToken: string; refreshToken: string; expiresAt: number }
-
>();
-
-
const HOME = Bun.env.HOME || Bun.env.USERPROFILE || ".";
-
const CACHE_DIR = `${HOME}/.config/crush/anthropic`;
-
const BEARER_FILE = `${CACHE_DIR}/bearer_token`;
-
const REFRESH_FILE = `${CACHE_DIR}/refresh_token`;
-
const EXPIRES_FILE = `${CACHE_DIR}/bearer_token.expires`;
-
-
async function ensureDir() {
-
await Bun.$`mkdir -p ${CACHE_DIR}`;
-
}
-
-
async function writeSecret(path: string, data: string) {
-
await Bun.write(path, data);
-
await Bun.$`chmod 600 ${path}`;
-
}
-
-
async function readText(path: string) {
-
const f = Bun.file(path);
-
if (!(await f.exists())) return undefined;
-
return await f.text();
-
}
-
-
async function loadFromDisk() {
-
const [bearer, refresh, expires] = await Promise.all([
-
readText(BEARER_FILE),
-
readText(REFRESH_FILE),
-
readText(EXPIRES_FILE),
-
]);
-
if (!bearer || !refresh || !expires) return undefined;
-
const exp = Number.parseInt(expires, 10) || 0;
-
return {
-
accessToken: bearer.trim(),
-
refreshToken: refresh.trim(),
-
expiresAt: exp,
-
};
-
}
-
-
async function saveToDisk(entry: {
-
accessToken: string;
-
refreshToken: string;
-
expiresAt: number;
-
}) {
-
await ensureDir();
-
await writeSecret(BEARER_FILE, `${entry.accessToken}\n`);
-
await writeSecret(REFRESH_FILE, `${entry.refreshToken}\n`);
-
await writeSecret(EXPIRES_FILE, `${String(entry.expiresAt)}\n`);
-
}
-
-
let serverStarted = false;
-
-
async function bootstrapFromDisk() {
-
const entry = await loadFromDisk();
-
if (!entry) return false;
-
const now = Math.floor(Date.now() / 1000);
-
if (now < entry.expiresAt - 60) {
-
Bun.write(Bun.stdout, `${entry.accessToken}\n`);
-
setTimeout(() => process.exit(0), 50);
-
memory.set("tokens", entry);
-
return true;
-
}
-
try {
-
const refreshed = await exchangeRefreshToken(entry.refreshToken);
-
entry.accessToken = refreshed.access_token;
-
entry.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in;
-
if (refreshed.refresh_token) entry.refreshToken = refreshed.refresh_token;
-
await saveToDisk(entry);
-
memory.set("tokens", entry);
-
Bun.write(Bun.stdout, `${entry.accessToken}\n`);
-
setTimeout(() => process.exit(0), 50);
-
return true;
-
} catch {
-
return false;
-
}
-
}
-
-
await bootstrapFromDisk();
-
-
const argv = process.argv.slice(2);
-
if (argv.includes("-h") || argv.includes("--help")) {
-
Bun.write(Bun.stdout, `Usage: anthropic\n\n`);
-
Bun.write(Bun.stdout, ` anthropic Start UI and flow; prints token on success and exits.\n`);
-
Bun.write(Bun.stdout, ` PORT=xxxx anthropic Override port (default 8787).\n`);
-
Bun.write(Bun.stdout, `\nTokens are cached at ~/.config/crush/anthropic and reused on later runs.\n`);
-
process.exit(0);
-
}
-
-
serve({
-
port: PORT,
-
development: { console: false },
-
async fetch(req) {
-
const url = new URL(req.url);
-
-
if (url.pathname.startsWith("/api/")) {
-
if (url.pathname === "/api/ping")
-
return json({ ok: true, ts: Date.now() });
-
-
if (url.pathname === "/api/auth/start" && req.method === "POST") {
-
const { verifier, challenge } = await pkcePair();
-
const authUrl = authorizeUrl(verifier, challenge);
-
return json({ authUrl, verifier });
-
}
-
-
if (url.pathname === "/api/auth/complete" && req.method === "POST") {
-
const body = (await req.json().catch(() => ({}))) as {
-
code?: string;
-
verifier?: string;
-
};
-
const code = String(body.code ?? "");
-
const verifier = String(body.verifier ?? "");
-
if (!code || !verifier)
-
return json({ error: "missing code or verifier" }, { status: 400 });
-
const tokens = await exchangeAuthorizationCode(code, verifier);
-
const expiresAt =
-
Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0);
-
const entry = {
-
accessToken: tokens.access_token,
-
refreshToken: tokens.refresh_token,
-
expiresAt,
-
};
-
memory.set("tokens", entry);
-
await saveToDisk(entry);
-
Bun.write(Bun.stdout, `${entry.accessToken}\n`);
-
setTimeout(() => process.exit(0), 100);
-
return json({ ok: true });
-
}
-
-
if (url.pathname === "/api/token" && req.method === "GET") {
-
let entry = memory.get("tokens");
-
if (!entry) {
-
const disk = await loadFromDisk();
-
if (disk) {
-
entry = disk;
-
memory.set("tokens", entry);
-
}
-
}
-
if (!entry)
-
return json({ error: "not_authenticated" }, { status: 401 });
-
const now = Math.floor(Date.now() / 1000);
-
if (now >= entry.expiresAt - 60) {
-
const refreshed = await exchangeRefreshToken(entry.refreshToken);
-
entry.accessToken = refreshed.access_token;
-
entry.expiresAt =
-
Math.floor(Date.now() / 1000) + refreshed.expires_in;
-
if (refreshed.refresh_token)
-
entry.refreshToken = refreshed.refresh_token;
-
memory.set("tokens", entry);
-
await saveToDisk(entry);
-
}
-
return json({
-
accessToken: entry.accessToken,
-
expiresAt: entry.expiresAt,
-
});
-
}
-
-
return notFound();
-
}
-
-
const staticResp = await serveStatic(url.pathname);
-
if (staticResp) return staticResp;
-
-
return notFound();
-
},
-
error() {},
-
});
-
-
if (!serverStarted) {
-
serverStarted = true;
-
const url = `http://localhost:${PORT}`;
-
const tryRun = async (cmd: string, ...args: string[]) => {
-
try {
-
await Bun.$`${[cmd, ...args]}`.quiet();
-
return true;
-
} catch {
-
return false;
-
}
-
};
-
(async () => {
-
if (process.platform === "darwin") {
-
if (await tryRun("open", url)) return;
-
} else if (process.platform === "win32") {
-
if (await tryRun("cmd", "/c", "start", "", url)) return;
-
} else {
-
if (await tryRun("xdg-open", url)) return;
-
}
-
})();
-
}
···
-23
bin/open.ts
···
-
#!/usr/bin/env bun
-
-
const PORT = Number(Bun.env.PORT || 8787);
-
-
async function open(url: string) {
-
const tryRun = async (cmd: string, ...args: string[]) => {
-
try {
-
await Bun.$`${[cmd, ...args]}`.quiet();
-
return true;
-
} catch {
-
return false;
-
}
-
};
-
if (process.platform === "darwin") {
-
if (await tryRun("open", url)) return;
-
} else if (process.platform === "win32") {
-
if (await tryRun("cmd", "/c", "start", "", url)) return;
-
} else {
-
if (await tryRun("xdg-open", url)) return;
-
}
-
}
-
-
await open(`http://localhost:${PORT}`);
···
+195 -2
bun.lock
···
"workspaces": {
"": {
"name": "anthropic-api-key",
"devDependencies": {
"@types/bun": "latest",
-
},
-
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
}
}
···
"workspaces": {
"": {
"name": "anthropic-api-key",
+
"dependencies": {
+
"@types/express": "^5.0.3",
+
"express": "^5.1.0",
+
"net": "^1.0.2",
+
"node-fetch": "^3.3.2",
+
"open": "^10.2.0",
+
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5",
},
},
},
"packages": {
+
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
+
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
+
+
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="],
+
+
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
+
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
+
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
+
+
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
+
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
+
+
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
+
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
+
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
+
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
+
+
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
+
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+
"default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
+
+
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
+
+
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
+
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
+
+
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
+
+
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
+
+
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
+
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
+
+
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
+
+
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
+
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
+
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
+
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+
"net": ["net@1.0.2", "", {}, "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ=="],
+
+
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
+
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
+
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
+
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
+
+
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
+
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+
"run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
+
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
+
+
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
+
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
+
+
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
+
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
+
+
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
}
}
+11 -4
package.json
···
{
"name": "anthropic-api-key",
-
"version": "0.1.0",
"description": "CLI to fetch Anthropic API access tokens via OAuth with PKCE using Bun.",
"type": "module",
"private": false,
···
},
"homepage": "https://github.com/taciturnaxolotl/anthropic-api-key#readme",
"bin": {
-
"anthropic": "dist/anthropic.js"
},
"exports": {
-
".": "./dist/anthropic.js"
},
"files": [
"dist",
"public"
],
"scripts": {
-
"build": "bun build bin/anthropic.ts --outdir=dist --target=bun --sourcemap=external",
"prepare": "bun run build"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5"
}
}
···
{
"name": "anthropic-api-key",
+
"version": "0.2.2",
"description": "CLI to fetch Anthropic API access tokens via OAuth with PKCE using Bun.",
"type": "module",
"private": false,
···
},
"homepage": "https://github.com/taciturnaxolotl/anthropic-api-key#readme",
"bin": {
+
"anthropic-api-key": "dist/index.js"
},
"exports": {
+
".": "./dist/index.js",
+
"./lib/token": "./dist/lib/token.js"
},
"files": [
"dist",
"public"
],
"scripts": {
+
"build": "bun build src/index.ts src/lib/token.ts --outdir=dist --target=node --sourcemap=external",
"prepare": "bun run build"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5"
+
},
+
"dependencies": {
+
"@types/express": "^5.0.3",
+
"express": "^5.1.0",
+
"node-fetch": "^3.3.2",
+
"open": "^10.2.0"
}
}
-181
public/index.html
···
-
<!doctype html>
-
<html>
-
<head>
-
<meta charset="utf-8" />
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
<title>Anthropic Auth</title>
-
<style>
-
body {
-
font-family:
-
system-ui,
-
-apple-system,
-
Segoe UI,
-
Roboto,
-
Ubuntu,
-
Cantarell,
-
Noto Sans,
-
sans-serif;
-
background: #0f0f10;
-
color: #fff;
-
margin: 0;
-
display: flex;
-
min-height: 100vh;
-
align-items: center;
-
justify-content: center;
-
}
-
.card {
-
background: #1a1a1b;
-
border: 1px solid #2b2b2c;
-
border-radius: 14px;
-
padding: 28px;
-
max-width: 560px;
-
width: 100%;
-
}
-
h1 {
-
margin: 0 0 8px;
-
}
-
p {
-
color: #9aa0a6;
-
}
-
button,
-
a.button {
-
background: linear-gradient(135deg, #ff6b35, #ff8e53);
-
color: #fff;
-
border: none;
-
border-radius: 10px;
-
padding: 12px 16px;
-
font-weight: 600;
-
cursor: pointer;
-
text-decoration: none;
-
display: inline-block;
-
}
-
textarea {
-
width: 100%;
-
min-height: 120px;
-
background: #111;
-
border: 1px solid #2b2b2c;
-
border-radius: 10px;
-
color: #fff;
-
padding: 10px;
-
}
-
.row {
-
margin: 16px 0;
-
}
-
.muted {
-
color: #9aa0a6;
-
}
-
.status {
-
margin-top: 8px;
-
font-size: 14px;
-
}
-
</style>
-
</head>
-
<body>
-
<div class="card">
-
<h1>Anthropic Authentication</h1>
-
<p class="muted">
-
Start the OAuth flow, authorize in the new tab, then paste the
-
returned token here.
-
</p>
-
-
<div class="row">
-
<a
-
id="authlink"
-
class="button"
-
href="#"
-
target="_blank"
-
style="display: none"
-
>Open Anthropic Authorization</a
-
>
-
</div>
-
-
<div class="row">
-
<label for="code">Authorization code</label>
-
<textarea
-
id="code"
-
placeholder="Paste the exact code shown by Anthropic (not a URL). If it includes a #, keep the part after it too."
-
></textarea>
-
</div>
-
-
<div class="row">
-
<button id="complete">Complete Authentication</button>
-
</div>
-
-
<div id="status" class="status"></div>
-
</div>
-
-
<script>
-
let verifier = "";
-
const statusEl = document.getElementById("status");
-
-
function setStatus(msg, ok) {
-
statusEl.textContent = msg;
-
statusEl.style.color = ok ? "#34a853" : "#ea4335";
-
}
-
-
(async () => {
-
setStatus("Preparing authorization...", true);
-
const res = await fetch("/api/auth/start", { method: "POST" });
-
if (!res.ok) {
-
setStatus("Failed to prepare auth", false);
-
return;
-
}
-
const data = await res.json();
-
verifier = data.verifier;
-
const a = document.getElementById("authlink");
-
a.href = data.authUrl;
-
a.style.display = "inline-block";
-
setStatus(
-
'Ready. Click "Open Authorization" to continue.',
-
true,
-
);
-
})();
-
-
const completeBtn = document.getElementById("complete");
-
document
-
.getElementById("complete")
-
.addEventListener("click", async () => {
-
if (completeBtn.disabled) return;
-
completeBtn.disabled = true;
-
const code = document.getElementById("code").value.trim();
-
if (!code || !verifier) {
-
setStatus(
-
"Missing code or verifier. Click Start first.",
-
false,
-
);
-
completeBtn.disabled = false;
-
return;
-
}
-
const res = await fetch("/api/auth/complete", {
-
method: "POST",
-
headers: { "content-type": "application/json" },
-
body: JSON.stringify({ code, verifier }),
-
});
-
if (!res.ok) {
-
setStatus("Code exchange failed", false);
-
completeBtn.disabled = false;
-
return;
-
}
-
setStatus("Authenticated! Fetching token...", true);
-
const t = await fetch("/api/token");
-
if (!t.ok) {
-
setStatus("Could not fetch token", false);
-
completeBtn.disabled = false;
-
return;
-
}
-
const tok = await t.json();
-
setStatus(
-
"Access token acquired (expires " +
-
new Date(tok.expiresAt * 1000).toLocaleString() +
-
")",
-
true,
-
);
-
setTimeout(() => {
-
try {
-
window.close();
-
} catch {}
-
}, 500);
-
});
-
</script>
-
</body>
-
</html>
···
+367
src/index.ts
···
···
+
#!/usr/bin/env node
+
+
import { createServer } from "node:http";
+
import express from "express";
+
import fetch from "node-fetch";
+
import open from "open";
+
import {
+
bootstrapFromDisk,
+
exchangeRefreshToken,
+
loadFromDisk,
+
saveToDisk,
+
} from "./lib/token";
+
+
const PORT = Number(process.env.PORT || 8787);
+
+
function json(res: express.Response, data: unknown, status = 200) {
+
res.status(status).json(data);
+
}
+
+
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
+
+
function authorizeUrl(verifier: string, challenge: string) {
+
const u = new URL("https://claude.ai/oauth/authorize");
+
u.searchParams.set("response_type", "code");
+
u.searchParams.set("client_id", CLIENT_ID);
+
u.searchParams.set(
+
"redirect_uri",
+
"https://console.anthropic.com/oauth/code/callback",
+
);
+
u.searchParams.set("scope", "org:create_api_key user:profile user:inference");
+
u.searchParams.set("code_challenge", challenge);
+
u.searchParams.set("code_challenge_method", "S256");
+
u.searchParams.set("state", verifier);
+
return u.toString();
+
}
+
+
function base64url(input: ArrayBuffer | Uint8Array) {
+
const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
+
return Buffer.from(buf)
+
.toString("base64")
+
.replace(/=/g, "")
+
.replace(/\+/g, "-")
+
.replace(/\//g, "_");
+
}
+
+
async function pkcePair() {
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
+
const verifier = base64url(bytes);
+
const digest = await crypto.subtle.digest(
+
"SHA-256",
+
new TextEncoder().encode(verifier),
+
);
+
const challenge = base64url(digest as ArrayBuffer);
+
return { verifier, challenge };
+
}
+
+
function cleanPastedCode(input: string) {
+
let v = input.trim();
+
v = v.replace(/^code\s*[:=]\s*/i, "");
+
v = v.replace(/^["'`]/, "").replace(/["'`]$/, "");
+
const m = v.match(/[A-Za-z0-9._~-]+(?:#[A-Za-z0-9._~-]+)?/);
+
if (m) return m[0];
+
return v;
+
}
+
+
async function exchangeAuthorizationCode(code: string, verifier: string) {
+
const cleaned = cleanPastedCode(code);
+
const [pure, state = ""] = cleaned.split("#");
+
const body = {
+
code: pure ?? "",
+
state: state ?? "",
+
grant_type: "authorization_code",
+
client_id: CLIENT_ID,
+
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
+
code_verifier: verifier,
+
} as Record<string, string>;
+
const res = await fetch("https://console.anthropic.com/v1/oauth/token", {
+
method: "POST",
+
headers: {
+
"content-type": "application/json",
+
"user-agent": "CRUSH/1.0",
+
},
+
body: JSON.stringify(body),
+
});
+
if (!res.ok) throw new Error(`code exchange failed: ${res.status}`);
+
return (await res.json()) as {
+
access_token: string;
+
refresh_token: string;
+
expires_in: number;
+
};
+
}
+
+
// Try to bootstrap from disk and exit if successful
+
const didBootstrap = await bootstrapFromDisk();
+
+
const argv = process.argv.slice(2);
+
if (argv.includes("-h") || argv.includes("--help")) {
+
console.log(`Usage: anthropic\n`);
+
console.log(
+
` anthropic Start UI and flow; prints token on success and exits.`,
+
);
+
console.log(` PORT=xxxx anthropic Override port (default 8787).`);
+
console.log(
+
`\nTokens are cached at ~/.config/crush/anthropic and reused on later runs.\n`,
+
);
+
process.exit(0);
+
}
+
+
const indexHtml = `
+
<!doctype html>
+
<html>
+
<head>
+
<meta charset="utf-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
<title>Anthropic Auth</title>
+
<style>
+
body {
+
font-family:
+
system-ui,
+
-apple-system,
+
Segoe UI,
+
Roboto,
+
Ubuntu,
+
Cantarell,
+
Noto Sans,
+
sans-serif;
+
background: #0f0f10;
+
color: #fff;
+
margin: 0;
+
display: flex;
+
min-height: 100vh;
+
align-items: center;
+
justify-content: center;
+
}
+
.card {
+
background: #1a1a1b;
+
border: 1px solid #2b2b2c;
+
border-radius: 14px;
+
padding: 28px;
+
max-width: 560px;
+
width: 100%;
+
}
+
h1 {
+
margin: 0 0 8px;
+
}
+
p {
+
color: #9aa0a6;
+
}
+
button,
+
a.button {
+
background: linear-gradient(135deg, #ff6b35, #ff8e53);
+
color: #fff;
+
border: none;
+
border-radius: 10px;
+
padding: 12px 16px;
+
font-weight: 600;
+
cursor: pointer;
+
text-decoration: none;
+
display: inline-block;
+
}
+
textarea {
+
width: 100%;
+
min-height: 120px;
+
background: #111;
+
border: 1px solid #2b2b2c;
+
border-radius: 10px;
+
color: #fff;
+
padding: 10px;
+
}
+
.row {
+
margin: 16px 0;
+
}
+
.muted {
+
color: #9aa0a6;
+
}
+
.status {
+
margin-top: 8px;
+
font-size: 14px;
+
}
+
</style>
+
<script type="module" crossorigin src="../anthropic-api-key/index-9f070n0a.js"></script></head>
+
<body>
+
<div class="card">
+
<h1>Anthropic Authentication</h1>
+
<p class="muted">
+
Start the OAuth flow, authorize in the new tab, then paste the
+
returned token here.
+
</p>
+
+
<div class="row">
+
<a
+
id="authlink"
+
class="button"
+
href="#"
+
target="_blank"
+
style="display: none"
+
>Open Anthropic Authorization</a
+
>
+
</div>
+
+
<div class="row">
+
<label for="code">Authorization code</label>
+
<textarea
+
id="code"
+
placeholder="Paste the exact code shown by Anthropic (not a URL). If it includes a #, keep the part after it too."
+
></textarea>
+
</div>
+
+
<div class="row">
+
<button id="complete">Complete Authentication</button>
+
</div>
+
+
<div id="status" class="status"></div>
+
</div>
+
+
<script>
+
let verifier = "";
+
const statusEl = document.getElementById("status");
+
+
function setStatus(msg, ok) {
+
statusEl.textContent = msg;
+
statusEl.style.color = ok ? "#34a853" : "#ea4335";
+
}
+
+
(async () => {
+
setStatus("Preparing authorization...", true);
+
const res = await fetch("/api/auth/start", { method: "POST" });
+
if (!res.ok) {
+
setStatus("Failed to prepare auth", false);
+
return;
+
}
+
const data = await res.json();
+
verifier = data.verifier;
+
const a = document.getElementById("authlink");
+
a.href = data.authUrl;
+
a.style.display = "inline-block";
+
setStatus(
+
'Ready. Click "Open Authorization" to continue.',
+
true,
+
);
+
})();
+
+
const completeBtn = document.getElementById("complete");
+
document
+
.getElementById("complete")
+
.addEventListener("click", async () => {
+
if (completeBtn.disabled) return;
+
completeBtn.disabled = true;
+
const code = document.getElementById("code").value.trim();
+
if (!code || !verifier) {
+
setStatus(
+
"Missing code or verifier. Click Start first.",
+
false,
+
);
+
completeBtn.disabled = false;
+
return;
+
}
+
const res = await fetch("/api/auth/complete", {
+
method: "POST",
+
headers: { "content-type": "application/json" },
+
body: JSON.stringify({ code, verifier }),
+
});
+
if (!res.ok) {
+
setStatus("Code exchange failed", false);
+
completeBtn.disabled = false;
+
return;
+
}
+
setStatus("Authenticated! Fetching token...", true);
+
const t = await fetch("/api/token");
+
if (!t.ok) {
+
setStatus("Could not fetch token", false);
+
completeBtn.disabled = false;
+
return;
+
}
+
const tok = await t.json();
+
setStatus(
+
"Access token acquired (expires " +
+
new Date(tok.expiresAt * 1000).toLocaleString() +
+
")",
+
true,
+
);
+
setTimeout(() => {
+
try {
+
window.close();
+
} catch {}
+
}, 500);
+
});
+
</script>
+
</body>
+
</html>
+
`;
+
+
if (!didBootstrap) {
+
// Only start the server and open the browser if we didn't bootstrap from disk
+
const memory = new Map<
+
string,
+
{ accessToken: string; refreshToken: string; expiresAt: number }
+
>();
+
const app = express();
+
app.use(express.json());
+
+
app.post("/api/auth/start", async (_req, res) => {
+
const { verifier, challenge } = await pkcePair();
+
const authUrl = authorizeUrl(verifier, challenge);
+
json(res, { authUrl, verifier });
+
});
+
+
app.post("/api/auth/complete", async (req, res) => {
+
const body = req.body as { code?: string; verifier?: string };
+
const code = String(body.code ?? "");
+
const verifier = String(body.verifier ?? "");
+
if (!code || !verifier)
+
return json(res, { error: "missing code or verifier" }, 400);
+
const tokens = await exchangeAuthorizationCode(code, verifier);
+
const expiresAt = Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0);
+
const entry = {
+
accessToken: tokens.access_token,
+
refreshToken: tokens.refresh_token,
+
expiresAt,
+
};
+
memory.set("tokens", entry);
+
await saveToDisk(entry);
+
console.log(`${entry.accessToken}\n`);
+
setTimeout(() => process.exit(0), 100);
+
json(res, { ok: true });
+
});
+
+
app.get("/api/token", async (_req, res) => {
+
let entry = memory.get("tokens");
+
if (!entry) {
+
const disk = await loadFromDisk();
+
if (disk) {
+
entry = disk;
+
memory.set("tokens", entry);
+
}
+
}
+
if (!entry) return json(res, { error: "not_authenticated" }, 401);
+
const now = Math.floor(Date.now() / 1000);
+
if (now >= entry.expiresAt - 60) {
+
const refreshed = await exchangeRefreshToken(entry.refreshToken);
+
entry.accessToken = refreshed.access_token;
+
entry.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in;
+
if (refreshed.refresh_token) entry.refreshToken = refreshed.refresh_token;
+
memory.set("tokens", entry);
+
await saveToDisk(entry);
+
}
+
json(res, {
+
accessToken: entry.accessToken,
+
expiresAt: entry.expiresAt,
+
});
+
});
+
+
app.get("/", (_req, res) => {
+
res.setHeader("content-type", "text/html; charset=utf-8");
+
res.send(indexHtml);
+
});
+
+
app.use((_req, res) => {
+
res.status(404).send("something went wrong and your request fell through");
+
});
+
+
const server = createServer(app);
+
server.listen(PORT, async () => {
+
const url = `http://localhost:${PORT}`;
+
await open(url);
+
});
+
}
+101
src/lib/token.ts
···
···
+
import { chmodSync, existsSync } from "node:fs";
+
import fs from "node:fs/promises";
+
import path from "node:path";
+
+
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
+
+
const HOME = process.env.HOME || process.env.USERPROFILE || ".";
+
const CACHE_DIR = path.join(HOME, ".config/crush/anthropic");
+
const BEARER_FILE = path.join(CACHE_DIR, "bearer_token");
+
const REFRESH_FILE = path.join(CACHE_DIR, "refresh_token");
+
const EXPIRES_FILE = path.join(CACHE_DIR, "bearer_token.expires");
+
+
export type TokenEntry = {
+
accessToken: string;
+
refreshToken: string;
+
expiresAt: number;
+
};
+
+
export async function ensureDir() {
+
await fs.mkdir(CACHE_DIR, { recursive: true });
+
}
+
+
export async function writeSecret(filePath: string, data: string) {
+
await fs.writeFile(filePath, data, { encoding: "utf8", mode: 0o600 });
+
chmodSync(filePath, 0o600);
+
}
+
+
export async function readText(filePath: string) {
+
if (!existsSync(filePath)) return undefined;
+
return await fs.readFile(filePath, "utf8");
+
}
+
+
export async function loadFromDisk(): Promise<TokenEntry | undefined> {
+
const [bearer, refresh, expires] = await Promise.all([
+
readText(BEARER_FILE),
+
readText(REFRESH_FILE),
+
readText(EXPIRES_FILE),
+
]);
+
if (!bearer || !refresh || !expires) return undefined;
+
const exp = Number.parseInt(expires, 10) || 0;
+
return {
+
accessToken: bearer.trim(),
+
refreshToken: refresh.trim(),
+
expiresAt: exp,
+
};
+
}
+
+
export async function saveToDisk(entry: TokenEntry) {
+
await ensureDir();
+
await writeSecret(BEARER_FILE, `${entry.accessToken}\n`);
+
await writeSecret(REFRESH_FILE, `${entry.refreshToken}\n`);
+
await writeSecret(EXPIRES_FILE, `${String(entry.expiresAt)}\n`);
+
}
+
+
export async function exchangeRefreshToken(refreshToken: string) {
+
const res = await fetch("https://console.anthropic.com/v1/oauth/token", {
+
method: "POST",
+
headers: {
+
"content-type": "application/json",
+
"user-agent": "anthropic",
+
},
+
body: JSON.stringify({
+
grant_type: "refresh_token",
+
refresh_token: refreshToken,
+
client_id: CLIENT_ID,
+
}),
+
});
+
if (!res.ok) throw new Error(`refresh failed: ${res.status}`);
+
return (await res.json()) as {
+
access_token: string;
+
refresh_token?: string;
+
expires_in: number;
+
};
+
}
+
+
/**
+
* Attempts to load a valid token from disk, refresh if needed, and print it to stdout.
+
* Returns true if a valid token was found and printed, false otherwise.
+
*/
+
export async function bootstrapFromDisk(): Promise<boolean> {
+
const entry = await loadFromDisk();
+
if (!entry) return false;
+
const now = Math.floor(Date.now() / 1000);
+
if (now < entry.expiresAt - 60) {
+
process.stdout.write(`${entry.accessToken}\n`);
+
setTimeout(() => process.exit(0), 50);
+
return true;
+
}
+
try {
+
const refreshed = await exchangeRefreshToken(entry.refreshToken);
+
entry.accessToken = refreshed.access_token;
+
entry.expiresAt = Math.floor(Date.now() / 1000) + refreshed.expires_in;
+
if (refreshed.refresh_token) entry.refreshToken = refreshed.refresh_token;
+
await saveToDisk(entry);
+
process.stdout.write(`${entry.accessToken}\n`);
+
setTimeout(() => process.exit(0), 50);
+
return true;
+
} catch {
+
return false;
+
}
+
}
-1
src/server.ts
···
-
export {};
···