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