| |
| |
| |
|
|
| 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; |
| } |
|
|
| |
| |
| |
| 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': |
| |
| if (trimmedKey.length < 10) { |
| return { isValid: false, error: 'Cursor API key too short' }; |
| } |
| break; |
| } |
|
|
| return { isValid: true, normalizedKey: trimmedKey }; |
| } |
|
|
| |
| |
| |
| |
| export function createSecureAuthEnv( |
| authMethod: 'cli' | 'api_key', |
| apiKey?: string, |
| provider: 'anthropic' | 'openai' | 'cursor' = 'anthropic' |
| ): SecureAuthEnv { |
| const env: SecureAuthEnv = { ...process.env }; |
|
|
| if (authMethod === 'cli') { |
| |
| const envKey = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'; |
| delete env[envKey]; |
| } else if (authMethod === 'api_key' && apiKey) { |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| export function createTempEnvOverride(authEnv: SecureAuthEnv): () => void { |
| const originalEnv = { ...process.env }; |
|
|
| |
| Object.assign(process.env, authEnv); |
|
|
| |
| return () => { |
| |
| Object.keys(process.env).forEach((key) => { |
| if (!(key in originalEnv)) { |
| delete process.env[key]; |
| } |
| }); |
| Object.assign(process.env, originalEnv); |
| }; |
| } |
|
|
| |
| |
| |
| 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); |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| export function safeGetEnv(key: string): string | undefined { |
| return process.env[key]; |
| } |
|
|
| |
| |
| |
| export function wouldModifyEnv(key: string, newValue: string): boolean { |
| const currentValue = safeGetEnv(key); |
| return currentValue !== newValue; |
| } |
|
|
| |
| |
| |
| export class AuthSessionManager { |
| private static activeSessions = new Map<string, SecureAuthEnv>(); |
|
|
| 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(); |
| } |
| } |
|
|
| |
| |
| |
| export class AuthRateLimiter { |
| private attempts = new Map<string, { count: number; lastAttempt: number }>(); |
|
|
| 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); |
| } |
| } |
|
|