import { HuggingFaceUserInfo } from './session.server'; const HF_OAUTH_BASE_URL = 'https://huggingface.co/oauth'; const HF_API_BASE_URL = 'https://huggingface.co/api'; export class HuggingFaceOAuthService { private clientId: string; private clientSecret: string; private redirectUri: string; constructor() { this.clientId = process.env.HF_CLIENT_ID || ''; this.clientSecret = process.env.HF_CLIENT_SECRET || ''; // Support both variable names for backward compatibility this.redirectUri = process.env.HF_REDIRECT_URI || process.env.HF_CALLBACK_URL || ''; if (!this.clientId || !this.clientSecret || !this.redirectUri) { console.warn('⚠️ HuggingFace OAuth not fully configured. Missing environment variables:'); console.warn('- HF_CLIENT_ID:', this.clientId ? '✅ Set' : '❌ Missing'); console.warn('- HF_CLIENT_SECRET:', this.clientSecret ? '✅ Set' : '❌ Missing'); console.warn('- HF_REDIRECT_URI/HF_CALLBACK_URL:', this.redirectUri ? '✅ Set' : '❌ Missing'); } else { console.log('✅ HuggingFace OAuth configuration:'); console.log('- Client ID:', this.clientId.substring(0, 4) + '...'); console.log('- Redirect URI:', this.redirectUri); } } /** * Check if HuggingFace OAuth is properly configured */ isConfigured(): boolean { return !!(this.clientId && this.clientSecret && this.redirectUri); } /** * Generate authorization URL for HuggingFace OAuth */ getAuthorizationUrl(state: string, codeChallenge: string): string { if (!this.isConfigured()) { throw new Error('HuggingFace OAuth is not properly configured'); } // Directly use the URL constructor to avoid any serialization issues with URLSearchParams let authUrl = new URL(`${HF_OAUTH_BASE_URL}/authorize`); authUrl.searchParams.set('client_id', this.clientId); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set("scope", "openid read-repos profile jobs-api"); // Explicitly use 'profile' as required by HF authUrl.searchParams.set('redirect_uri', this.redirectUri); authUrl.searchParams.set('state', state); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); console.log('Debug - Auth URL:', authUrl.toString()); return authUrl.toString(); } /** * Exchange authorization code for access token */ async exchangeCodeForToken(code: string, codeVerifier: string): Promise { if (!this.isConfigured()) { throw new Error('HuggingFace OAuth is not properly configured'); } console.log('Debug - Token exchange parameters:'); console.log('- Code:', code.substring(0, 4) + '...'); console.log('- Code verifier length:', codeVerifier.length); console.log('- Redirect URI:', this.redirectUri); const formData = new URLSearchParams(); formData.append('grant_type', 'authorization_code'); formData.append('client_id', this.clientId); formData.append('client_secret', this.clientSecret); formData.append('code', code); formData.append('redirect_uri', this.redirectUri); formData.append('code_verifier', codeVerifier); console.log('Debug - Request body:', formData.toString()); const response = await fetch(`${HF_OAUTH_BASE_URL}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, body: formData, }); if (!response.ok) { const errorText = await response.text(); console.error('HuggingFace token exchange failed:', errorText); console.error('Response status:', response.status); console.error('Response headers:', Object.fromEntries(response.headers.entries())); throw new Error(`Failed to exchange code for token: ${response.status} - ${errorText}`); } const tokenData = await response.json(); if (!tokenData.access_token) { throw new Error('No access token received from HuggingFace'); } return tokenData.access_token; } /** * Get user information from HuggingFace API */ async getUserInfo(accessToken: string): Promise { // First try the v2 endpoint try { const response = await fetch(`${HF_API_BASE_URL}/whoami-v2`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/json', }, }); if (response.ok) { const userData = await response.json(); console.log('Debug - User data keys:', Object.keys(userData)); return { username: userData.name || '', fullName: userData.fullname || userData.name || '', email: userData.email || undefined, avatarUrl: userData.avatarUrl || undefined, }; } } catch (error) { console.log('Error with whoami-v2 endpoint, falling back to v1'); } // Fall back to v1 endpoint const response = await fetch(`${HF_API_BASE_URL}/whoami`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/json', }, }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to get HuggingFace user info:', errorText); throw new Error(`Failed to get user info: ${response.status} - ${errorText}`); } const userData = await response.json(); console.log('Debug - User data keys (v1):', Object.keys(userData)); return { username: userData.name || userData.user || '', fullName: userData.fullname || userData.name || userData.user || '', email: userData.email || undefined, avatarUrl: userData.avatarUrl || userData.avatar_url || undefined, }; } /** * Complete OAuth flow: exchange code and get user info */ async completeOAuthFlow(code: string, codeVerifier: string): Promise<{ accessToken: string; userInfo: HuggingFaceUserInfo; }> { const accessToken = await this.exchangeCodeForToken(code, codeVerifier); const userInfo = await this.getUserInfo(accessToken); return { accessToken, userInfo }; } } // Singleton instance export const huggingFaceOAuth = new HuggingFaceOAuthService();