Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
at main 138 lines 4.3 kB view raw
1/** 2 * OAuth session management 3 * Framework-agnostic session storage and retrieval 4 */ 5 6import type { OAuthStorage } from "@tijs/atproto-storage"; 7import { NetworkError } from "@tijs/oauth-client-deno"; 8import type { 9 Logger, 10 OAuthClientInterface, 11 OAuthSessionsInterface, 12 SessionInterface, 13} from "./types.ts"; 14import { noopLogger } from "./types.ts"; 15 16/** 17 * Configuration for OAuthSessions 18 */ 19export interface OAuthSessionsConfig { 20 /** OAuth client for session restoration */ 21 oauthClient: OAuthClientInterface; 22 23 /** Storage for OAuth session data */ 24 storage: OAuthStorage; 25 26 /** Session TTL in seconds */ 27 sessionTtl: number; 28 29 /** Optional logger */ 30 logger?: Logger; 31} 32 33/** 34 * OAuth session manager - handles storing and restoring OAuth sessions 35 */ 36export class OAuthSessions implements OAuthSessionsInterface { 37 private readonly oauthClient: OAuthClientInterface; 38 private readonly storage: OAuthStorage; 39 private readonly sessionTtl: number; 40 private readonly logger: Logger; 41 42 constructor(config: OAuthSessionsConfig) { 43 this.oauthClient = config.oauthClient; 44 this.storage = config.storage; 45 this.sessionTtl = config.sessionTtl; 46 this.logger = config.logger ?? noopLogger; 47 } 48 49 /** 50 * Get OAuth session for a DID with automatic token refresh 51 */ 52 async getOAuthSession(did: string): Promise<SessionInterface | null> { 53 this.logger.debug(`Restoring OAuth session for DID: ${did}`); 54 55 try { 56 // The OAuth client's restore() method handles automatic token refresh 57 const session = await this.oauthClient.restore(did); 58 59 if (session) { 60 this.logger.info(`OAuth session restored successfully for DID: ${did}`); 61 62 // Log token expiration information if available 63 if (session.timeUntilExpiry !== undefined) { 64 const timeUntilExpiryMinutes = Math.round( 65 session.timeUntilExpiry / 1000 / 60, 66 ); 67 const wasLikelyRefreshed = session.timeUntilExpiry > (60 * 60 * 1000); // More than 1 hour 68 const now = Date.now(); 69 const expiresAt = now + session.timeUntilExpiry; 70 71 this.logger.debug(`Token status for DID ${did}:`, { 72 expiresAt: new Date(expiresAt).toISOString(), 73 currentTime: new Date(now).toISOString(), 74 timeUntilExpiryMinutes, 75 wasLikelyRefreshed, 76 hasRefreshToken: !!session.refreshToken, 77 }); 78 } 79 } else { 80 this.logger.debug(`OAuth session not found for DID: ${did}`); 81 } 82 83 return session; 84 } catch (error) { 85 // NetworkError is transient — re-throw so callers can retry or handle 86 if (error instanceof NetworkError) { 87 this.logger.warn(`Network error restoring session for DID ${did}:`, { 88 error: error.message, 89 }); 90 throw error; 91 } 92 93 // All other errors mean the session is unrecoverable (expired tokens, 94 // revoked tokens, corrupt data, deserialization failures). Return null 95 // per the method contract and clean up the dead session from storage. 96 this.logger.warn(`Session unrecoverable for DID ${did}, removing:`, { 97 error: error instanceof Error ? error.message : String(error), 98 errorName: error instanceof Error ? error.constructor.name : "Unknown", 99 }); 100 101 try { 102 await this.storage.delete(`session:${did}`); 103 } catch (cleanupError) { 104 this.logger.error(`Failed to clean up session for DID ${did}:`, { 105 error: cleanupError instanceof Error 106 ? cleanupError.message 107 : String(cleanupError), 108 }); 109 } 110 111 return null; 112 } 113 } 114 115 /** 116 * Save OAuth session to storage 117 */ 118 async saveOAuthSession(session: SessionInterface): Promise<void> { 119 this.logger.debug(`Saving OAuth session for DID: ${session.did}`); 120 121 await this.storage.set(`session:${session.did}`, session.toJSON(), { 122 ttl: this.sessionTtl, 123 }); 124 125 this.logger.info(`OAuth session saved for DID: ${session.did}`); 126 } 127 128 /** 129 * Delete OAuth session from storage 130 */ 131 async deleteOAuthSession(did: string): Promise<void> { 132 this.logger.debug(`Deleting OAuth session for DID: ${did}`); 133 134 await this.storage.delete(`session:${did}`); 135 136 this.logger.info(`OAuth session deleted for DID: ${did}`); 137 } 138}