/** * OAuth Client Singleton for ATproto * * This module provides a singleton instance of NodeOAuthClient * that manages OAuth flows, DPoP proofs, and session persistence. */ import { NodeOAuthClient } from '@atproto/oauth-client-node'; import { createStateStore } from './oauth-state-store'; import { createSessionStore } from './oauth-session-store'; let clientInstance: NodeOAuthClient | null = null; /** * Get or create the singleton OAuth client instance. * * In development, uses the localhost client exception (no keys needed). * In production, uses backend service with private keys (TODO). * * The client handles: * - OAuth authorization flow with PKCE * - DPoP (Demonstrating Proof of Possession) for token requests * - Automatic token refresh * - Session persistence in SurrealDB */ export async function getOAuthClient(): Promise { if (clientInstance) { return clientInstance; } const isDev = process.env.NODE_ENV === 'development'; const callbackUrl = process.env.BLUESKY_REDIRECT_URI; if (!callbackUrl) { throw new Error('BLUESKY_REDIRECT_URI environment variable is required'); } if (isDev) { // Development: Use localhost exception // Per ATproto spec, client_id must be exactly "http://localhost" // (no port number) with metadata in query parameters const clientId = `http://localhost?${new URLSearchParams({ redirect_uri: callbackUrl, scope: 'atproto', })}`; console.log('[OAuth] Initializing development client with localhost exception'); console.log('[OAuth] client_id:', clientId); clientInstance = await NodeOAuthClient.fromClientId({ clientId, stateStore: createStateStore(), sessionStore: createSessionStore(), }); console.log('[OAuth] ✓ Development client initialized'); } else { // Production: Backend service with keys // TODO: Implement when deploying to production // See plans/oauth-dpop-implementation.md for details throw new Error( 'Production OAuth client not yet implemented. ' + 'See plans/oauth-dpop-implementation.md for production setup instructions.' ); } return clientInstance; } /** * Clear the singleton instance (mainly for testing). */ export function clearOAuthClient(): void { clientInstance = null; }