npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-plugin

Prerequisites

1. GitHub App Setup

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)

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

  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

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 token

clearTokenCache()

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 tokenCacheTTL is 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 userId matches the one stored during installation

API 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 methods

Examples

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 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
  • [ ] onInstallationSuccess callback 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