/** * 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 client metadata served from /client-metadata.json. * * 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 appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; const callbackUrl = `${appUrl}/api/auth/callback`; if (isDev) { // Development: Use localhost loopback client // Per ATproto spec, we encode metadata in the client_id query params // Request 'transition:generic' scope for repository write access const clientId = `http://localhost/?${new URLSearchParams({ redirect_uri: callbackUrl, scope: 'atproto transition:generic', }).toString()}`; console.log('[OAuth] Initializing development client with loopback exception'); console.log('[OAuth] client_id:', clientId); clientInstance = new NodeOAuthClient({ clientMetadata: { client_id: clientId, redirect_uris: [callbackUrl], scope: 'atproto transition:generic', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'native', token_endpoint_auth_method: 'none', dpop_bound_access_tokens: true, }, stateStore: createStateStore(), sessionStore: createSessionStore(), }); console.log('[OAuth] ✓ Development client initialized'); } else { // Production: Use client metadata from /client-metadata.json endpoint const clientId = `${appUrl}/client-metadata.json`; console.log('[OAuth] Initializing production client'); console.log('[OAuth] client_id:', clientId); console.log('[OAuth] callback_url:', callbackUrl); clientInstance = new NodeOAuthClient({ clientMetadata: { client_id: clientId, client_name: 'Ponderants', client_uri: appUrl, redirect_uris: [callbackUrl], scope: 'atproto transition:generic', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none', application_type: 'web', dpop_bound_access_tokens: true, }, stateStore: createStateStore(), sessionStore: createSessionStore(), }); console.log('[OAuth] ✓ Production client initialized'); } return clientInstance; } /** * Clear the singleton instance (mainly for testing). */ export function clearOAuthClient(): void { clientInstance = null; }