UNPKG

cliseo

Version:

Instant AI-Powered SEO Optimization CLI for Developers

305 lines 11 kB
import { exec } from 'child_process'; import { createServer } from 'http'; import { URL } from 'url'; import chalk from 'chalk'; import ora from 'ora'; import axios from 'axios'; import { setAuthToken, clearAuthToken, getAuthToken } from './config.js'; import { createHash, randomBytes } from 'crypto'; // Auth0 and backend configuration const AUTH0_DOMAIN = 'auth.cliseo.com'; const CLIENT_ID = 'kCZh9ll7L7RItLWLc47aOmDbffjQTmNd'; const REDIRECT_URI = 'http://localhost:8080/callback'; const API_BASE = 'https://a8iza6csua.execute-api.us-east-2.amazonaws.com'; /** * Generate PKCE codes for secure OAuth flow */ function generatePKCECodes() { // Generate code verifier (random string) const codeVerifier = randomBytes(32).toString('base64url'); // Generate code challenge (SHA256 hash of verifier) const codeChallenge = createHash('sha256') .update(codeVerifier) .digest('base64url'); return { codeVerifier, codeChallenge, }; } /** * Opens a URL in the default browser */ function openBrowser(url) { return new Promise((resolve, reject) => { const platform = process.platform; let command; if (platform === 'darwin') { command = `open "${url}"`; } else if (platform === 'win32') { command = `start "" "${url}"`; } else { command = `xdg-open "${url}"`; } exec(command, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } /** * Creates a local server to handle OAuth callback */ function createAuthServer() { return new Promise((resolve, reject) => { const server = createServer(); let authResolve; let authReject; const authPromise = new Promise((res, rej) => { authResolve = res; authReject = rej; }); server.on('request', (req, res) => { if (!req.url) return; const url = new URL(req.url, `http://localhost`); if (url.pathname === '/callback') { // Handle OAuth callback const params = url.searchParams; const authData = {}; // Check for authorization code flow if (params.has('code')) { authData.code = params.get('code') || undefined; } // Check for implicit flow tokens if (params.has('access_token')) { authData.access_token = params.get('access_token') || undefined; } if (params.has('id_token')) { authData.id_token = params.get('id_token') || undefined; } // Check for errors if (params.has('error')) { authData.error = params.get('error') || undefined; authData.error_description = params.get('error_description') || undefined; } // Send response to browser const responseHtml = authData.error ? ` <html> <head><title>cliseo - Authentication Failed</title></head> <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; text-align: center; padding: 50px;"> <h1 style="color: #e74c3c;">Authentication Failed</h1> <p>Error: ${authData.error}</p> <p>${authData.error_description || ''}</p> <p>You can close this window and try again.</p> </body> </html> ` : ` <html> <head><title>cliseo - Authentication Successful</title></head> <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; text-align: center; padding: 50px;"> <h1 style="color: #27ae60;">Authentication Successful!</h1> <p>You have successfully authenticated with cliseo.</p> <p>You can now close this window and return to your terminal.</p> <script> setTimeout(() => window.close(), 3000); </script> </body> </html> `; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(responseHtml); // Resolve the auth promise authResolve(authData); } else { // 404 for other paths res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); server.listen(8080, 'localhost', () => { resolve({ server, port: 8080, authPromise }); }); server.on('error', (error) => { reject(error); }); }); } /** * Exchange authorization code for tokens using Auth0 with PKCE */ async function exchangeCodeForTokens(code, codeVerifier) { try { const response = await axios.post(`https://${AUTH0_DOMAIN}/oauth/token`, { grant_type: 'authorization_code', client_id: CLIENT_ID, code, redirect_uri: REDIRECT_URI, code_verifier: codeVerifier, // PKCE code verifier }, { headers: { 'Content-Type': 'application/json', }, }); return response.data; } catch (error) { console.error('Error exchanging code for tokens:', error); if (axios.isAxiosError(error) && error.response) { const errorData = error.response.data; console.error('Auth0 error response:', errorData); throw new Error(errorData.error_description || errorData.error || 'Failed to exchange authorization code for tokens'); } throw new Error('Failed to exchange authorization code for tokens'); } } /** * Exchange Auth0 token for CLI token */ async function getCliToken(auth0Token) { try { const response = await axios.post(`${API_BASE}/cli-auth`, {}, { headers: { 'Authorization': `Bearer ${auth0Token}`, 'Content-Type': 'application/json', }, }); const data = response.data; return { token: data.token, email: data.email, aiAccess: data.ai_access, emailVerified: data.email_verified, requiresVerification: data.requires_verification, }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const errorData = error.response.data; throw new Error(errorData.error || 'Authentication failed'); } throw new Error('Failed to authenticate with cliseo backend'); } } /** * Main authentication function using browser OAuth flow with PKCE */ export async function authenticateUser() { const spinner = ora('Starting authentication...').start(); try { // Generate PKCE codes const pkceCodes = generatePKCECodes(); // Create local server for OAuth callback spinner.text = 'Setting up authentication server...'; const { server, authPromise } = await createAuthServer(); // Build Auth0 authorization URL with PKCE const authUrl = `https://${AUTH0_DOMAIN}/authorize?` + new URLSearchParams({ response_type: 'code', client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scope: 'openid profile email', code_challenge: pkceCodes.codeChallenge, code_challenge_method: 'S256', // SHA256 }).toString(); // Open browser spinner.text = 'Opening browser...'; try { await openBrowser(authUrl); spinner.stop(); console.log(chalk.gray(`If the browser doesn't open, visit: ${authUrl}\n`)); spinner.start('Waiting for authentication...'); } catch (error) { spinner.stop(); console.log(chalk.yellow('\n⚠️ Could not open browser automatically.')); console.log(chalk.cyan('Please visit the following URL to authenticate:')); console.log(chalk.blue(authUrl)); console.log(''); spinner.start('Waiting for authentication...'); } // Wait for OAuth callback spinner.text = 'Waiting for authentication...'; const authData = await authPromise; // Close the server server.close(); // Handle authentication errors if (authData.error) { spinner.fail('Authentication failed'); return { success: false, error: authData.error_description || authData.error, }; } // Exchange authorization code for tokens if (!authData.code) { spinner.fail('Authentication failed'); return { success: false, error: 'No authorization code received', }; } spinner.text = 'Exchanging authorization code for tokens...'; const tokens = await exchangeCodeForTokens(authData.code, pkceCodes.codeVerifier); // Get CLI token from backend spinner.text = 'Getting CLI authentication token...'; const cliAuth = await getCliToken(tokens.id_token); // Store authentication data await setAuthToken(cliAuth.token, cliAuth.email, cliAuth.aiAccess); spinner.succeed('Authentication successful!'); return { success: true, token: cliAuth.token, email: cliAuth.email, aiAccess: cliAuth.aiAccess, emailVerified: cliAuth.emailVerified, requiresVerification: cliAuth.requiresVerification, }; } catch (error) { spinner.fail('Authentication failed'); if (error instanceof Error) { return { success: false, error: error.message, }; } return { success: false, error: 'An unexpected error occurred during authentication', }; } } /** * Verify current authentication status */ export async function verifyAuthentication() { try { const token = await getAuthToken(); if (!token) { return false; } const response = await axios.post(`${API_BASE}/verify-cli-token`, {}, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); return response.data.valid === true; } catch (error) { return false; } } /** * Logout user by clearing stored tokens */ export async function logoutUser() { await clearAuthToken(); } //# sourceMappingURL=auth.js.map