UNPKG

trace.ai-cli

Version:

A powerful AI-powered CLI tool

255 lines (230 loc) 7.98 kB
const http = require('http'); const crypto = require('crypto'); const path = require('path'); const fs = require('fs').promises; const os = require('os'); const fetch = require('node-fetch'); const chalk = require('chalk'); // Load environment variables require('dotenv').config(); const SUPABASE_URL = process.env.SUPABASE_URL || 'https://lcgzszeurmhxpxuhdyvk.supabase.co'; const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxjZ3pzemV1cm1oeHB4dWhkeXZrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDUxNDIxNDQsImV4cCI6MjA2MDcxODE0NH0.AC3dQV4WXUN8Cixd97M9fczWLc3XOzD3KOa739xzn64'; const AUTH_DIR = path.join(os.homedir(), '.traceai'); const AUTH_FILE = path.join(AUTH_DIR, 'auth.json'); class AuthService { constructor() { this.session = null; } get isLoggedIn() { return this.session && this.session.access_token; } get user() { return this.session?.user || null; } get accessToken() { return this.session?.access_token || null; } async load() { try { const data = await fs.readFile(AUTH_FILE, 'utf8'); this.session = JSON.parse(data); // Check if token is expired if (this.session.expires_at && Date.now() / 1000 > this.session.expires_at) { await this._refreshToken(); } } catch { this.session = null; } } async _save() { await fs.mkdir(AUTH_DIR, { recursive: true }); await fs.writeFile(AUTH_FILE, JSON.stringify(this.session, null, 2)); } async _refreshToken() { if (!this.session?.refresh_token) { this.session = null; return; } try { const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': SUPABASE_ANON_KEY }, body: JSON.stringify({ refresh_token: this.session.refresh_token }) }); if (!res.ok) { this.session = null; return; } this.session = await res.json(); await this._save(); } catch { this.session = null; } } async loginWithGoogle() { return new Promise((resolve, reject) => { const port = 54321 + Math.floor(Math.random() * 100); const redirectUri = `http://localhost:${port}/callback`; const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${port}`); if (url.pathname === '/callback') { // Serve a page that extracts the hash fragment and sends it back res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html><body> <script> const hash = window.location.hash.substring(1); if (hash) { fetch('/token?' + hash).then(() => { document.body.innerHTML = '<h2>Signed in! This tab will close...</h2>'; setTimeout(() => window.close(), 1000); }); } else { const params = new URLSearchParams(window.location.search); const code = params.get('code'); if (code) { fetch('/code?code=' + code).then(() => { document.body.innerHTML = '<h2>Signed in! This tab will close...</h2>'; setTimeout(() => window.close(), 1000); }); } else { document.body.innerHTML = '<h2>Authentication failed.</h2>'; } } </script> <h2>Authenticating...</h2> </body></html> `); } else if (url.pathname === '/token') { // Hash fragment tokens sent via fetch const accessToken = url.searchParams.get('access_token'); const refreshToken = url.searchParams.get('refresh_token'); const expiresIn = url.searchParams.get('expires_in'); if (accessToken) { // Fetch user info try { const userRes = await fetch(`${SUPABASE_URL}/auth/v1/user`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'apikey': SUPABASE_ANON_KEY } }); const user = userRes.ok ? await userRes.json() : null; this.session = { access_token: accessToken, refresh_token: refreshToken, expires_at: Math.floor(Date.now() / 1000) + parseInt(expiresIn || '3600'), user }; await this._save(); res.writeHead(200); res.end('ok'); server.close(); resolve(this.session); } catch (err) { res.writeHead(500); res.end('error'); server.close(); reject(err); } } else { res.writeHead(400); res.end('missing token'); } } else if (url.pathname === '/code') { // Authorization code flow const code = url.searchParams.get('code'); if (code) { try { const tokenRes = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=authorization_code`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': SUPABASE_ANON_KEY }, body: JSON.stringify({ code, redirect_uri: redirectUri }) }); if (!tokenRes.ok) { throw new Error('Token exchange failed'); } this.session = await tokenRes.json(); await this._save(); res.writeHead(200); res.end('ok'); server.close(); resolve(this.session); } catch (err) { res.writeHead(500); res.end('error'); server.close(); reject(err); } } } else { res.writeHead(404); res.end(); } }); server.listen(port, () => { const authUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(redirectUri)}`; console.log(chalk.cyan('\n Opening browser for Google sign-in...')); console.log(chalk.gray(` If browser doesn't open, visit:\n ${authUrl}\n`)); // Open browser const { exec } = require('child_process'); const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; exec(`${cmd} "${authUrl}"`); }); // Timeout after 2 minutes setTimeout(() => { server.close(); reject(new Error('Authentication timed out')); }, 120000); }); } async logout() { if (this.session?.access_token) { try { await fetch(`${SUPABASE_URL}/auth/v1/logout`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.session.access_token}`, 'apikey': SUPABASE_ANON_KEY } }); } catch { // ignore } } this.session = null; try { await fs.unlink(AUTH_FILE); } catch { // ignore } } async fetchAgents() { if (!this.isLoggedIn) return []; try { const res = await fetch( `${SUPABASE_URL}/rest/v1/trace ai agents?select=id,agent_name,workflow,created_at&order=created_at.desc`, { headers: { 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${this.session.access_token}`, 'Accept': 'application/json' } } ); if (!res.ok) return []; return await res.json(); } catch { return []; } } } module.exports = AuthService;