UNPKG

mnee-cli

Version:

A CLI tool for interacting with MNEE USD

192 lines (191 loc) 7.55 kB
import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import http from 'http'; import { URL } from 'url'; import open from 'open'; const CONFIG_DIR = path.join(os.homedir(), '.mnee'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); export async function ensureConfigDir() { try { await fs.mkdir(CONFIG_DIR, { recursive: true }); } catch (error) { // Directory already exists } } export async function loadConfig() { try { await ensureConfigDir(); const data = await fs.readFile(CONFIG_FILE, 'utf-8'); return JSON.parse(data); } catch (error) { return {}; } } export async function saveConfig(config) { await ensureConfigDir(); await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); } export async function clearConfig() { try { await fs.unlink(CONFIG_FILE); } catch (error) { // File doesn't exist } } export async function startAuthFlow(apiUrl) { return new Promise((resolve, reject) => { let server = null; // Find an available port const tryPort = async (port) => { return new Promise((resolve, reject) => { const testServer = http.createServer(); testServer.listen(port, '127.0.0.1', () => { testServer.close(() => resolve(port)); }); testServer.on('error', () => { // Port is in use, try next one resolve(tryPort(port + 1)); }); }); }; (async () => { try { const port = await tryPort(8900); const redirectUri = `http://localhost:${port}/callback`; // Start local server to receive callback server = http.createServer((req, res) => { const url = new URL(req.url || '', `http://localhost:${port}`); if (url.pathname === '/callback') { const error = url.searchParams.get('error'); if (error) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;"> <div style="text-align: center;"> <h1 style="color: #dc2626;">Authentication Failed</h1> <p>${error === 'access_denied' ? 'Access was denied' : 'An error occurred during authentication'}</p> <p style="color: #666;">You can close this window and return to your terminal.</p> </div> </body> </html> `); server?.close(); reject(new Error(error)); return; } // Success page res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;"> <div style="text-align: center;"> <h1 style="color: #16a34a;">Authentication Successful!</h1> <p>You can close this window and return to your terminal.</p> </div> </body> </html> `); } }); server.listen(port, '127.0.0.1'); // Initialize auth session const initResponse = await fetch(`${apiUrl}/cli/auth/init`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirectUri }), }); if (!initResponse.ok) { const errorText = await initResponse.text(); console.error('API Error:', initResponse.status, errorText); throw new Error(`Failed to initialize authentication session: ${initResponse.status}`); } const initData = await initResponse.json(); console.log(`\nOpening browser for authentication...`); console.log(`If the browser doesn't open, visit: ${initData.authUrl}\n`); // Open browser await open(initData.authUrl); // Poll for completion let attempts = 0; const maxAttempts = 120; // 10 minutes (5 second intervals) while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds const statusResponse = await fetch(`${apiUrl}/cli/auth/status/${initData.state}`); if (!statusResponse.ok) { attempts++; continue; } const status = await statusResponse.json(); if (status.status === 'completed') { server?.close(); resolve(status); return; } else if (status.status === 'error') { throw new Error(status.message || 'Authentication failed'); } attempts++; } throw new Error('Authentication timeout'); } catch (error) { server?.close(); reject(error); } })(); }); } export async function validateToken(apiUrl, token) { try { const response = await fetch(`${apiUrl}/cli/auth/profile`, { headers: { 'Authorization': `Bearer ${token}`, }, }); return response.ok; } catch (error) { return false; } } export async function getProfile(apiUrl, token) { const response = await fetch(`${apiUrl}/cli/auth/profile`, { headers: { 'Authorization': `Bearer ${token}`, }, }); if (!response.ok) { throw new Error('Failed to get profile'); } return response.json(); } export async function logout(apiUrl, token) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout const response = await fetch(`${apiUrl}/cli/auth/logout`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, }, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok && response.status !== 404) { console.warn('⚠️ Logout API returned an error, but local session cleared.'); } } catch (error) { if (error.name === 'AbortError') { console.warn('⚠️ Logout request timed out, but local session cleared.'); } else if (error.code === 'ECONNREFUSED') { console.warn('⚠️ Could not connect to server, but local session cleared.'); } // Still proceed with local logout even if API call fails } }