/** * Secure authentication utilities that avoid environment variable race conditions */ import { spawn } from 'child_process'; import { createLogger } from '@automaker/utils'; const logger = createLogger('AuthUtils'); export interface SecureAuthEnv { [key: string]: string | undefined; } export interface AuthValidationResult { isValid: boolean; error?: string; normalizedKey?: string; } /** * Validates API key format without modifying process.env */ export function validateApiKey( key: string, provider: 'anthropic' | 'openai' | 'cursor' ): AuthValidationResult { if (!key || typeof key !== 'string' || key.trim().length === 0) { return { isValid: false, error: 'API key is required' }; } const trimmedKey = key.trim(); switch (provider) { case 'anthropic': if (!trimmedKey.startsWith('sk-ant-')) { return { isValid: false, error: 'Invalid Anthropic API key format. Should start with "sk-ant-"', }; } if (trimmedKey.length < 20) { return { isValid: false, error: 'Anthropic API key too short' }; } break; case 'openai': if (!trimmedKey.startsWith('sk-')) { return { isValid: false, error: 'Invalid OpenAI API key format. Should start with "sk-"' }; } if (trimmedKey.length < 20) { return { isValid: false, error: 'OpenAI API key too short' }; } break; case 'cursor': // Cursor API keys might have different format if (trimmedKey.length < 10) { return { isValid: false, error: 'Cursor API key too short' }; } break; } return { isValid: true, normalizedKey: trimmedKey }; } /** * Creates a secure environment object for authentication testing * without modifying the global process.env */ export function createSecureAuthEnv( authMethod: 'cli' | 'api_key', apiKey?: string, provider: 'anthropic' | 'openai' | 'cursor' = 'anthropic' ): SecureAuthEnv { const env: SecureAuthEnv = { ...process.env }; if (authMethod === 'cli') { // For CLI auth, remove the API key to force CLI authentication const envKey = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'; delete env[envKey]; } else if (authMethod === 'api_key' && apiKey) { // For API key auth, validate and set the provided key const validation = validateApiKey(apiKey, provider); if (!validation.isValid) { throw new Error(validation.error); } const envKey = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'; env[envKey] = validation.normalizedKey; } return env; } /** * Creates a temporary environment override for the current process * WARNING: This should only be used in isolated contexts and immediately cleaned up */ export function createTempEnvOverride(authEnv: SecureAuthEnv): () => void { const originalEnv = { ...process.env }; // Apply the auth environment Object.assign(process.env, authEnv); // Return cleanup function return () => { // Restore original environment Object.keys(process.env).forEach((key) => { if (!(key in originalEnv)) { delete process.env[key]; } }); Object.assign(process.env, originalEnv); }; } /** * Spawns a process with secure environment isolation */ export function spawnSecureAuth( command: string, args: string[], authEnv: SecureAuthEnv, options: { cwd?: string; timeout?: number; } = {} ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { return new Promise((resolve, reject) => { const { cwd = process.cwd(), timeout = 30000 } = options; logger.debug(`Spawning secure auth process: ${command} ${args.join(' ')}`); const child = spawn(command, args, { cwd, env: authEnv, stdio: 'pipe', shell: false, }); let stdout = ''; let stderr = ''; let isResolved = false; const timeoutId = setTimeout(() => { if (!isResolved) { child.kill('SIGTERM'); isResolved = true; reject(new Error('Authentication process timed out')); } }, timeout); child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { clearTimeout(timeoutId); if (!isResolved) { isResolved = true; resolve({ stdout, stderr, exitCode: code }); } }); child.on('error', (error) => { clearTimeout(timeoutId); if (!isResolved) { isResolved = true; reject(error); } }); }); } /** * Safely extracts environment variable without race conditions */ export function safeGetEnv(key: string): string | undefined { return process.env[key]; } /** * Checks if an environment variable would be modified without actually modifying it */ export function wouldModifyEnv(key: string, newValue: string): boolean { const currentValue = safeGetEnv(key); return currentValue !== newValue; } /** * Secure auth session management */ export class AuthSessionManager { private static activeSessions = new Map(); static createSession( sessionId: string, authMethod: 'cli' | 'api_key', apiKey?: string, provider: 'anthropic' | 'openai' | 'cursor' = 'anthropic' ): SecureAuthEnv { const env = createSecureAuthEnv(authMethod, apiKey, provider); this.activeSessions.set(sessionId, env); return env; } static getSession(sessionId: string): SecureAuthEnv | undefined { return this.activeSessions.get(sessionId); } static destroySession(sessionId: string): void { this.activeSessions.delete(sessionId); } static cleanup(): void { this.activeSessions.clear(); } } /** * Rate limiting for auth attempts to prevent abuse */ export class AuthRateLimiter { private attempts = new Map(); constructor( private maxAttempts = 5, private windowMs = 60000 ) {} canAttempt(identifier: string): boolean { const now = Date.now(); const record = this.attempts.get(identifier); if (!record || now - record.lastAttempt > this.windowMs) { this.attempts.set(identifier, { count: 1, lastAttempt: now }); return true; } if (record.count >= this.maxAttempts) { return false; } record.count++; record.lastAttempt = now; return true; } getRemainingAttempts(identifier: string): number { const record = this.attempts.get(identifier); if (!record) return this.maxAttempts; return Math.max(0, this.maxAttempts - record.count); } getResetTime(identifier: string): Date | null { const record = this.attempts.get(identifier); if (!record) return null; return new Date(record.lastAttempt + this.windowMs); } }