hugex-gh / app /lib /github-app.server.ts
drbh
feat: trigger on issue
e06b71c
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<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,
},
});
}
/**
* 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();