UNPKG

alnilam-cli

Version:

Git-native AI career coach that converts multi-year ambitions into weekly execution

340 lines (339 loc) 11.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthManager = void 0; exports.getAuthenticatedClient = getAuthenticatedClient; exports.getCurrentAuthToken = getCurrentAuthToken; exports.createAuthenticatedSupabaseClient = createAuthenticatedSupabaseClient; exports.readAuthConfig = readAuthConfig; const supabase_js_1 = require("@supabase/supabase-js"); const config_js_1 = require("./config.js"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); // Storage paths const AUTH_CONFIG_DIR = path.join(os.homedir(), '.config', 'alnilam'); const SESSION_FILE = path.join(AUTH_CONFIG_DIR, 'session.json'); // Custom storage adapter for CLI environment class CLIStorage { async getItem(key) { try { if (!fs.existsSync(SESSION_FILE)) { return null; } const sessions = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); return sessions[key] || null; } catch (error) { return null; } } async setItem(key, value) { try { // Ensure config directory exists if (!fs.existsSync(AUTH_CONFIG_DIR)) { fs.mkdirSync(AUTH_CONFIG_DIR, { recursive: true }); } let sessions = {}; if (fs.existsSync(SESSION_FILE)) { sessions = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } sessions[key] = value; fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2)); fs.chmodSync(SESSION_FILE, 0o600); // Secure file permissions } catch (error) { console.error('Failed to save session:', error); } } async removeItem(key) { try { if (!fs.existsSync(SESSION_FILE)) { return; } const sessions = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); delete sessions[key]; if (Object.keys(sessions).length === 0) { // Remove file if no sessions left fs.unlinkSync(SESSION_FILE); } else { fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2)); } } catch (error) { console.error('Failed to remove session:', error); } } } // Singleton authenticated Supabase client let authenticatedClient = null; let clientInitializing = false; /** * Get or create the authenticated Supabase client * This client automatically handles session management, token refresh, and persistence * Thread-safe to prevent race conditions in concurrent access */ function getAuthenticatedClient() { if (authenticatedClient) { return authenticatedClient; } if (clientInitializing) { // Wait for initialization to complete if another thread is already doing it // This is a simple synchronous wait since CLI is typically single-threaded while (clientInitializing && !authenticatedClient) { // Simple busy wait - in CLI context this is acceptable } return authenticatedClient; } clientInitializing = true; try { const config = (0, config_js_1.getConfig)(); // Check for stored service role key synchronously try { if (fs.existsSync(SESSION_FILE)) { const sessions = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); const serviceRoleKey = sessions['service_role_key']; if (serviceRoleKey) { // Create service role client authenticatedClient = (0, supabase_js_1.createClient)(config.supabaseUrl, serviceRoleKey); return authenticatedClient; } } } catch (error) { // Ignore errors - fallback to regular client } // Create regular client with custom storage for CLI persistence authenticatedClient = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey, { auth: { storage: new CLIStorage(), autoRefreshToken: true, persistSession: true, detectSessionInUrl: false // CLI doesn't use URL-based auth } }); return authenticatedClient; } finally { clientInitializing = false; } } /** * Authentication manager using Supabase's built-in session management */ class AuthManager { client; constructor() { this.client = getAuthenticatedClient(); } /** * Register a new user with email and password */ async register(email, password) { const { data, error } = await this.client.auth.signUp({ email, password }); return { data, error }; } /** * Login with email and password */ async login(email, password) { const { data, error } = await this.client.auth.signInWithPassword({ email, password }); return { data, error }; } /** * Check if user is currently authenticated * Uses Supabase's automatic session validation */ async isAuthenticated() { // Check for service role authentication first if (await this.isServiceRoleAuthenticated()) { return true; } const { data: { user } } = await this.client.auth.getUser(); return !!user; } /** * Get current authenticated user */ async getCurrentUser() { // Check for service role authentication first if (await this.isServiceRoleAuthenticated()) { return { user: { id: 'service-role', email: 'service-role@system', role: 'service_role' }, error: null }; } const { data: { user }, error } = await this.client.auth.getUser(); return { user, error }; } /** * Check if we're using service role authentication */ async isServiceRoleAuthenticated() { try { const storage = new CLIStorage(); const serviceRoleKey = await storage.getItem('service_role_key'); return !!serviceRoleKey; } catch (error) { return false; } } /** * Get current session information */ async getSession() { const { data: { session }, error } = await this.client.auth.getSession(); return { session, error }; } /** * Refresh the current session * Note: Supabase handles this automatically, but exposed for explicit calls */ async refreshSession() { const { data, error } = await this.client.auth.refreshSession(); return { data, error }; } /** * Send a password reset email */ async resetPassword(email, redirectTo) { const config = (0, config_js_1.getConfig)(); // Use provided redirect URL or default to Supabase auth callback const finalRedirectTo = redirectTo || `${config.supabaseUrl}/auth/v1/callback`; const { data, error } = await this.client.auth.resetPasswordForEmail(email, { redirectTo: finalRedirectTo }); return { data, error }; } /** * Set session from tokens (for handling password reset callbacks) */ async setSession(accessToken, refreshToken) { const { data, error } = await this.client.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }); return { data, error }; } /** * Update password for currently authenticated user */ async updatePassword(newPassword) { const { data, error } = await this.client.auth.updateUser({ password: newPassword }); return { data, error }; } /** * Logout and clear session */ async logout() { // Clear service role session if it exists try { const storage = new CLIStorage(); await storage.removeItem('service_role_key'); } catch (error) { // Ignore errors } // Clear regular session const { error } = await this.client.auth.signOut(); // Reset the authenticated client authenticatedClient = null; return { error }; } /** * For automation/CI: Set session from service role token */ async setServiceRoleSession(serviceRoleKey) { try { // For service role, we create a separate client const config = (0, config_js_1.getConfig)(); const serviceClient = (0, supabase_js_1.createClient)(config.supabaseUrl, serviceRoleKey); // Test the service role key by making a simple query const { error } = await serviceClient.from('goals').select('count').limit(1); if (error) { return { success: false, error: 'Invalid service role key' }; } // Store service role key in session storage for persistence const storage = new CLIStorage(); await storage.setItem('service_role_key', serviceRoleKey); // Replace the authenticated client with service role client authenticatedClient = serviceClient; this.client = serviceClient; return { success: true }; } catch (error) { return { success: false, error: error.message }; } } } exports.AuthManager = AuthManager; /** * Get the current auth token for API requests * Supabase handles token validation and refresh automatically */ async function getCurrentAuthToken() { const client = getAuthenticatedClient(); const { data: { session } } = await client.auth.getSession(); return session?.access_token || null; } /** * Create an authenticated Supabase client for direct database operations * This replaces the need for separate axios clients */ function createAuthenticatedSupabaseClient() { return getAuthenticatedClient(); } /** * Legacy function for backward compatibility * @deprecated Use AuthManager class instead */ function readAuthConfig() { // For backward compatibility during transition return null; }