hugex-gh / app /lib /huggingface-oauth.server.ts
drbh
fix: update scopes
c3744db
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<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;
}
/**
* Get user information from HuggingFace API
*/
async getUserInfo(accessToken: string): Promise<HuggingFaceUserInfo> {
// 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();