import fetch from 'node-fetch'; export interface GitLabOAuthTokens { access_token: string; token_type: string; expires_in?: number; refresh_token: string; created_at: number; } export interface GitLabOAuthConfig { applicationId: string; secret: string; gitlabUrl?: string; } /** * Helper class for GitLab OAuth operations */ export class GitLabOAuth { private applicationId: string; private secret: string; private gitlabUrl: string; constructor(config: GitLabOAuthConfig) { this.applicationId = config.applicationId; this.secret = config.secret; this.gitlabUrl = config.gitlabUrl || 'https://gitlab.com'; } /** * Generate GitLab OAuth authorization URL */ getAuthUrl(redirectUri: string, scopes: string[] = ['read_api']): string { const params = new URLSearchParams({ client_id: this.applicationId, redirect_uri: redirectUri, response_type: 'code', scope: scopes.join(' '), }); return `${this.gitlabUrl}/oauth/authorize?${params.toString()}`; } /** * Exchange authorization code for access token */ async getTokenFromCode(code: string, redirectUri: string): Promise { const params = new URLSearchParams({ client_id: this.applicationId, client_secret: this.secret, code: code, grant_type: 'authorization_code', redirect_uri: redirectUri, }); const response = await fetch(`${this.gitlabUrl}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { let errorInfo = { status: response.status, statusText: response.statusText, error: '', error_description: '', redirectUri, }; try { const errorData = await response.json(); errorInfo.error = errorData.error || ''; errorInfo.error_description = errorData.error_description || ''; } catch { const errorText = await response.text(); errorInfo.error_description = errorText; } const errorMessage = errorInfo.error_description || errorInfo.error || `HTTP ${response.status} ${response.statusText}`; throw new Error(`GitLab token exchange failed (${response.status}): ${errorMessage} Debug Info: - Status: ${response.status} ${response.statusText} - Error: ${errorInfo.error || 'N/A'} - Description: ${errorInfo.error_description || 'N/A'} - Redirect URI used: ${redirectUri} - GitLab URL: ${this.gitlabUrl}`); } const tokens = (await response.json()) as GitLabOAuthTokens; return tokens; } /** * Refresh an expired access token */ async refreshToken(refreshToken: string): Promise { const params = new URLSearchParams({ client_id: this.applicationId, client_secret: this.secret, refresh_token: refreshToken, grant_type: 'refresh_token', }); const response = await fetch(`${this.gitlabUrl}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to refresh GitLab token: ${error}`); } const tokens = (await response.json()) as GitLabOAuthTokens; return tokens; } }