alnilam-cli
Version:
Git-native AI career coach that converts multi-year ambitions into weekly execution
340 lines (339 loc) • 11.9 kB
JavaScript
;
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;
}