import { BrowserOAuthClient } from '@atproto/oauth-client-browser' // Client metadata for the OAuth client const CLIENT_METADATA = { "client_id": "https://flushes.app/oauth-client-metadata.json", "application_type": "web" as const, "client_name": "Flushes", "client_uri": "https://flushes.app", "logo_uri": "https://flushes.app/logo.png", "tos_uri": "https://flushes.app/terms", "policy_uri": "https://flushes.app/privacy", "dpop_bound_access_tokens": true, "grant_types": ["authorization_code", "refresh_token"] as const, "redirect_uris": ["https://flushes.app/auth/callback"] as const, "response_types": ["code"] as const, "scope": "atproto transition:generic", "token_endpoint_auth_method": "none" as const } // Lazy OAuth client - only initialize on client side let _oauthClient: BrowserOAuthClient | null = null // Get or create the OAuth client instance - client-side only function getOAuthClient(): BrowserOAuthClient { // Ensure we're on the client side if (typeof window === 'undefined') { throw new Error('OAuth client can only be used on the client side') } if (!_oauthClient) { _oauthClient = new BrowserOAuthClient({ clientMetadata: CLIENT_METADATA as any, handleResolver: 'https://public.api.bsky.app', responseMode: 'fragment' }) } return _oauthClient } // Export the getter function instead of the instance export const oauthClient = { get instance() { return getOAuthClient() } } // Initialize the client - this should be called once when the app loads export async function initializeOAuthClient() { // Only run on client side if (typeof window === 'undefined') { console.log('Skipping OAuth client initialization on server side') return null } try { const client = getOAuthClient() const result = await client.init() if (result) { const { session } = result const state = 'state' in result ? result.state : null console.log(`OAuth client initialized with session for ${session.sub}`) if (state) { console.log(`User successfully authenticated with state: ${state}`) } else { console.log(`Restored previous session`) } return { session, state } } console.log('OAuth client initialized without existing session') return null } catch (error) { console.error('Failed to initialize OAuth client:', error) throw error } } // Sign in with handle/DID export async function signIn(handle: string, options?: { state?: string signal?: AbortSignal }) { // Only run on client side if (typeof window === 'undefined') { throw new Error('Sign in can only be called on the client side') } try { console.log(`Initiating OAuth flow for ${handle}`) const client = getOAuthClient() await client.signIn(handle, { state: options?.state || `signin-${Date.now()}`, signal: options?.signal }) // This will never resolve as the user gets redirected } catch (error) { console.error('OAuth sign in failed:', error) throw error } } // Restore a specific session by DID export async function restoreSession(did: string) { // Only run on client side if (typeof window === 'undefined') { throw new Error('Restore session can only be called on the client side') } try { console.log(`Restoring session for ${did}`) const client = getOAuthClient() const session = await client.restore(did) console.log(`Successfully restored session for ${session.sub}`) return session } catch (error) { console.error(`Failed to restore session for ${did}:`, error) throw error } } // Sign out the current session export async function signOut() { // Only run on client side if (typeof window === 'undefined') { throw new Error('Sign out can only be called on the client side') } try { console.log('Signing out user') // Clear any remaining localStorage items from the old implementation if (typeof localStorage !== 'undefined') { localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') localStorage.removeItem('did') localStorage.removeItem('handle') localStorage.removeItem('keyPair') localStorage.removeItem('dpopNonce') localStorage.removeItem('pdsEndpoint') localStorage.removeItem('bsky_auth_pdsEndpoint') } // The OAuth client manages its own storage // We don't have direct access to clear it, but it will handle session cleanup console.log('User signed out') } catch (error) { console.error('Error during sign out:', error) throw error } } // Event listener for session deletion/invalidation export function onSessionDeleted(callback: (event: { sub: string, cause: any }) => void) { // Only run on client side if (typeof window === 'undefined') { console.log('Skipping session deleted listener setup on server side') return } try { const client = getOAuthClient() client.addEventListener('deleted', (event: any) => { const { sub, cause } = event.detail console.error(`Session for ${sub} was invalidated:`, cause) callback({ sub, cause }) }) } catch (error) { console.error('Failed to set up session deleted listener:', error) } }