Flink plugin for GitHub App integration with installation management and webhook handling
npm install @flink-app/github-app-pluginA standalone Flink plugin for GitHub App integration with installation management, JWT-based authentication, webhook handling with signature validation, and GitHub API client wrapper.
- 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
``bash`
npm install @flink-app/github-app-plugin
You need to create a GitHub App to use this plugin:
1. Go to GitHub Settings > Developer settings > GitHub Apps
2. Click "New GitHub App"
3. 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!)
4. Set Repository permissions based on your needs:
- Contents: Read or Write
- Issues: Read or Write
- Pull requests: Read or Write
- etc.
5. Subscribe to Webhook events:
- Push
- Pull request
- Issues
- Installation
- etc.
6. Click "Create GitHub App"
7. 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)
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:
`bashOn macOS/Linux:
base64 -i github-app-private-key.pem | tr -d '\n'
Store the base64 encoded key in an environment variable:
`bash
.env
GITHUB_APP_PRIVATE_KEY_BASE64="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVB..."
`Important: Never commit your private key to version control!
$3
The plugin requires MongoDB to store installation data and sessions.
Quick Start
$3
`typescript
import { FlinkApp } from "@flink-app/flink";
import { githubAppPlugin } from "@flink-app/github-app-plugin";
import { Context } from "./Context";const app = new FlinkApp({
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();
`$3
The plugin does NOT include an opinionated installation callback handler. You must implement your own handler with your own authentication and authorization logic.
`typescript
// 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
$3
| 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 |$3
#### onWebhookEvent (Optional)
Called when a webhook event is received.
`typescript
onWebhookEvent: async (
params: {
event: string;
action?: string;
payload: Record;
installationId: number;
deliveryId: string;
},
ctx: Context
) => Promise;
`Example:
`typescript
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
$3
1. User navigates to:
GET /github-app/install?user_id=USER_ID
- The user_id query parameter is optional and determined by your app
2. User is redirected to GitHub's installation page
3. User selects repositories to grant access
4. User clicks "Install" or "Install & Authorize"
5. GitHub redirects back to: GET /github-app/callback?installation_id=...&state=...
6. Plugin validates the state parameter (CSRF protection)
7. Plugin fetches installation details from GitHub
8. Plugin calls your onInstallationSuccess callback
9. Plugin stores installation in MongoDB
10. User is redirected to your app$3
HTML Button:
`html
Install GitHub App
`React Component:
`typescript
function InstallGitHubApp() {
const handleInstall = () => {
const userId = getCurrentUserId(); // Your function
window.location.href = /github-app/install?user_id=${userId};
}; return ;
}
`Webhook Setup
$3
- 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.)$3
`typescript
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
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:$3
Get GitHub API client for an installation.
`typescript
const client = await ctx.plugins.githubApp.getClient(12345);
const repos = await client.getRepositories();
`$3
Get installation for a user (returns first installation if multiple exist).
`typescript
const installation = await ctx.plugins.githubApp.getInstallation("user-123");
if (installation) {
console.log(Installed on: ${installation.accountLogin});
}
`$3
Get all installations for a user.
`typescript
const installations = await ctx.plugins.githubApp.getInstallations("user-123");
installations.forEach((inst) => {
console.log(${inst.accountLogin} (${inst.accountType}));
});
`$3
Delete an installation from the database.
`typescript
await ctx.plugins.githubApp.deleteInstallation("user-123", 12345);
`$3
Check if user has access to a specific repository.
`typescript
const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess("user-123", "facebook", "react");if (!hasAccess) {
return forbidden("You do not have access to this repository");
}
`$3
Complete GitHub App installation after callback from GitHub.
`typescript
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);
}
`$3
Get raw installation access token (for advanced usage).
`typescript
const token = await ctx.plugins.githubApp.getInstallationToken(12345);
// Make custom API call with token
`$3
Clear all cached installation tokens.
`typescript
ctx.plugins.githubApp.clearTokenCache();
`GitHub API Client
The plugin provides a GitHub API client with automatic token injection:
`typescript
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.
$3
`typescript
// 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});
};
`$3
`typescript
// 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
$3
- 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
$3
- Uses RS256 algorithm with RSA private key
- Tokens expire after 10 minutes
- Automatic key format detection (PKCS#1 and PKCS#8)
$3
- HMAC-SHA256 signature validation
- Constant-time comparison to prevent timing attacks
- Rejects webhooks with invalid signatures
$3
- 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
$3
- Tokens cached in memory only (never in database)
- Automatic expiration after 55 minutes (tokens expire at 60 minutes)
- Clear cache on demand via
clearTokenCache()$3
All GitHub API calls and webhook URLs must use HTTPS in production.
Troubleshooting
$3
Issue:
invalid-private-key error on plugin initializationSolution:
- 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$3
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)
$3
Issue:
invalid-state error during callbackSolution:
- Ensure MongoDB is running and accessible
- Check session TTL hasn't expired (default: 10 minutes)
- Verify cookies are enabled
- Check clock synchronization between servers
$3
Issue: Too many GitHub API calls
Solution:
- Verify
tokenCacheTTL is set appropriately (default: 55 minutes)
- Check memory usage (tokens cached in-memory)
- Call clearTokenCache() only when necessary$3
Issue:
installation-not-found errorSolution:
- Verify user has installed the GitHub App
- Check MongoDB for installation record
- Ensure
userId matches the one stored during installationAPI Reference
See TypeScript interfaces for complete type definitions:
-
GitHubAppPluginOptions - Plugin configuration
- GitHubAppPluginContext - Context API methods
- GitHubInstallation - Installation model
- WebhookEvent - Webhook event model
- GitHubAPIClient - API client methodsExamples
See the
examples/ directory for complete working examples:-
basic-installation.ts - Basic GitHub App installation
- webhook-handling.ts - Process webhook events
- repository-access.ts - Access repositories via API client
- create-issue.ts - Create GitHub issue with permission check
- with-jwt-auth.ts - Optional integration with JWT Auth Plugin
- organization-installation.ts - Organization-level installation
- error-handling.ts - Comprehensive error handling
- multi-event-webhook.ts - Handle multiple webhook event typesProduction 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
- [ ]
onInstallationSuccess` callback implementedMIT