|
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 || ''; |
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
isConfigured(): boolean { |
|
return !!(this.clientId && this.clientSecret && this.redirectUri); |
|
} |
|
|
|
|
|
|
|
|
|
getAuthorizationUrl(state: string, codeChallenge: string): string { |
|
if (!this.isConfigured()) { |
|
throw new Error('HuggingFace OAuth is not properly configured'); |
|
} |
|
|
|
|
|
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"); |
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
async exchangeCodeForToken(code: string, codeVerifier: string): Promise<string> { |
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
async getUserInfo(accessToken: string): Promise<HuggingFaceUserInfo> { |
|
|
|
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'); |
|
} |
|
|
|
|
|
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, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
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 }; |
|
} |
|
} |
|
|
|
|
|
export const huggingFaceOAuth = new HuggingFaceOAuthService(); |