|
import { App } from "@octokit/app"; |
|
import { createAppAuth } from "@octokit/auth-app"; |
|
import { Octokit } from "@octokit/rest"; |
|
import jwt from "jsonwebtoken"; |
|
import { createHmac } from "crypto"; |
|
|
|
|
|
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.'); |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
const userAuthStore = new Map<string, any>(); |
|
|
|
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, |
|
}, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
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()}`; |
|
} |
|
|
|
|
|
|
|
|
|
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()}`; |
|
} |
|
|
|
|
|
|
|
|
|
async handleCallback(code: string, state?: string) { |
|
try { |
|
console.log('π Starting OAuth callback...'); |
|
|
|
const tokenResponse = await this.app.oauth.createToken({ |
|
code, |
|
}); |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
const octokit = new Octokit({ |
|
auth: token, |
|
}); |
|
|
|
const { data: user } = await octokit.rest.users.getAuthenticated(); |
|
console.log('β
User authenticated:', user.login); |
|
|
|
|
|
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'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
getUserAuth(login: string) { |
|
return userAuthStore.get(login); |
|
} |
|
|
|
|
|
|
|
|
|
getAllUserAuths() { |
|
return Array.from(userAuthStore.values()); |
|
} |
|
|
|
|
|
|
|
|
|
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, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
async getInstallationOctokit(installationId: number) { |
|
return await this.app.getInstallationOctokit(installationId); |
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
} |
|
|
|
|
|
export const githubApp = new GitHubAppAuth(); |
|
|