import { App } from "@octokit/app"; import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; import jwt from "jsonwebtoken"; import { createHmac } from "crypto"; // GitHub App configuration - these should be environment variables in production const GITHUB_APP_ID = process.env.GITHUB_APP_ID; const GITHUB_APP_PRIVATE_KEY = process.env.GITHUB_APP_PRIVATE_KEY; const GITHUB_APP_CLIENT_ID = process.env.GITHUB_APP_CLIENT_ID; const GITHUB_APP_CLIENT_SECRET = process.env.GITHUB_APP_CLIENT_SECRET; if (!GITHUB_APP_ID || !GITHUB_APP_PRIVATE_KEY || !GITHUB_APP_CLIENT_ID || !GITHUB_APP_CLIENT_SECRET) { console.error('❌ Missing required GitHub App environment variables:'); console.error('- GITHUB_APP_ID:', GITHUB_APP_ID ? '✅ Set' : '❌ Missing'); console.error('- GITHUB_APP_PRIVATE_KEY:', GITHUB_APP_PRIVATE_KEY ? '✅ Set' : '❌ Missing'); console.error('- GITHUB_APP_CLIENT_ID:', GITHUB_APP_CLIENT_ID ? '✅ Set' : '❌ Missing'); console.error('- GITHUB_APP_CLIENT_SECRET:', GITHUB_APP_CLIENT_SECRET ? '✅ Set' : '❌ Missing'); throw new Error('Missing required GitHub App environment variables. Please check your .env file.'); } // Log startup info (safe - no secrets) console.log('🚀 GitHub App Configuration:'); console.log('- App ID:', GITHUB_APP_ID); console.log('- Client ID:', GITHUB_APP_CLIENT_ID); console.log('- App Name:', process.env.GITHUB_APP_NAME); console.log('- Callback URL:', process.env.GITHUB_CALLBACK_URL); // For now, we'll hardcode a simple in-memory store // In production, you'd use a database const userAuthStore = new Map(); export class GitHubAppAuth { private app: App; constructor() { this.app = new App({ appId: GITHUB_APP_ID, privateKey: GITHUB_APP_PRIVATE_KEY, oauth: { clientId: GITHUB_APP_CLIENT_ID, clientSecret: GITHUB_APP_CLIENT_SECRET, }, }); } /** * Generate the installation URL for users to authorize the app */ getInstallationUrl(state?: string): string { const appName = process.env.GITHUB_APP_NAME; if (!appName) { throw new Error('GITHUB_APP_NAME environment variable is required for installation URL'); } const baseUrl = `https://github.com/apps/${appName}/installations/new`; const params = new URLSearchParams(); if (state) { params.append('state', state); } return `${baseUrl}?${params.toString()}`; } /** * Get OAuth authorization URL for user identity */ getOAuthUrl(state?: string): string { const callbackUrl = process.env.GITHUB_CALLBACK_URL; if (!callbackUrl) { throw new Error('GITHUB_CALLBACK_URL environment variable is required'); } const params = new URLSearchParams({ client_id: GITHUB_APP_CLIENT_ID, redirect_uri: callbackUrl, scope: 'user:email', state: state || '', }); return `https://github.com/login/oauth/authorize?${params.toString()}`; } /** * Exchange code for access token and get user info */ async handleCallback(code: string, state?: string) { try { console.log('🔄 Starting OAuth callback...'); const tokenResponse = await this.app.oauth.createToken({ code, }); // Handle different response structures let token; if (tokenResponse.authentication && tokenResponse.authentication.token) { token = tokenResponse.authentication.token; } else if (tokenResponse.data && tokenResponse.data.token) { token = tokenResponse.data.token; } else if (tokenResponse.token) { token = tokenResponse.token; } else if (tokenResponse.data && tokenResponse.data.access_token) { token = tokenResponse.data.access_token; } else if (tokenResponse.access_token) { token = tokenResponse.access_token; } else { console.error('❌ Could not find token in response'); throw new Error('No access token found in OAuth response'); } // Get user information - create Octokit instance directly with the token const octokit = new Octokit({ auth: token, }); const { data: user } = await octokit.rest.users.getAuthenticated(); console.log('✅ User authenticated:', user.login); // Store user auth info (in production, save to database) const userAuth = { id: user.id, login: user.login, name: user.name, email: user.email, avatar_url: user.avatar_url, token, authenticated_at: new Date().toISOString(), state, }; userAuthStore.set(user.login, userAuth); return userAuth; } catch (error: any) { console.error('❌ GitHub callback error details:'); console.error('- Error type:', error.constructor.name); console.error('- Error message:', error.message); console.error('- Error status:', error.status); if (error.request) { console.error('- Request details:'); console.error(' - URL:', error.request.url); console.error(' - Method:', error.request.method); console.error(' - Client ID used:', error.request.client_id); } if (error.response?.data) { console.error('- Response data:', error.response.data); } throw new Error('Failed to authenticate with GitHub'); } } /** * Get stored user authentication info */ getUserAuth(login: string) { return userAuthStore.get(login); } /** * Get all stored user auths (for debugging) */ getAllUserAuths() { return Array.from(userAuthStore.values()); } /** * Create an authenticated Octokit instance for a user */ async getUserOctokit(login: string) { const userAuth = this.getUserAuth(login); if (!userAuth) { throw new Error(`No authentication found for user: ${login}`); } return await this.app.oauth.getUserOctokit({ token: userAuth.token, }); } /** * Get app installation for a repository */ async getInstallationOctokit(installationId: number) { return await this.app.getInstallationOctokit(installationId); } /** * Get installation by repository owner and name */ async getInstallationByRepo(owner: string, repo: string) { try { const appOctokit = await this.app.getOctokit(); const { data } = await appOctokit.rest.apps.getRepoInstallation({ owner, repo, }); return data; } catch (error) { console.error(`Failed to get installation for repository ${owner}/${repo}:`, error); return null; } } /** * Verify webhook signature */ verifyWebhookSignature(payload: string, signature: string): boolean { const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET; if (!webhookSecret) { console.warn('GITHUB_WEBHOOK_SECRET not set, skipping signature verification'); return true; } try { const expectedSignature = `sha256=${createHmac('sha256', webhookSecret) .update(payload, 'utf8') .digest('hex')}`; return signature === expectedSignature; } catch (error) { console.error('Error verifying webhook signature:', error); return false; } } } // Singleton instance export const githubApp = new GitHubAppAuth();