import { BrowserOAuthClient } from '@atproto/oauth-client-browser' import { Agent } from '@atproto/api' import type { NetworkConfig } from '../types.js' let oauthClient: BrowserOAuthClient | null = null let agent: Agent | null = null let currentNetworkConfig: NetworkConfig | null = null export interface AuthSession { did: string handle: string agent: Agent } export function setAuthNetworkConfig(config: NetworkConfig): void { currentNetworkConfig = config // Reset client when network changes oauthClient = null } export async function initOAuthClient(): Promise { if (oauthClient) return oauthClient const handleResolver = currentNetworkConfig?.bsky || 'https://bsky.social' const plcDirectoryUrl = currentNetworkConfig?.plc || 'https://plc.directory' oauthClient = await BrowserOAuthClient.load({ clientId: getClientId(), handleResolver, plcDirectoryUrl, }) return oauthClient } function getClientId(): string { const host = window.location.host // For localhost development if (host.includes('localhost') || host.includes('127.0.0.1')) { // client_id must start with http://localhost, redirect_uri must use 127.0.0.1 const port = window.location.port || '3000' const redirectUri = `http://127.0.0.1:${port}/` return `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent('atproto transition:generic')}` } // For production, use the client-metadata.json return `${window.location.origin}/client-metadata.json` } export async function login(handle: string): Promise { const client = await initOAuthClient() await client.signIn(handle, { scope: 'atproto transition:generic', }) } export async function handleOAuthCallback(): Promise { const params = new URLSearchParams(window.location.search) if (!params.has('code') && !params.has('state')) { return null } try { const client = await initOAuthClient() const result = await client.callback(params) agent = new Agent(result.session) // Get profile to get handle const profile = await agent.getProfile({ actor: result.session.did }) // Clear URL params window.history.replaceState({}, '', window.location.pathname) return { did: result.session.did, handle: profile.data.handle, agent, } } catch (err) { console.error('OAuth callback error:', err) return null } } export async function restoreSession(): Promise { try { const client = await initOAuthClient() const result = await client.init() if (result?.session) { agent = new Agent(result.session) const profile = await agent.getProfile({ actor: result.session.did }) return { did: result.session.did, handle: profile.data.handle, agent, } } } catch (err) { // Silently fail for CORS/network errors - don't spam console // Only log if it's not a network error if (err instanceof Error && !err.message.includes('NetworkError') && !err.message.includes('CORS')) { console.error('Session restore error:', err) } } return null } export async function logout(): Promise { // Clear all storage sessionStorage.clear() localStorage.clear() // Clear IndexedDB (used by OAuth client) const databases = await indexedDB.databases() for (const db of databases) { if (db.name) { indexedDB.deleteDatabase(db.name) } } agent = null oauthClient = null } export function getAgent(): Agent | null { return agent } export async function createPost(collection: string, title: string, content: string): Promise<{ uri: string; cid: string } | null> { if (!agent) return null try { const result = await agent.com.atproto.repo.createRecord({ repo: agent.assertDid, collection, record: { $type: collection, title, content, createdAt: new Date().toISOString(), }, }) return { uri: result.data.uri, cid: result.data.cid } } catch (err) { console.error('Create post error:', err) throw err } } export async function deleteRecord(collection: string, rkey: string): Promise { if (!agent) return false try { await agent.com.atproto.repo.deleteRecord({ repo: agent.assertDid, collection, rkey, }) return true } catch (err) { console.error('Delete record error:', err) throw err } } export async function putRecord( collection: string, rkey: string, record: Record ): Promise<{ uri: string; cid: string } | null> { if (!agent) return null try { const result = await agent.com.atproto.repo.putRecord({ repo: agent.assertDid, collection, rkey, record: { $type: collection, ...record, }, }) return { uri: result.data.uri, cid: result.data.cid } } catch (err) { console.error('Put record error:', err) throw err } }