From 922ee7d90abda2dfc7e82531444082059a54ffed Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 22 Apr 2025 23:01:20 -0700 Subject: [PATCH] Handle token refresh for supabase --- src/ipc/utils/lock_utils.ts | 21 ++-- src/lib/schemas.ts | 1 + .../supabase_management_client.ts | 97 ++++++++++++++++++- src/supabase_admin/supabase_return_handler.ts | 1 + 4 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/ipc/utils/lock_utils.ts b/src/ipc/utils/lock_utils.ts index 36e5d30..afd5d73 100644 --- a/src/ipc/utils/lock_utils.ts +++ b/src/ipc/utils/lock_utils.ts @@ -1,12 +1,11 @@ -// Track app operations that are in progress -const appOperationLocks = new Map>(); +const locks = new Map>(); /** * Acquires a lock for an app operation - * @param appId The app ID to lock + * @param lockId The app ID to lock * @returns An object with release function and promise */ -export function acquireLock(appId: number): { +export function acquireLock(lockId: number | string): { release: () => void; promise: Promise; } { @@ -14,33 +13,33 @@ export function acquireLock(appId: number): { const promise = new Promise((resolve) => { release = () => { - appOperationLocks.delete(appId); + locks.delete(lockId); resolve(); }; }); - appOperationLocks.set(appId, promise); + locks.set(lockId, promise); return { release, promise }; } /** - * Executes a function with a lock on the app ID - * @param appId The app ID to lock + * Executes a function with a lock on the lock ID + * @param lockId The lock ID to lock * @param fn The function to execute with the lock * @returns Result of the function */ export async function withLock( - appId: number, + lockId: number | string, fn: () => Promise ): Promise { // Wait for any existing operation to complete - const existingLock = appOperationLocks.get(appId); + const existingLock = locks.get(lockId); if (existingLock) { await existingLock; } // Acquire a new lock - const { release, promise } = acquireLock(appId); + const { release, promise } = acquireLock(lockId); try { const result = await fn(); diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 3164c5e..dc7e8b5 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -84,6 +84,7 @@ export const SupabaseSchema = z.object({ accessToken: SecretSchema.optional(), refreshToken: SecretSchema.optional(), expiresIn: z.number().optional(), + tokenTimestamp: z.number().optional(), }); export type Supabase = z.infer; diff --git a/src/supabase_admin/supabase_management_client.ts b/src/supabase_admin/supabase_management_client.ts index 38a02f3..f32d09d 100644 --- a/src/supabase_admin/supabase_management_client.ts +++ b/src/supabase_admin/supabase_management_client.ts @@ -1,11 +1,90 @@ -import { readSettings } from "../main/settings"; +import { withLock } from "../ipc/utils/lock_utils"; +import { readSettings, writeSettings } from "../main/settings"; import { SupabaseManagementAPI } from "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( @@ -13,6 +92,22 @@ export async function getSupabaseClient(): Promise { ); } + // 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, }); diff --git a/src/supabase_admin/supabase_return_handler.ts b/src/supabase_admin/supabase_return_handler.ts index 3de3a37..1b29ca2 100644 --- a/src/supabase_admin/supabase_return_handler.ts +++ b/src/supabase_admin/supabase_return_handler.ts @@ -18,6 +18,7 @@ export function handleSupabaseOAuthReturn({ value: refreshToken, }, expiresIn, + tokenTimestamp: Math.floor(Date.now() / 1000), }, }); }