@flink-app/github-app-plugin
v0.12.1-alpha.45
Published
Flink plugin for GitHub App integration with installation management and webhook handling
Downloads
222
Readme
GitHub App Plugin
A standalone Flink plugin for GitHub App integration with installation management, JWT-based authentication, webhook handling with signature validation, and GitHub API client wrapper.
Features
- GitHub App installation flow with CSRF protection
- Automatic JWT signing with RSA private key (RS256 algorithm)
- Installation access token management with automatic caching and refresh
- Webhook integration with HMAC-SHA256 signature validation
- GitHub API client wrapper with automatic token injection
- Repository access verification
- Standalone plugin (works with any authentication system)
- TypeScript support with full type safety
- Auto-detection of PKCS#1 and PKCS#8 private key formats
- Configurable MongoDB collections and TTL settings
Installation
npm install @flink-app/github-app-pluginPrerequisites
1. GitHub App Setup
You need to create a GitHub App to use this plugin:
- Go to GitHub Settings > Developer settings > GitHub Apps
- Click "New GitHub App"
- Fill in the required fields:
- App Name: Your app name (e.g., "My Flink App")
- Homepage URL: Your application URL
- Webhook URL:
https://yourdomain.com/github-app/webhook - Webhook Secret: Generate a secure random string (save this!)
- Set Repository permissions based on your needs:
- Contents: Read or Write
- Issues: Read or Write
- Pull requests: Read or Write
- etc.
- Subscribe to Webhook events:
- Push
- Pull request
- Issues
- Installation
- etc.
- Click "Create GitHub App"
- After creation:
- Note the App ID
- Note the Client ID
- Generate and download the private key (PEM file)
- Generate and save the Client Secret
- Note the App Slug (optional, used in installation URL)
2. Configure Private Key
The plugin requires your GitHub App's private key in Base64 encoded format to avoid issues with line breaks in environment variables.
Encode your private key to base64:
# On macOS/Linux:
base64 -i github-app-private-key.pem | tr -d '\n'
# On Windows (PowerShell):
[Convert]::ToBase64String([IO.File]::ReadAllBytes("github-app-private-key.pem"))Store the base64 encoded key in an environment variable:
# .env
GITHUB_APP_PRIVATE_KEY_BASE64="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVB..."Important: Never commit your private key to version control!
3. MongoDB Connection
The plugin requires MongoDB to store installation data and sessions.
Quick Start
1. Configure the Plugin
import { FlinkApp } from "@flink-app/flink";
import { githubAppPlugin } from "@flink-app/github-app-plugin";
import { Context } from "./Context";
const app = new FlinkApp<Context>({
name: "My App",
db: {
uri: process.env.MONGODB_URI!,
},
plugins: [
githubAppPlugin({
// GitHub App credentials (required)
appId: process.env.GITHUB_APP_ID!,
privateKey: process.env.GITHUB_APP_PRIVATE_KEY_BASE64!, // Base64 encoded
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
clientId: process.env.GITHUB_APP_CLIENT_ID!,
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
appSlug: "my-flink-app",
// Optional: Handle webhook events
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
if (event === "push") {
console.log(`Push to ${payload.repository.full_name}`);
}
},
}),
],
});
await app.start();2. Implement Installation Callback Handler
The plugin does NOT include an opinionated installation callback handler. You must implement your own handler with your own authentication and authorization logic.
// src/handlers/github/GetGitHubInstallCallback.ts
import { GetHandler, unauthorized, badRequest, redirect } from "@flink-app/flink";
import { Context } from "../../Context";
export const Route = {
path: "/github/callback",
};
const GetGitHubInstallCallback: GetHandler<{}, {}, {}, { installation_id: string; state: string }> = async ({
ctx,
req,
}) => {
// 1. Check authentication (your way)
if (!ctx.auth?.tokenData?.userId) {
return unauthorized("Please log in to connect GitHub");
}
// 2. Parse query params
const { installation_id, state } = req.query;
if (!installation_id || !state) {
return badRequest("Missing required parameters");
}
// 3. Complete installation using plugin
const result = await ctx.plugins.githubApp.completeInstallation({
installationId: parseInt(installation_id, 10),
state,
userId: ctx.auth.tokenData.userId,
});
// 4. Handle response (your way)
if (!result.success) {
console.error("Installation failed:", result.error);
return redirect(`/settings/github?error=${result.error.code}`);
}
console.log("GitHub App installed:", result.installation);
return redirect("/settings/github?success=true");
};
export default GetGitHubInstallCallback;Configuration
GitHubAppPluginOptions
| Option | Type | Required | Default | Description |
| ----------------------------- | ---------- | -------- | ------------------------ | ------------------------------------------------- |
| appId | string | Yes | - | GitHub App ID |
| privateKey | string | Yes | - | Base64 encoded RSA private key (PKCS#1 or PKCS#8) |
| webhookSecret | string | Yes | - | Webhook secret for signature validation |
| clientId | string | Yes | - | GitHub App client ID |
| clientSecret | string | Yes | - | GitHub App client secret |
| appSlug | string | No | Auto-detected | GitHub App slug (used in installation URL) |
| baseUrl | string | No | https://api.github.com | GitHub API base URL (for GitHub Enterprise) |
| onWebhookEvent | Function | No | - | Callback for webhook events |
| sessionsCollectionName | string | No | github_app_sessions | MongoDB collection for sessions |
| installationsCollectionName | string | No | github_installations | MongoDB collection for installations |
| webhookEventsCollectionName | string | No | github_webhook_events | MongoDB collection for webhook events |
| tokenCacheTTL | number | No | 3300 (55 minutes) | Installation token cache TTL in seconds |
| sessionTTL | number | No | 600 (10 minutes) | Session TTL in seconds |
| registerRoutes | boolean | No | true | Register HTTP handlers automatically |
| logWebhookEvents | boolean | No | false | Log webhook events to MongoDB |
Callback Functions
onWebhookEvent (Optional)
Called when a webhook event is received.
onWebhookEvent: async (
params: {
event: string;
action?: string;
payload: Record<string, any>;
installationId: number;
deliveryId: string;
},
ctx: Context
) => Promise<void>;Example:
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
switch (event) {
case "push":
console.log(`Push to ${payload.repository.full_name}`);
break;
case "pull_request":
if (action === "opened") {
const client = await ctx.plugins.githubApp.getClient(installationId);
await client.createIssue(payload.repository.owner.login, payload.repository.name, {
title: "Thanks for the PR!",
body: "We appreciate your contribution.",
});
}
break;
case "installation":
if (action === "deleted") {
console.log(`Installation ${installationId} was deleted`);
}
break;
}
};Installation Flow
How Users Install Your GitHub App
- User navigates to:
GET /github-app/install?user_id=USER_ID- The
user_idquery parameter is optional and determined by your app
- The
- User is redirected to GitHub's installation page
- User selects repositories to grant access
- User clicks "Install" or "Install & Authorize"
- GitHub redirects back to:
GET /github-app/callback?installation_id=...&state=... - Plugin validates the state parameter (CSRF protection)
- Plugin fetches installation details from GitHub
- Plugin calls your
onInstallationSuccesscallback - Plugin stores installation in MongoDB
- User is redirected to your app
Initiating Installation from Your App
HTML Button:
<a href="/github-app/install?user_id=123">Install GitHub App</a>React Component:
function InstallGitHubApp() {
const handleInstall = () => {
const userId = getCurrentUserId(); // Your function
window.location.href = `/github-app/install?user_id=${userId}`;
};
return <button onClick={handleInstall}>Connect GitHub</button>;
}Webhook Setup
1. Configure Webhook in GitHub App Settings
- Webhook URL:
https://yourdomain.com/github-app/webhook - Webhook Secret: Same secret used in plugin configuration
- Events: Select events you want to receive (push, PR, issues, etc.)
2. Handle Webhook Events
onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
console.log(`Event: ${event}, Action: ${action}, Delivery: ${deliveryId}`);
// Access installation
const installation = await ctx.repos.githubInstallationRepo.findByInstallationId(installationId);
// Get API client
const client = await ctx.plugins.githubApp.getClient(installationId);
// Process event
if (event === "push") {
const commits = payload.commits;
console.log(`Received ${commits.length} commits`);
}
};3. Webhook Signature Validation
The plugin automatically validates webhook signatures using HMAC-SHA256 with constant-time comparison. Invalid signatures are rejected with a 401 status code.
Context API
The plugin exposes methods via ctx.plugins.githubApp:
getClient(installationId)
Get GitHub API client for an installation.
const client = await ctx.plugins.githubApp.getClient(12345);
const repos = await client.getRepositories();getInstallation(userId)
Get installation for a user (returns first installation if multiple exist).
const installation = await ctx.plugins.githubApp.getInstallation("user-123");
if (installation) {
console.log(`Installed on: ${installation.accountLogin}`);
}getInstallations(userId)
Get all installations for a user.
const installations = await ctx.plugins.githubApp.getInstallations("user-123");
installations.forEach((inst) => {
console.log(`${inst.accountLogin} (${inst.accountType})`);
});deleteInstallation(userId, installationId)
Delete an installation from the database.
await ctx.plugins.githubApp.deleteInstallation("user-123", 12345);hasRepositoryAccess(userId, owner, repo)
Check if user has access to a specific repository.
const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess("user-123", "facebook", "react");
if (!hasAccess) {
return forbidden("You do not have access to this repository");
}completeInstallation(params)
Complete GitHub App installation after callback from GitHub.
const result = await ctx.plugins.githubApp.completeInstallation({
installationId: 12345,
state: "csrf-state-token",
userId: "user-123",
});
if (result.success) {
console.log("Installation completed:", result.installation);
} else {
console.error("Installation failed:", result.error);
}getInstallationToken(installationId)
Get raw installation access token (for advanced usage).
const token = await ctx.plugins.githubApp.getInstallationToken(12345);
// Make custom API call with tokenclearTokenCache()
Clear all cached installation tokens.
ctx.plugins.githubApp.clearTokenCache();GitHub API Client
The plugin provides a GitHub API client with automatic token injection:
const client = await ctx.plugins.githubApp.getClient(installationId);
// Get repositories accessible by this installation
const repos = await client.getRepositories();
// Get specific repository
const repo = await client.getRepository("facebook", "react");
// Get file contents
const contents = await client.getContents("facebook", "react", "README.md");
// Create an issue
const issue = await client.createIssue("facebook", "react", {
title: "Bug Report",
body: "Found a bug...",
});
// Generic API call
const response = await client.request("GET", "/rate_limit");Authentication Integration
This plugin is auth-agnostic and works with any authentication system. You implement your own installation callback handler with your own auth logic.
Example with Session-Based Auth
// In your handler
const GetGitHubCallback: GetHandler = async ({ ctx, req }) => {
// Check session-based auth
const userId = ctx.req.session?.userId;
if (!userId) {
return unauthorized("Please log in");
}
const { installation_id, state } = req.query;
const result = await ctx.plugins.githubApp.completeInstallation({
installationId: parseInt(installation_id),
state,
userId,
});
return result.success
? redirect("/dashboard")
: redirect(`/error?code=${result.error.code}`);
};Example with JWT Auth Plugin
// In your handler with @flink-app/jwt-auth-plugin
const GetGitHubCallback: GetHandler = async ({ ctx, req }) => {
// Check JWT auth
const userId = ctx.auth?.tokenData?.userId;
if (!userId) {
return unauthorized("Please log in");
}
const { installation_id, state } = req.query;
const result = await ctx.plugins.githubApp.completeInstallation({
installationId: parseInt(installation_id),
state,
userId,
});
return result.success
? redirect("/dashboard/github")
: redirect(`/error?code=${result.error.code}`);
};Security Considerations
Private Key Management
- Store base64 encoded private key in environment variables
- Never commit private key to version control
- Encode keys using base64 before storing in environment variables
- Original key must be in PEM format (PKCS#1 or PKCS#8)
- Rotate keys periodically
JWT Signing Security
- Uses RS256 algorithm with RSA private key
- Tokens expire after 10 minutes
- Automatic key format detection (PKCS#1 and PKCS#8)
Webhook Signature Validation
- HMAC-SHA256 signature validation
- Constant-time comparison to prevent timing attacks
- Rejects webhooks with invalid signatures
CSRF Protection
- State parameter with cryptographically secure random generation
- Session stored with TTL (default: 10 minutes)
- One-time use: session deleted after successful callback
- Constant-time comparison for state validation
Token Caching Security
- Tokens cached in memory only (never in database)
- Automatic expiration after 55 minutes (tokens expire at 60 minutes)
- Clear cache on demand via
clearTokenCache()
HTTPS Requirements
All GitHub API calls and webhook URLs must use HTTPS in production.
Troubleshooting
Invalid Private Key Format
Issue: invalid-private-key error on plugin initialization
Solution:
- Ensure private key is base64 encoded before storing in environment variable
- Verify original PEM key starts with
-----BEGIN RSA PRIVATE KEY-----(PKCS#1) or-----BEGIN PRIVATE KEY-----(PKCS#8) - Use the encoding commands:
base64 -i private-key.pem | tr -d '\n'(macOS/Linux) - Ensure entire base64 string is included in environment variable with no line breaks
Webhook Signature Validation Failed
Issue: Webhooks rejected with 401 status
Solution:
- Verify webhook secret matches exactly
- Check webhook secret is set in GitHub App settings
- Ensure raw request body is used (not parsed JSON)
Installation State Mismatch
Issue: invalid-state error during callback
Solution:
- Ensure MongoDB is running and accessible
- Check session TTL hasn't expired (default: 10 minutes)
- Verify cookies are enabled
- Check clock synchronization between servers
Token Cache Not Working
Issue: Too many GitHub API calls
Solution:
- Verify
tokenCacheTTLis set appropriately (default: 55 minutes) - Check memory usage (tokens cached in-memory)
- Call
clearTokenCache()only when necessary
Installation Not Found
Issue: installation-not-found error
Solution:
- Verify user has installed the GitHub App
- Check MongoDB for installation record
- Ensure
userIdmatches the one stored during installation
API Reference
See TypeScript interfaces for complete type definitions:
GitHubAppPluginOptions- Plugin configurationGitHubAppPluginContext- Context API methodsGitHubInstallation- Installation modelWebhookEvent- Webhook event modelGitHubAPIClient- API client methods
Examples
See the examples/ directory for complete working examples:
basic-installation.ts- Basic GitHub App installationwebhook-handling.ts- Process webhook eventsrepository-access.ts- Access repositories via API clientcreate-issue.ts- Create GitHub issue with permission checkwith-jwt-auth.ts- Optional integration with JWT Auth Pluginorganization-installation.ts- Organization-level installationerror-handling.ts- Comprehensive error handlingmulti-event-webhook.ts- Handle multiple webhook event types
Production Checklist
- [ ] GitHub App created with proper permissions
- [ ] Webhook URL configured with HTTPS
- [ ] Private key stored securely in environment variables
- [ ] Webhook secret configured and stored securely
- [ ] MongoDB connection configured and tested
- [ ]
onInstallationSuccesscallback implemented - [ ] Webhook event handling implemented
- [ ] Error handling configured
- [ ] HTTPS enabled for all endpoints
- [ ] Rate limiting configured (app-level)
- [ ] Monitoring and logging set up
- [ ] Test installation flow end-to-end
- [ ] Test webhook delivery and signature validation
License
MIT
