WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
at feature/issue-4-pr-create-list-view 149 lines 3.9 kB view raw
1import type { AtpSessionData } from '@atproto/api'; 2import { AtpAgent } from '@atproto/api'; 3import { 4 clearCurrentSessionMetadata, 5 deleteSession, 6 getCurrentSessionMetadata, 7 KeychainAccessError, 8 loadSession, 9 saveCurrentSessionMetadata, 10 saveSession, 11} from './session.js'; 12 13/** 14 * API client wrapper for AT Protocol operations 15 * Integrates with session management for persistent authentication 16 */ 17export class TangledApiClient { 18 private agent: AtpAgent; 19 20 constructor(serviceUrl = 'https://bsky.social') { 21 this.agent = new AtpAgent({ service: serviceUrl }); 22 } 23 24 /** 25 * Login with identifier (handle or DID) and password 26 * Supports custom domain handles (e.g., "markbennett.ca") 27 * 28 * @param identifier - User's handle or DID 29 * @param password - App password 30 */ 31 async login(identifier: string, password: string): Promise<AtpSessionData> { 32 try { 33 const response = await this.agent.login({ identifier, password }); 34 35 if (!response.success || !response.data) { 36 throw new Error('Login failed: No session data received'); 37 } 38 39 // Ensure all required fields are present 40 const sessionData: AtpSessionData = { 41 ...response.data, 42 active: response.data.active ?? true, 43 }; 44 45 // Save session to keychain 46 await saveSession(sessionData); 47 48 // Save metadata for current session tracking 49 await saveCurrentSessionMetadata({ 50 handle: sessionData.handle, 51 did: sessionData.did, 52 pds: this.agent.service.toString(), 53 lastUsed: new Date().toISOString(), 54 }); 55 56 return sessionData; 57 } catch (error) { 58 throw new Error(`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`); 59 } 60 } 61 62 /** 63 * Logout and clear session data 64 */ 65 async logout(): Promise<void> { 66 const metadata = await getCurrentSessionMetadata(); 67 68 if (!metadata) { 69 throw new Error('No active session found'); 70 } 71 72 // Delete session from keychain 73 await deleteSession(metadata.did); 74 75 // Clear current session metadata 76 await clearCurrentSessionMetadata(); 77 } 78 79 /** 80 * Resume session from stored credentials 81 * Returns true if session was successfully resumed 82 */ 83 async resumeSession(): Promise<boolean> { 84 try { 85 const metadata = await getCurrentSessionMetadata(); 86 87 if (!metadata) { 88 return false; 89 } 90 91 const sessionData = await loadSession(metadata.did); 92 93 if (!sessionData) { 94 // Metadata exists but session data is missing - clean up 95 await clearCurrentSessionMetadata(); 96 return false; 97 } 98 99 // Resume session with agent 100 await this.agent.resumeSession(sessionData); 101 102 // Update last used timestamp 103 await saveCurrentSessionMetadata({ 104 ...metadata, 105 lastUsed: new Date().toISOString(), 106 }); 107 108 return true; 109 } catch (error) { 110 if (error instanceof KeychainAccessError) { 111 // Don't clear credentials — keychain may just be temporarily locked 112 throw error; 113 } 114 // Session resume failed (network error, expired refresh token, etc.) 115 // Don't clear credentials — the error may be transient. The user can 116 // run "auth login" explicitly if they need to re-authenticate. 117 return false; 118 } 119 } 120 121 /** 122 * Check if user is currently authenticated 123 */ 124 isAuthenticated(): boolean { 125 return !!this.agent.session; 126 } 127 128 /** 129 * Get the underlying AtpAgent instance 130 * Use this for direct API calls 131 */ 132 getAgent(): AtpAgent { 133 return this.agent; 134 } 135 136 /** 137 * Get current session data 138 */ 139 getSession(): AtpSessionData | undefined { 140 return this.agent.session; 141 } 142} 143 144/** 145 * Create a new API client instance 146 */ 147export function createApiClient(serviceUrl?: string): TangledApiClient { 148 return new TangledApiClient(serviceUrl); 149}