/** * Authentication manager for Task Master CLI */ import { AuthCredentials, OAuthFlowOptions, AuthenticationError, AuthConfig } from './types'; import { CredentialStore } from './credential-store'; import { OAuthService } from './oauth-service'; import { SupabaseAuthClient } from '../clients/supabase-client'; /** * Authentication manager class */ export class AuthManager { private static instance: AuthManager; private credentialStore: CredentialStore; private oauthService: OAuthService; private supabaseClient: SupabaseAuthClient; private constructor(config?: Partial) { this.credentialStore = new CredentialStore(config); this.supabaseClient = new SupabaseAuthClient(); this.oauthService = new OAuthService(this.credentialStore, config); } /** * Get singleton instance */ static getInstance(config?: Partial): AuthManager { if (!AuthManager.instance) { AuthManager.instance = new AuthManager(config); } return AuthManager.instance; } /** * Get stored authentication credentials */ getCredentials(): AuthCredentials | null { return this.credentialStore.getCredentials(); } /** * Start OAuth 2.0 Authorization Code Flow with browser handling */ async authenticateWithOAuth( options: OAuthFlowOptions = {} ): Promise { return this.oauthService.authenticate(options); } /** * Get the authorization URL (for browser opening) */ getAuthorizationUrl(): string | null { return this.oauthService.getAuthorizationUrl(); } /** * Authenticate with API key * Note: This would require a custom implementation or Supabase RLS policies */ async authenticateWithApiKey(apiKey: string): Promise { const token = apiKey.trim(); if (!token || token.length < 10) { throw new AuthenticationError('Invalid API key', 'INVALID_API_KEY'); } const authData: AuthCredentials = { token, tokenType: 'api_key', userId: 'api-user', email: undefined, expiresAt: undefined, // API keys don't expire savedAt: new Date().toISOString() }; this.credentialStore.saveCredentials(authData); return authData; } /** * Refresh authentication token */ async refreshToken(): Promise { const authData = this.credentialStore.getCredentials({ allowExpired: true }); if (!authData || !authData.refreshToken) { throw new AuthenticationError( 'No refresh token available', 'NO_REFRESH_TOKEN' ); } try { // Use Supabase client to refresh the token const response = await this.supabaseClient.refreshSession( authData.refreshToken ); // Update authentication data const newAuthData: AuthCredentials = { ...authData, token: response.token, refreshToken: response.refreshToken, expiresAt: response.expiresAt, savedAt: new Date().toISOString() }; this.credentialStore.saveCredentials(newAuthData); return newAuthData; } catch (error) { throw error; } } /** * Logout and clear credentials */ async logout(): Promise { try { // First try to sign out from Supabase to revoke tokens await this.supabaseClient.signOut(); } catch (error) { // Log but don't throw - we still want to clear local credentials console.warn('Failed to sign out from Supabase:', error); } // Always clear local credentials (removes auth.json file) this.credentialStore.clearCredentials(); } /** * Check if authenticated */ isAuthenticated(): boolean { return this.credentialStore.hasValidCredentials(); } /** * Get authorization headers */ getAuthHeaders(): Record { const authData = this.getCredentials(); if (!authData) { throw new AuthenticationError( 'Not authenticated. Please authenticate first.', 'NOT_AUTHENTICATED' ); } return { Authorization: `Bearer ${authData.token}` }; } }