import { withLock } from "../ipc/utils/lock_utils"; import { readSettings, writeSettings } from "../main/settings"; import { SupabaseManagementAPI, SupabaseManagementAPIError, } from "@dyad-sh/supabase-management-js"; /** * Checks if the Supabase access token is expired or about to expire * Returns true if token needs to be refreshed */ function isTokenExpired(expiresIn?: number): boolean { if (!expiresIn) return true; // Get when the token was saved (expiresIn is stored at the time of token receipt) const settings = readSettings(); const tokenTimestamp = settings.supabase?.tokenTimestamp || 0; const currentTime = Math.floor(Date.now() / 1000); // Check if the token is expired or about to expire (within 5 minutes) return currentTime >= tokenTimestamp + expiresIn - 300; } /** * Refreshes the Supabase access token using the refresh token * Updates settings with new tokens and expiration time */ export async function refreshSupabaseToken(): Promise { const settings = readSettings(); const refreshToken = settings.supabase?.refreshToken?.value; if (!isTokenExpired(settings.supabase?.expiresIn)) { return; } if (!refreshToken) { throw new Error( "Supabase refresh token not found. Please authenticate first." ); } try { // Make request to Supabase refresh endpoint const response = await fetch( "https://supabase-oauth.dyad.sh/api/connect-supabase/refresh", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ refreshToken }), } ); if (!response.ok) { throw new Error(`Token refresh failed: ${response.statusText}`); } const { accessToken, refreshToken: newRefreshToken, expiresIn, } = await response.json(); // Update settings with new tokens writeSettings({ supabase: { accessToken: { value: accessToken, }, refreshToken: { value: newRefreshToken, }, expiresIn, tokenTimestamp: Math.floor(Date.now() / 1000), // Store current timestamp }, }); } catch (error) { console.error("Error refreshing Supabase token:", error); throw error; } } // Function to get the Supabase Management API client export async function getSupabaseClient(): Promise { const settings = readSettings(); // Check if Supabase token exists in settings const supabaseAccessToken = settings.supabase?.accessToken?.value; const expiresIn = settings.supabase?.expiresIn; if (!supabaseAccessToken) { throw new Error( "Supabase access token not found. Please authenticate first." ); } // Check if token needs refreshing if (isTokenExpired(expiresIn)) { await withLock("refresh-supabase-token", refreshSupabaseToken); // Get updated settings after refresh const updatedSettings = readSettings(); const newAccessToken = updatedSettings.supabase?.accessToken?.value; if (!newAccessToken) { throw new Error("Failed to refresh Supabase access token"); } return new SupabaseManagementAPI({ accessToken: newAccessToken, }); } return new SupabaseManagementAPI({ accessToken: supabaseAccessToken, }); } export async function getSupabaseProjectName( projectId: string ): Promise { const supabase = await getSupabaseClient(); const projects = await supabase.getProjects(); const project = projects?.find((p) => p.id === projectId); return project?.name || ``; } export async function executeSupabaseSql({ supabaseProjectId, query, }: { supabaseProjectId: string; query: string; }): Promise { const supabase = await getSupabaseClient(); const result = await supabase.runQuery(supabaseProjectId, query); return JSON.stringify(result); } export async function deploySupabaseFunctions({ supabaseProjectId, functionName, content, }: { supabaseProjectId: string; functionName: string; content: string; }): Promise { const supabase = await getSupabaseClient(); const formData = new FormData(); formData.append( "metadata", JSON.stringify({ entrypoint_path: "index.ts", name: functionName, }) ); formData.append("file", new Blob([content]), "index.ts"); const response = await fetch( `https://api.supabase.com/v1/projects/${supabaseProjectId}/functions/deploy?slug=${functionName}`, { method: "POST", headers: { Authorization: `Bearer ${(supabase as any).options.accessToken}`, }, body: formData, } ); if (response.status !== 201) { throw await createResponseError(response, "create function"); } return response.json(); } async function createResponseError(response: Response, action: string) { const errorBody = await safeParseErrorResponseBody(response); return new SupabaseManagementAPIError( `Failed to ${action}: ${response.statusText} (${response.status})${ errorBody ? `: ${errorBody.message}` : "" }`, response ); } async function safeParseErrorResponseBody( response: Response ): Promise<{ message: string } | undefined> { try { const body = await response.json(); if ( typeof body === "object" && body !== null && "message" in body && typeof body.message === "string" ) { return { message: body.message }; } } catch (error) { return; } }