trace.ai-cli
Version:
A powerful AI-powered CLI tool
255 lines (230 loc) • 7.98 kB
JavaScript
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;