192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
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<BrowserOAuthClient> {
|
|
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<void> {
|
|
const client = await initOAuthClient()
|
|
await client.signIn(handle, {
|
|
scope: 'atproto transition:generic',
|
|
})
|
|
}
|
|
|
|
export async function handleOAuthCallback(): Promise<AuthSession | null> {
|
|
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<AuthSession | null> {
|
|
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<void> {
|
|
// 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<boolean> {
|
|
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<string, unknown>
|
|
): 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
|
|
}
|
|
}
|