File size: 6,301 Bytes
dc06026 c3744db dc06026 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
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(); |