refactor: centralize constants, add error classes, and DRY test utilities

- Create src/errors.js with custom error classes (RateLimitError, AuthError, ApiError, etc.)
- Create src/utils/helpers.js with shared utilities (formatDuration, sleep)
- Create tests/helpers/http-client.cjs with shared test utilities (~250 lines deduped)
- Centralize OAuth config and other constants in src/constants.js
- Add JSDoc types to all major exported functions
- Refactor all test files to use shared http-client utilities
- Update CLAUDE.md with new architecture documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Badri Narayanan S
2025-12-24 18:11:45 +05:30
parent 712da8f7f2
commit 0edc718672
16 changed files with 641 additions and 626 deletions

View File

@@ -8,27 +8,11 @@
import crypto from 'crypto';
import http from 'http';
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_HEADERS } from './constants.js';
// Google OAuth configuration (from opencode-antigravity-auth)
const GOOGLE_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const GOOGLE_CLIENT_SECRET = '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';
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v1/userinfo';
// Local callback server configuration
const CALLBACK_PORT = 51121;
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/oauth-callback`;
// Scopes needed for Cloud Code access (matching Antigravity)
const SCOPES = [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cclog',
'https://www.googleapis.com/auth/experimentsandconfigs'
].join(' ');
import {
ANTIGRAVITY_ENDPOINT_FALLBACKS,
ANTIGRAVITY_HEADERS,
OAUTH_CONFIG
} from './constants.js';
/**
* Generate PKCE code verifier and challenge
@@ -45,16 +29,18 @@ function generatePKCE() {
/**
* Generate authorization URL for Google OAuth
* Returns the URL and the PKCE verifier (needed for token exchange)
*
* @returns {{url: string, verifier: string, state: string}} Auth URL and PKCE data
*/
export function getAuthorizationUrl() {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
client_id: OAUTH_CONFIG.clientId,
redirect_uri: OAUTH_REDIRECT_URI,
response_type: 'code',
scope: SCOPES,
scope: OAUTH_CONFIG.scopes.join(' '),
access_type: 'offline',
prompt: 'consent',
code_challenge: challenge,
@@ -63,7 +49,7 @@ export function getAuthorizationUrl() {
});
return {
url: `${GOOGLE_AUTH_URL}?${params.toString()}`,
url: `${OAUTH_CONFIG.authUrl}?${params.toString()}`,
verifier,
state
};
@@ -72,11 +58,15 @@ export function getAuthorizationUrl() {
/**
* Start a local server to receive the OAuth callback
* Returns a promise that resolves with the authorization code
*
* @param {string} expectedState - Expected state parameter for CSRF protection
* @param {number} timeoutMs - Timeout in milliseconds (default 120000)
* @returns {Promise<string>} Authorization code from OAuth callback
*/
export function startCallbackServer(expectedState, timeoutMs = 120000) {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
const url = new URL(req.url, `http://localhost:${OAUTH_CONFIG.callbackPort}`);
if (url.pathname !== '/oauth-callback') {
res.writeHead(404);
@@ -158,14 +148,14 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Close any other OAuth flows and try again.`));
reject(new Error(`Port ${OAUTH_CONFIG.callbackPort} is already in use. Close any other OAuth flows and try again.`));
} else {
reject(err);
}
});
server.listen(CALLBACK_PORT, () => {
console.log(`[OAuth] Callback server listening on port ${CALLBACK_PORT}`);
server.listen(OAUTH_CONFIG.callbackPort, () => {
console.log(`[OAuth] Callback server listening on port ${OAUTH_CONFIG.callbackPort}`);
});
// Timeout after specified duration
@@ -178,20 +168,24 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
/**
* Exchange authorization code for tokens
*
* @param {string} code - Authorization code from OAuth callback
* @param {string} verifier - PKCE code verifier
* @returns {Promise<{accessToken: string, refreshToken: string, expiresIn: number}>} OAuth tokens
*/
export async function exchangeCode(code, verifier) {
const response = await fetch(GOOGLE_TOKEN_URL, {
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
code: code,
code_verifier: verifier,
grant_type: 'authorization_code',
redirect_uri: REDIRECT_URI
redirect_uri: OAUTH_REDIRECT_URI
})
});
@@ -219,16 +213,19 @@ export async function exchangeCode(code, verifier) {
/**
* Refresh access token using refresh token
*
* @param {string} refreshToken - OAuth refresh token
* @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
*/
export async function refreshAccessToken(refreshToken) {
const response = await fetch(GOOGLE_TOKEN_URL, {
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token'
})
@@ -248,9 +245,12 @@ export async function refreshAccessToken(refreshToken) {
/**
* Get user email from access token
*
* @param {string} accessToken - OAuth access token
* @returns {Promise<string>} User's email address
*/
export async function getUserEmail(accessToken) {
const response = await fetch(GOOGLE_USERINFO_URL, {
const response = await fetch(OAUTH_CONFIG.userInfoUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
@@ -268,6 +268,9 @@ export async function getUserEmail(accessToken) {
/**
* Discover project ID for the authenticated user
*
* @param {string} accessToken - OAuth access token
* @returns {Promise<string|null>} Project ID or null if not found
*/
export async function discoverProjectId(accessToken) {
for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
@@ -308,6 +311,10 @@ export async function discoverProjectId(accessToken) {
/**
* Complete OAuth flow: exchange code and get all account info
*
* @param {string} code - Authorization code from OAuth callback
* @param {string} verifier - PKCE code verifier
* @returns {Promise<{email: string, refreshToken: string, accessToken: string, projectId: string|null}>} Complete account info
*/
export async function completeOAuthFlow(code, verifier) {
// Exchange code for tokens