magnitude-core
Version:
Magnitude e2e testing agent
187 lines (185 loc) • 6.28 kB
JavaScript
import { promises as fs } from 'fs';
import { homedir } from 'os';
import { join, dirname } from 'path';
import crypto from 'crypto';
import { bold, cyanBright } from 'ansis';
import { exec } from 'node:child_process';
// Simple cross-platform function to open URLs
function openUrl(url) {
return new Promise((resolve, reject) => {
let command;
switch (process.platform) {
case 'darwin':
command = `open "${url}"`;
break;
case 'win32':
command = `start "${url}"`;
break;
default:
command = `xdg-open "${url}"`;
break;
}
exec(command, (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
});
}
// Constants
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
const CREDS_PATH = join(homedir(), '.magnitude', 'credentials', 'claudeCode.json');
// 1. Generate PKCE pair
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// 2. Get OAuth authorization URL
function getAuthorizationURL(pkce) {
const url = new URL('https://claude.ai/oauth/authorize');
url.searchParams.set('code', 'true');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('response_type', 'code');
url.searchParams.set('redirect_uri', 'https://console.anthropic.com/oauth/code/callback');
url.searchParams.set('scope', 'org:create_api_key user:profile user:inference');
url.searchParams.set('code_challenge', pkce.challenge);
url.searchParams.set('code_challenge_method', 'S256');
url.searchParams.set('state', pkce.verifier);
return url.toString();
}
// 3. Exchange authorization code for tokens
async function exchangeCodeForTokens(code, verifier) {
const [authCode, state] = code.split('#');
const response = await fetch('https://console.anthropic.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: authCode,
state: state,
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: 'https://console.anthropic.com/oauth/code/callback',
code_verifier: verifier,
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + (data.expires_in * 1000),
};
}
// 4. Refresh access token
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://console.anthropic.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.statusText}`);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + (data.expires_in * 1000),
};
}
// 5. Save credentials
async function saveCredentials(creds) {
await fs.mkdir(dirname(CREDS_PATH), { recursive: true });
await fs.writeFile(CREDS_PATH, JSON.stringify(creds, null, 2));
await fs.chmod(CREDS_PATH, 0o600); // Read/write for owner only
}
// 6. Load credentials
async function loadCredentials() {
try {
const data = await fs.readFile(CREDS_PATH, 'utf-8');
return JSON.parse(data);
}
catch {
return null;
}
}
// 7. Get valid access token (refresh if needed)
async function getValidAccessToken() {
const creds = await loadCredentials();
if (!creds)
return null;
// If token is still valid, return it
if (creds.expires_at > Date.now() + 60000) { // 1 minute buffer
return creds.access_token;
}
// Otherwise, refresh it
try {
const newCreds = await refreshAccessToken(creds.refresh_token);
await saveCredentials(newCreds);
return newCreds.access_token;
}
catch {
return null;
}
}
export async function completeClaudeCodeAuthFlow() {
// Try to get existing valid token
const existingToken = await getValidAccessToken();
if (existingToken)
return existingToken;
// Otherwise, go through auth flow
const pkce = generatePKCE();
const authUrl = getAuthorizationURL(pkce);
console.log(bold `Claude Code Pro/Max access token missing or expired.`);
//console.log(cyanBright`Accounts with Max plan can be used for API access.`)
console.log(cyanBright `Opening browser for authentication...`);
try {
await openUrl(authUrl);
}
catch (err) {
console.log('Could not open browser automatically');
}
//console.log('Open this URL in your browser:');
console.log(bold `\nIf browser did not open, visit:`);
console.log(authUrl);
console.log(bold `\nPaste the authorization code here:`);
const code = await new Promise((resolve) => {
process.stdin.once('data', (data) => {
resolve(data.toString().trim());
});
});
const creds = await exchangeCodeForTokens(code, pkce.verifier);
await saveCredentials(creds);
console.log(bold `\nCredentials saved!`);
return creds.access_token;
}
/*
// Usage
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'anthropic-beta': 'oauth-2025-04-20',
// make sure to exclude x-api-key
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
messages: [{ role: 'user', content: 'Hello!' }],
max_tokens: 100,
}),
});
*/