/** * Production Supabase Management API Client for VC-SYS CLI * Direct TypeScript port from frontend with auto-refresh token handling */ import fetch from 'node-fetch'; import { SupabaseAuth } from '../auth/supabase-auth'; import { Logger } from '../core/logger'; import { SupabaseProject, CreateProjectRequest, ProjectApiKeys, Organization } from '../../types/provisioning'; export class SupabaseManagementError extends Error { public readonly status?: number; public readonly requiresReauth: boolean; constructor(message: string, status?: number, requiresReauth = false) { super(message); this.name = 'SupabaseManagementError'; this.status = status; this.requiresReauth = requiresReauth; } } export class SupabaseManagementClient { private baseUrl: string = 'https://api.supabase.com/v1'; private logger: Logger; private auth: SupabaseAuth; constructor() { this.logger = new Logger('SupabaseManagementClient'); this.auth = new SupabaseAuth(); } /** * Makes authenticated request to Supabase Management API with automatic token refresh * EXACT PORT from frontend with CLI error handling */ private async makeRequest(endpoint: string, options: any = {}): Promise { // Check if user has valid tokens (SAME AS FRONTEND) if (!(await this.auth.isAuthenticated())) { throw new SupabaseManagementError( 'No valid OAuth tokens found. Run: vcsys auth login', 401, true ); } // Ensure we have a valid access token (handles refresh automatically) const accessToken = await this.auth.ensureValidToken(); if (!accessToken) { throw new SupabaseManagementError( 'Failed to obtain valid access token. Run: vcsys auth login', 401, true ); } // Use node-fetch for consistency (imported at top) try { // Make the API request (EXACT SAME LOGIC AS FRONTEND) const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'Accept': 'application/json', ...(options.headers as Record || {}), }, body: options.body ? JSON.stringify(options.body) : undefined, } as any); // Handle authentication errors (SAME AS FRONTEND) if (response.status === 401 || response.status === 403) { this.logger.error('Authentication failed', { status: response.status, endpoint }); throw new SupabaseManagementError( 'Authentication failed - run: vcsys auth login', response.status, true ); } if (!response.ok) { let errorMessage = `API request failed: ${response.status}`; try { const errorData: any = await response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { try { errorMessage = await response.text() || errorMessage; } catch { // Use default error message } } this.logger.error('API request failed', { status: response.status, endpoint, error: errorMessage }); throw new SupabaseManagementError(errorMessage, response.status); } const data = await response.json(); this.logger.debug('API request successful', { endpoint, dataType: typeof data }); return data; } catch (error) { if (error instanceof SupabaseManagementError) { throw error; } this.logger.error('Network or parsing error', { endpoint, error }); throw new SupabaseManagementError( `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Lists all organizations the user has access to * EXACT PORT from frontend */ async listOrganizations(): Promise { return this.makeRequest('/organizations'); } /** * Lists all projects in an organization * EXACT PORT from frontend */ async listProjects(organizationId?: string): Promise { const endpoint = organizationId ? `/organizations/${organizationId}/projects` : '/projects'; return this.makeRequest(endpoint); } /** * Creates a new Supabase project * EXACT PORT from frontend */ async createProject(projectData: CreateProjectRequest): Promise { return this.makeRequest('/projects', { method: 'POST', body: projectData, }); } /** * Gets a specific project by reference * EXACT PORT from frontend */ async getProject(projectRef: string): Promise { return this.makeRequest(`/projects/${projectRef}`); } /** * Gets API keys for a project * EXACT PORT from frontend */ async getProjectApiKeys(projectRef: string): Promise { return this.makeRequest(`/projects/${projectRef}/api-keys`); } /** * Executes SQL on a project's database * EXACT PORT from frontend */ async executeSQL(projectRef: string, sql: string): Promise { return this.makeRequest(`/projects/${projectRef}/database/query`, { method: 'POST', body: { query: sql }, }); } /** * Gets project configuration * EXACT PORT from frontend */ async getProjectConfig(projectRef: string): Promise { return this.makeRequest(`/projects/${projectRef}/config`); } /** * Updates project configuration * EXACT PORT from frontend */ async updateProjectConfig(projectRef: string, config: any): Promise { return this.makeRequest(`/projects/${projectRef}/config`, { method: 'PATCH', body: config, }); } /** * Deletes a project * EXACT PORT from frontend */ async deleteProject(projectRef: string): Promise { await this.makeRequest(`/projects/${projectRef}`, { method: 'DELETE', }); this.logger.info('Project deleted', { projectRef }); } /** * Checks if user has valid authentication * EXACT PORT from frontend */ async isAuthenticated(): Promise { try { await this.listOrganizations(); return true; } catch (error) { return false; } } /** * Gets user profile information * EXACT PORT from frontend */ async getUserProfile(): Promise { return this.makeRequest('/profile'); } /** * Pause a project */ async pauseProject(projectRef: string): Promise { await this.makeRequest(`/projects/${projectRef}/pause`, { method: 'POST', }); this.logger.info('Project paused', { projectRef }); } /** * Resume a project */ async resumeProject(projectRef: string): Promise { await this.makeRequest(`/projects/${projectRef}/resume`, { method: 'POST', }); this.logger.info('Project resumed', { projectRef }); } /** * Get project database settings */ async getProjectDatabase(projectRef: string): Promise { return this.makeRequest(`/projects/${projectRef}/database`); } /** * Health check for the management client */ async healthCheck(): Promise { try { // Use organizations endpoint instead of profile - profile returns 404 const organizations = await this.listOrganizations(); return Array.isArray(organizations); } catch (error) { this.logger.warn('Health check failed', error); return false; } } /** * Test connection and return user info */ async testConnection(): Promise<{ isAuthenticated: boolean; organizationCount: number; userProfile?: any; error?: string; }> { try { // Only test organizations endpoint - profile endpoint returns 404 const organizations = await this.listOrganizations(); return { isAuthenticated: true, organizationCount: organizations.length }; } catch (error) { return { isAuthenticated: false, organizationCount: 0, error: error instanceof Error ? error.message : 'Unknown error' }; } } }