UNPKG

@autifyhq/muon

Version:

Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities

226 lines (225 loc) â€ĸ 8.51 kB
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import open from 'open'; const CONFIG_DIR = path.join(os.homedir(), '.muon'); const TOKEN_FILE = path.join(CONFIG_DIR, 'auth.json'); export class MuonAuth { constructor(serverUrl) { this.serverUrl = serverUrl; } ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } } async login() { try { console.log('🔐 Starting Muon CLI authentication...\n'); const deviceAuth = await this.initializeDeviceAuth(); console.log('📱 Please complete authentication in your browser:'); console.log(` Visit: ${deviceAuth.verificationUri}`); console.log(` Enter code: ${deviceAuth.userCode}\n`); console.log(` Or visit: ${deviceAuth.verificationUriComplete}\n`); console.log('🌐 Opening browser...'); await open(deviceAuth.verificationUriComplete); console.log('âŗ Waiting for authentication...'); const tokens = await this.pollForTokens(deviceAuth.deviceCode, deviceAuth.interval); this.saveTokens(tokens); console.log('✅ Authentication successful!'); console.log(` Logged in as: ${tokens.user.email}`); console.log(` Organization: ${tokens.user.organizationId}`); } catch (error) { console.error('❌ Authentication failed:', error.message); process.exit(1); } } async logout() { try { if (fs.existsSync(TOKEN_FILE)) { fs.unlinkSync(TOKEN_FILE); console.log('✅ Logged out successfully'); } else { console.log('â„šī¸ You are not currently logged in'); } } catch (error) { console.error('❌ Logout failed:', error.message); process.exit(1); } } async status() { try { const tokens = this.loadTokens(); if (tokens) { console.log('✅ You are logged in'); console.log(` Email: ${tokens.user.email}`); console.log(` Name: ${tokens.user.name}`); console.log(` Organization: ${tokens.user.organizationId}`); console.log(` Role: ${tokens.user.role}`); } else { console.log('❌ You are not logged in'); console.log(' Run "muon login" to authenticate'); } } catch (error) { console.error('❌ Failed to check status:', error.message); process.exit(1); } } getTokens() { return this.loadTokens(); } // Check if access token is expired or near expiration (within 2 minutes) isTokenExpiredOrNearExpiration(token) { try { // Decode JWT payload (without verification, just to check expiration) const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); const exp = payload.exp * 1000; // Convert to milliseconds const now = Date.now(); const twoMinutes = 2 * 60 * 1000; // Return true if token is already expired OR will expire within 2 minutes return exp - now < twoMinutes; } catch { // If we can't decode, assume it's expired return true; } } async getValidTokens() { const tokens = this.loadTokens(); if (!tokens) { return null; } if (tokens.accessToken && this.isTokenExpiredOrNearExpiration(tokens.accessToken)) { const refreshedTokens = await this.refreshTokens(); if (!refreshedTokens) { return null; } return refreshedTokens; } return tokens; } async refreshTokens() { try { const tokens = this.loadTokens(); if (!tokens || !tokens.refreshToken) { console.log('❌ No refresh token available'); return null; } console.log('🔄 Attempting to refresh tokens...'); const response = await fetch(`${this.serverUrl}/api/auth/refresh-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refreshToken: tokens.refreshToken, }), }); if (!response.ok) { const errorData = await response.json(); if (response.status === 401 && errorData.error === 'invalid_refresh_token') { this.clearTokens(); return null; } throw new Error(`Failed to refresh token: ${errorData.error}`); } const newTokens = await response.json(); this.saveTokens(newTokens); return newTokens; } catch (error) { if (error instanceof Error && error.message.includes('invalid_refresh_token')) { console.log('đŸ—‘ī¸ Clearing invalid tokens'); this.clearTokens(); } return null; } } clearTokens() { if (fs.existsSync(TOKEN_FILE)) { fs.unlinkSync(TOKEN_FILE); } } async initializeDeviceAuth() { const response = await fetch(`${this.serverUrl}/api/auth/device-authorization`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ clientName: 'Muon CLI', }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to initialize device auth: ${error}`); } return response.json(); } async pollForTokens(deviceCode, interval) { const maxAttempts = 60; // 5 minutes max let attempts = 0; while (attempts < maxAttempts) { try { const response = await fetch(`${this.serverUrl}/api/auth/device-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ deviceCode, }), }); if (response.ok) { return response.json(); } const errorData = await response.json(); if (response.status === 428 && errorData.error === 'authorization_pending') { await new Promise((resolve) => setTimeout(resolve, interval * 1000)); attempts++; continue; } if (response.status === 429 && errorData.error === 'slow_down') { await new Promise((resolve) => setTimeout(resolve, (interval + 5) * 1000)); attempts++; continue; } if (response.status === 410 && errorData.error === 'expired_token') { throw new Error('Authentication session expired. Please try again.'); } if (response.status === 403 && errorData.error === 'access_denied') { throw new Error('Authentication was denied. Please try again.'); } throw new Error(`Authentication failed: ${errorData.error}`); } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Network error during authentication'); } } throw new Error('Authentication timeout. Please try again.'); } saveTokens(tokens) { this.ensureConfigDir(); fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2)); } loadTokens() { try { if (!fs.existsSync(TOKEN_FILE)) { return null; } const data = fs.readFileSync(TOKEN_FILE, 'utf8'); return JSON.parse(data); } catch (_error) { return null; } } }