@flink-app/github-app-plugin 2.0.0-alpha.59 → 2.0.0-alpha.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,610 +1,251 @@
1
- # GitHub App Plugin
1
+ # @flink-app/github-app-plugin
2
2
 
3
- A standalone Flink plugin for GitHub App integration with installation management, JWT-based authentication, webhook handling with signature validation, and GitHub API client wrapper.
3
+ GitHub App integration plugin for Flink. Handles installation management, JWT-based authentication, webhook signature validation, and provides an authenticated GitHub API client. Webhook events can be processed via auto-discovered handler files in `src/github-events/` or a single `onWebhookEvent` callback.
4
4
 
5
- ## Features
5
+ ## Setup
6
6
 
7
- - GitHub App installation flow with CSRF protection
8
- - Automatic JWT signing with RSA private key (RS256 algorithm)
9
- - Installation access token management with automatic caching and refresh
10
- - Webhook integration with HMAC-SHA256 signature validation
11
- - GitHub API client wrapper with automatic token injection
12
- - Repository access verification
13
- - Standalone plugin (works with any authentication system)
14
- - TypeScript support with full type safety
15
- - Auto-detection of PKCS#1 and PKCS#8 private key formats
16
- - Configurable MongoDB collections and TTL settings
17
-
18
- ## Installation
7
+ ### 1. Install
19
8
 
20
9
  ```bash
21
- npm install @flink-app/github-app-plugin
10
+ pnpm add @flink-app/github-app-plugin
22
11
  ```
23
12
 
24
- ## Prerequisites
25
-
26
- ### 1. GitHub App Setup
27
-
28
- You need to create a GitHub App to use this plugin:
29
-
30
- 1. Go to [GitHub Settings > Developer settings > GitHub Apps](https://github.com/settings/apps)
31
- 2. Click "New GitHub App"
32
- 3. Fill in the required fields:
33
- - **App Name:** Your app name (e.g., "My Flink App")
34
- - **Homepage URL:** Your application URL
35
- - **Webhook URL:** `https://yourdomain.com/github-app/webhook`
36
- - **Webhook Secret:** Generate a secure random string (save this!)
37
- 4. Set **Repository permissions** based on your needs:
38
- - Contents: Read or Write
39
- - Issues: Read or Write
40
- - Pull requests: Read or Write
41
- - etc.
42
- 5. Subscribe to **Webhook events**:
43
- - Push
44
- - Pull request
45
- - Issues
46
- - Installation
47
- - etc.
48
- 6. Click "Create GitHub App"
49
- 7. After creation:
50
- - Note the **App ID**
51
- - Note the **Client ID**
52
- - Generate and download the **private key** (PEM file)
53
- - Generate and save the **Client Secret**
54
- - Note the **App Slug** (optional, used in installation URL)
55
-
56
- ### 2. Configure Private Key
57
-
58
- The plugin requires your GitHub App's private key in **Base64 encoded** format to avoid issues with line breaks in environment variables.
59
-
60
- **Encode your private key to base64:**
61
-
62
- ```bash
63
- # On macOS/Linux:
64
- base64 -i github-app-private-key.pem | tr -d '\n'
65
-
66
- # On Windows (PowerShell):
67
- [Convert]::ToBase64String([IO.File]::ReadAllBytes("github-app-private-key.pem"))
68
- ```
13
+ ### 2. Register the compiler plugin (for auto-discovered handlers)
69
14
 
70
- **Store the base64 encoded key in an environment variable:**
15
+ ```js
16
+ // flink.config.js
17
+ const { compilerPlugin } = require("@flink-app/github-app-plugin/compiler");
71
18
 
72
- ```bash
73
- # .env
74
- GITHUB_APP_PRIVATE_KEY_BASE64="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVB..."
19
+ module.exports = {
20
+ compilerPlugins: [compilerPlugin()],
21
+ };
75
22
  ```
76
23
 
77
- **Important:** Never commit your private key to version control!
24
+ This tells the Flink compiler to scan `src/github-events/` and auto-register every file that contains `GitHubEventHandler`.
78
25
 
79
- ### 3. MongoDB Connection
26
+ ### 3. Extend your app context type
80
27
 
81
- The plugin requires MongoDB to store installation data and sessions.
28
+ ```typescript
29
+ // src/Ctx.ts
30
+ import { FlinkContext } from "@flink-app/flink";
31
+ import { GitHubAppPluginContext } from "@flink-app/github-app-plugin";
82
32
 
83
- ## Quick Start
33
+ export interface Ctx extends FlinkContext<GitHubAppPluginContext> {
34
+ repos: {
35
+ // your repos
36
+ };
37
+ }
38
+ ```
84
39
 
85
- ### 1. Configure the Plugin
40
+ ### 4. Register the runtime plugin
86
41
 
87
42
  ```typescript
88
- import { FlinkApp } from "@flink-app/flink";
89
43
  import { githubAppPlugin } from "@flink-app/github-app-plugin";
90
- import { Context } from "./Context";
91
-
92
- const app = new FlinkApp<Context>({
93
- name: "My App",
94
-
95
- db: {
96
- uri: process.env.MONGODB_URI!,
97
- },
98
44
 
45
+ const app = new FlinkApp<Ctx>({
46
+ db: { uri: process.env.MONGODB_URI! },
99
47
  plugins: [
100
48
  githubAppPlugin({
101
- // GitHub App credentials (required)
102
49
  appId: process.env.GITHUB_APP_ID!,
103
- privateKey: process.env.GITHUB_APP_PRIVATE_KEY_BASE64!, // Base64 encoded
50
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY_BASE64!,
104
51
  webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
105
52
  clientId: process.env.GITHUB_APP_CLIENT_ID!,
106
53
  clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
107
- appSlug: "my-flink-app",
108
-
109
- // Optional: Handle webhook events
110
- onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
111
- if (event === "push") {
112
- console.log(`Push to ${payload.repository.full_name}`);
113
- }
114
- },
54
+ appSlug: "my-app",
115
55
  }),
116
56
  ],
117
57
  });
118
-
119
- await app.start();
120
58
  ```
121
59
 
122
- ### 2. Implement Installation Callback Handler
123
-
124
- The plugin does NOT include an opinionated installation callback handler. You must implement your own handler with your own authentication and authorization logic.
125
-
126
- ```typescript
127
- // src/handlers/github/GetGitHubInstallCallback.ts
128
- import { GetHandler, unauthorized, badRequest, redirect } from "@flink-app/flink";
129
- import { Context } from "../../Context";
130
-
131
- export const Route = {
132
- path: "/github/callback",
133
- };
60
+ ## GitHub App Prerequisites
134
61
 
135
- const GetGitHubInstallCallback: GetHandler<{}, {}, {}, { installation_id: string; state: string }> = async ({
136
- ctx,
137
- req,
138
- }) => {
139
- // 1. Check authentication (your way)
140
- if (!ctx.auth?.tokenData?.userId) {
141
- return unauthorized("Please log in to connect GitHub");
142
- }
143
-
144
- // 2. Parse query params
145
- const { installation_id, state } = req.query;
146
- if (!installation_id || !state) {
147
- return badRequest("Missing required parameters");
148
- }
149
-
150
- // 3. Complete installation using plugin
151
- const result = await ctx.plugins.githubApp.completeInstallation({
152
- installationId: parseInt(installation_id, 10),
153
- state,
154
- userId: ctx.auth.tokenData.userId,
155
- });
156
-
157
- // 4. Handle response (your way)
158
- if (!result.success) {
159
- console.error("Installation failed:", result.error);
160
- return redirect(`/settings/github?error=${result.error.code}`);
161
- }
162
-
163
- console.log("GitHub App installed:", result.installation);
164
- return redirect("/settings/github?success=true");
165
- };
62
+ 1. Create a GitHub App at [GitHub Settings > Developer settings > GitHub Apps](https://github.com/settings/apps)
63
+ 2. Configure:
64
+ - **Webhook URL:** `https://yourdomain.com/github-app/webhook`
65
+ - **Webhook Secret:** A secure random string
66
+ - **Permissions & events:** Select what your app needs
67
+ 3. After creation, note: **App ID**, **Client ID**, **Client Secret**, **App Slug**
68
+ 4. Generate and download the **private key** (PEM file), then base64 encode it:
166
69
 
167
- export default GetGitHubInstallCallback;
70
+ ```bash
71
+ base64 -i private-key.pem | tr -d '\n'
168
72
  ```
169
73
 
170
- ## Configuration
171
-
172
- ### GitHubAppPluginOptions
74
+ Store the base64 string as `GITHUB_APP_PRIVATE_KEY_BASE64` in your environment.
173
75
 
174
- | Option | Type | Required | Default | Description |
175
- | ----------------------------- | ---------- | -------- | ------------------------ | ------------------------------------------------- |
176
- | `appId` | `string` | Yes | - | GitHub App ID |
177
- | `privateKey` | `string` | Yes | - | Base64 encoded RSA private key (PKCS#1 or PKCS#8) |
178
- | `webhookSecret` | `string` | Yes | - | Webhook secret for signature validation |
179
- | `clientId` | `string` | Yes | - | GitHub App client ID |
180
- | `clientSecret` | `string` | Yes | - | GitHub App client secret |
181
- | `appSlug` | `string` | No | Auto-detected | GitHub App slug (used in installation URL) |
182
- | `baseUrl` | `string` | No | `https://api.github.com` | GitHub API base URL (for GitHub Enterprise) |
183
- | `onWebhookEvent` | `Function` | No | - | Callback for webhook events |
184
- | `sessionsCollectionName` | `string` | No | `github_app_sessions` | MongoDB collection for sessions |
185
- | `installationsCollectionName` | `string` | No | `github_installations` | MongoDB collection for installations |
186
- | `webhookEventsCollectionName` | `string` | No | `github_webhook_events` | MongoDB collection for webhook events |
187
- | `tokenCacheTTL` | `number` | No | `3300` (55 minutes) | Installation token cache TTL in seconds |
188
- | `sessionTTL` | `number` | No | `600` (10 minutes) | Session TTL in seconds |
189
- | `registerRoutes` | `boolean` | No | `true` | Register HTTP handlers automatically |
190
- | `logWebhookEvents` | `boolean` | No | `false` | Log webhook events to MongoDB |
76
+ ## Event Handlers
191
77
 
192
- ### Callback Functions
193
-
194
- #### onWebhookEvent (Optional)
195
-
196
- Called when a webhook event is received.
78
+ Create one file per handler in `src/github-events/`. Export a `Route` for matching and a default handler function:
197
79
 
198
80
  ```typescript
199
- onWebhookEvent: async (
200
- params: {
201
- event: string;
202
- action?: string;
203
- payload: Record<string, any>;
204
- installationId: number;
205
- deliveryId: string;
206
- },
207
- ctx: Context
208
- ) => Promise<void>;
209
- ```
81
+ // src/github-events/OnPush.ts
82
+ import { GitHubEventHandler, GitHubEventRouteProps } from "@flink-app/github-app-plugin";
83
+ import { Ctx } from "../Ctx";
210
84
 
211
- **Example:**
85
+ export const Route: GitHubEventRouteProps = { event: "push" };
212
86
 
213
- ```typescript
214
- onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
215
- switch (event) {
216
- case "push":
217
- console.log(`Push to ${payload.repository.full_name}`);
218
- break;
219
-
220
- case "pull_request":
221
- if (action === "opened") {
222
- const client = await ctx.plugins.githubApp.getClient(installationId);
223
- await client.createIssue(payload.repository.owner.login, payload.repository.name, {
224
- title: "Thanks for the PR!",
225
- body: "We appreciate your contribution.",
226
- });
227
- }
228
- break;
229
-
230
- case "installation":
231
- if (action === "deleted") {
232
- console.log(`Installation ${installationId} was deleted`);
233
- }
234
- break;
235
- }
87
+ const handler: GitHubEventHandler<Ctx> = async ({ ctx, event, payload, github }) => {
88
+ const repo = payload.repository.full_name;
89
+ const commits = payload.commits || [];
90
+ console.log(`Push to ${repo}: ${commits.length} commits`);
236
91
  };
237
- ```
238
-
239
- ## Installation Flow
240
-
241
- ### How Users Install Your GitHub App
242
-
243
- 1. User navigates to: `GET /github-app/install?user_id=USER_ID`
244
- - The `user_id` query parameter is optional and determined by your app
245
- 2. User is redirected to GitHub's installation page
246
- 3. User selects repositories to grant access
247
- 4. User clicks "Install" or "Install & Authorize"
248
- 5. GitHub redirects back to: `GET /github-app/callback?installation_id=...&state=...`
249
- 6. Plugin validates the state parameter (CSRF protection)
250
- 7. Plugin fetches installation details from GitHub
251
- 8. Plugin calls your `onInstallationSuccess` callback
252
- 9. Plugin stores installation in MongoDB
253
- 10. User is redirected to your app
254
-
255
- ### Initiating Installation from Your App
256
92
 
257
- **HTML Button:**
258
-
259
- ```html
260
- <a href="/github-app/install?user_id=123">Install GitHub App</a>
93
+ export default handler;
261
94
  ```
262
95
 
263
- **React Component:**
96
+ Handlers are evaluated in discovery order. All matching handlers run (not just the first). Each handler receives a pre-authenticated `github` API client scoped to the installation that triggered the event.
264
97
 
265
- ```typescript
266
- function InstallGitHubApp() {
267
- const handleInstall = () => {
268
- const userId = getCurrentUserId(); // Your function
269
- window.location.href = `/github-app/install?user_id=${userId}`;
270
- };
98
+ ### Routing
271
99
 
272
- return <button onClick={handleInstall}>Connect GitHub</button>;
273
- }
274
- ```
275
-
276
- ## Webhook Setup
277
-
278
- ### 1. Configure Webhook in GitHub App Settings
100
+ `GitHubEventRouteProps` all fields are optional. Omit any to match everything (catch-all). All specified criteria must match (AND logic).
279
101
 
280
- - **Webhook URL:** `https://yourdomain.com/github-app/webhook`
281
- - **Webhook Secret:** Same secret used in plugin configuration
282
- - **Events:** Select events you want to receive (push, PR, issues, etc.)
283
-
284
- ### 2. Handle Webhook Events
102
+ | Field | Type | Matches when |
103
+ | ---------------- | ------------------------------------------------- | ----------------------------------- |
104
+ | `event` | `string \| string[]` | GitHub event type (e.g. `"push"`) |
105
+ | `action` | `string \| string[]` | Event action (e.g. `"opened"`) |
106
+ | `repository` | `string \| RegExp \| (payload) => boolean` | `payload.repository.full_name` |
107
+ | `installationId` | `number \| number[]` | GitHub installation ID |
285
108
 
286
109
  ```typescript
287
- onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
288
- console.log(`Event: ${event}, Action: ${action}, Delivery: ${deliveryId}`);
289
-
290
- // Access installation
291
- const installation = await ctx.repos.githubInstallationRepo.findByInstallationId(installationId);
110
+ // Single event
111
+ export const Route: GitHubEventRouteProps = { event: "push" };
292
112
 
293
- // Get API client
294
- const client = await ctx.plugins.githubApp.getClient(installationId);
113
+ // Multiple events
114
+ export const Route: GitHubEventRouteProps = { event: ["push", "pull_request"] };
295
115
 
296
- // Process event
297
- if (event === "push") {
298
- const commits = payload.commits;
299
- console.log(`Received ${commits.length} commits`);
300
- }
301
- };
302
- ```
303
-
304
- ### 3. Webhook Signature Validation
116
+ // Event + action
117
+ export const Route: GitHubEventRouteProps = { event: "pull_request", action: ["opened", "synchronize"] };
305
118
 
306
- The plugin automatically validates webhook signatures using HMAC-SHA256 with constant-time comparison. Invalid signatures are rejected with a 401 status code.
119
+ // Specific repository
120
+ export const Route: GitHubEventRouteProps = { event: "push", repository: "myorg/myrepo" };
307
121
 
308
- ## Context API
122
+ // Regex on repository
123
+ export const Route: GitHubEventRouteProps = { repository: /^myorg\// };
309
124
 
310
- The plugin exposes methods via `ctx.plugins.githubApp`:
311
-
312
- ### getClient(installationId)
313
-
314
- Get GitHub API client for an installation.
125
+ // Custom function
126
+ export const Route: GitHubEventRouteProps = {
127
+ repository: (payload) => payload.repository?.private === true,
128
+ };
315
129
 
316
- ```typescript
317
- const client = await ctx.plugins.githubApp.getClient(12345);
318
- const repos = await client.getRepositories();
130
+ // Catch-all (no Route export or empty object)
131
+ export const Route: GitHubEventRouteProps = {};
319
132
  ```
320
133
 
321
- ### getInstallation(userId)
322
-
323
- Get installation for a user (returns first installation if multiple exist).
134
+ ### Handler arguments
324
135
 
325
136
  ```typescript
326
- const installation = await ctx.plugins.githubApp.getInstallation("user-123");
327
- if (installation) {
328
- console.log(`Installed on: ${installation.accountLogin}`);
329
- }
137
+ const handler: GitHubEventHandler<Ctx> = async ({
138
+ ctx, // Flink app context
139
+ event, // Event type string (e.g. "push")
140
+ action, // Event action string (e.g. "opened"), may be undefined
141
+ payload, // Full webhook payload from GitHub
142
+ installationId, // GitHub installation ID
143
+ deliveryId, // Unique delivery ID (X-GitHub-Delivery header)
144
+ github, // Pre-authenticated GitHubAPIClient
145
+ }) => {
146
+ // ...
147
+ };
330
148
  ```
331
149
 
332
- ### getInstallations(userId)
150
+ ## onWebhookEvent callback (alternative)
333
151
 
334
- Get all installations for a user.
152
+ For simpler setups, use the `onWebhookEvent` callback instead of handler files:
335
153
 
336
154
  ```typescript
337
- const installations = await ctx.plugins.githubApp.getInstallations("user-123");
338
- installations.forEach((inst) => {
339
- console.log(`${inst.accountLogin} (${inst.accountType})`);
155
+ githubAppPlugin({
156
+ // ...credentials
157
+ onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
158
+ if (event === "push") {
159
+ const client = await ctx.plugins.githubApp.getClient(installationId);
160
+ // ...
161
+ }
162
+ },
340
163
  });
341
164
  ```
342
165
 
343
- ### deleteInstallation(userId, installationId)
166
+ Both approaches can be used simultaneously — the callback runs first, then auto-registered handlers.
344
167
 
345
- Delete an installation from the database.
168
+ ## Context API (ctx.plugins.githubApp)
346
169
 
347
- ```typescript
348
- await ctx.plugins.githubApp.deleteInstallation("user-123", 12345);
349
- ```
350
-
351
- ### hasRepositoryAccess(userId, owner, repo)
170
+ ### getClient(installationId)
352
171
 
353
- Check if user has access to a specific repository.
172
+ Get an authenticated GitHub API client for an installation:
354
173
 
355
174
  ```typescript
356
- const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess("user-123", "facebook", "react");
357
-
358
- if (!hasAccess) {
359
- return forbidden("You do not have access to this repository");
360
- }
175
+ const client = await ctx.plugins.githubApp.getClient(12345);
176
+ const repos = await client.getRepositories();
177
+ const repo = await client.getRepository("owner", "repo");
178
+ const contents = await client.getContents("owner", "repo", "README.md");
179
+ const issue = await client.createIssue("owner", "repo", { title: "Bug", body: "..." });
180
+ const data = await client.request("GET", "/rate_limit"); // generic API call
361
181
  ```
362
182
 
363
- ### completeInstallation(params)
364
-
365
- Complete GitHub App installation after callback from GitHub.
183
+ ### Installation management
366
184
 
367
185
  ```typescript
186
+ // Initiate installation flow (generates CSRF-protected URL)
187
+ const { redirectUrl } = await ctx.plugins.githubApp.initiateInstallation({
188
+ userId: "user-123",
189
+ metadata: { source: "settings" },
190
+ });
191
+
192
+ // Complete installation after GitHub callback
368
193
  const result = await ctx.plugins.githubApp.completeInstallation({
369
194
  installationId: 12345,
370
- state: "csrf-state-token",
195
+ state: "csrf-state",
371
196
  userId: "user-123",
372
197
  });
373
198
 
374
- if (result.success) {
375
- console.log("Installation completed:", result.installation);
376
- } else {
377
- console.error("Installation failed:", result.error);
378
- }
379
- ```
380
-
381
- ### getInstallationToken(installationId)
199
+ // Query installations
200
+ const installation = await ctx.plugins.githubApp.getInstallation("user-123");
201
+ const installations = await ctx.plugins.githubApp.getInstallations("user-123");
382
202
 
383
- Get raw installation access token (for advanced usage).
203
+ // Uninstall
204
+ await ctx.plugins.githubApp.uninstall({ userId: "user-123", installationId: 12345 });
205
+ await ctx.plugins.githubApp.deleteInstallation("user-123", 12345);
384
206
 
385
- ```typescript
386
- const token = await ctx.plugins.githubApp.getInstallationToken(12345);
387
- // Make custom API call with token
207
+ // Check repository access
208
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess("user-123", "owner", "repo");
388
209
  ```
389
210
 
390
- ### clearTokenCache()
391
-
392
- Clear all cached installation tokens.
211
+ ### Token management
393
212
 
394
213
  ```typescript
214
+ const token = await ctx.plugins.githubApp.getInstallationToken(12345);
395
215
  ctx.plugins.githubApp.clearTokenCache();
396
216
  ```
397
217
 
398
- ## GitHub API Client
399
-
400
- The plugin provides a GitHub API client with automatic token injection:
401
-
402
- ```typescript
403
- const client = await ctx.plugins.githubApp.getClient(installationId);
404
-
405
- // Get repositories accessible by this installation
406
- const repos = await client.getRepositories();
407
-
408
- // Get specific repository
409
- const repo = await client.getRepository("facebook", "react");
410
-
411
- // Get file contents
412
- const contents = await client.getContents("facebook", "react", "README.md");
413
-
414
- // Create an issue
415
- const issue = await client.createIssue("facebook", "react", {
416
- title: "Bug Report",
417
- body: "Found a bug...",
418
- });
419
-
420
- // Generic API call
421
- const response = await client.request("GET", "/rate_limit");
422
- ```
423
-
424
- ## Authentication Integration
425
-
426
- This plugin is **auth-agnostic** and works with any authentication system. You implement your own installation callback handler with your own auth logic.
427
-
428
- ### Example with Session-Based Auth
429
-
430
- ```typescript
431
- // In your handler
432
- const GetGitHubCallback: GetHandler = async ({ ctx, req }) => {
433
- // Check session-based auth
434
- const userId = ctx.req.session?.userId;
435
- if (!userId) {
436
- return unauthorized("Please log in");
437
- }
438
-
439
- const { installation_id, state } = req.query;
440
- const result = await ctx.plugins.githubApp.completeInstallation({
441
- installationId: parseInt(installation_id),
442
- state,
443
- userId,
444
- });
445
-
446
- return result.success
447
- ? redirect("/dashboard")
448
- : redirect(`/error?code=${result.error.code}`);
449
- };
450
- ```
451
-
452
- ### Example with JWT Auth Plugin
453
-
454
- ```typescript
455
- // In your handler with @flink-app/jwt-auth-plugin
456
- const GetGitHubCallback: GetHandler = async ({ ctx, req }) => {
457
- // Check JWT auth
458
- const userId = ctx.auth?.tokenData?.userId;
459
- if (!userId) {
460
- return unauthorized("Please log in");
461
- }
462
-
463
- const { installation_id, state } = req.query;
464
- const result = await ctx.plugins.githubApp.completeInstallation({
465
- installationId: parseInt(installation_id),
466
- state,
467
- userId,
468
- });
469
-
470
- return result.success
471
- ? redirect("/dashboard/github")
472
- : redirect(`/error?code=${result.error.code}`);
473
- };
474
- ```
475
-
476
- ## Security Considerations
477
-
478
- ### Private Key Management
479
-
480
- - Store base64 encoded private key in environment variables
481
- - Never commit private key to version control
482
- - Encode keys using base64 before storing in environment variables
483
- - Original key must be in PEM format (PKCS#1 or PKCS#8)
484
- - Rotate keys periodically
485
-
486
- ### JWT Signing Security
487
-
488
- - Uses RS256 algorithm with RSA private key
489
- - Tokens expire after 10 minutes
490
- - Automatic key format detection (PKCS#1 and PKCS#8)
491
-
492
- ### Webhook Signature Validation
493
-
494
- - HMAC-SHA256 signature validation
495
- - Constant-time comparison to prevent timing attacks
496
- - Rejects webhooks with invalid signatures
497
-
498
- ### CSRF Protection
499
-
500
- - State parameter with cryptographically secure random generation
501
- - Session stored with TTL (default: 10 minutes)
502
- - One-time use: session deleted after successful callback
503
- - Constant-time comparison for state validation
504
-
505
- ### Token Caching Security
506
-
507
- - Tokens cached in memory only (never in database)
508
- - Automatic expiration after 55 minutes (tokens expire at 60 minutes)
509
- - Clear cache on demand via `clearTokenCache()`
510
-
511
- ### HTTPS Requirements
512
-
513
- All GitHub API calls and webhook URLs must use HTTPS in production.
514
-
515
- ## Troubleshooting
516
-
517
- ### Invalid Private Key Format
518
-
519
- **Issue:** `invalid-private-key` error on plugin initialization
520
-
521
- **Solution:**
522
-
523
- - Ensure private key is base64 encoded before storing in environment variable
524
- - Verify original PEM key starts with `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1) or `-----BEGIN PRIVATE KEY-----` (PKCS#8)
525
- - Use the encoding commands: `base64 -i private-key.pem | tr -d '\n'` (macOS/Linux)
526
- - Ensure entire base64 string is included in environment variable with no line breaks
527
-
528
- ### Webhook Signature Validation Failed
529
-
530
- **Issue:** Webhooks rejected with 401 status
531
-
532
- **Solution:**
533
-
534
- - Verify webhook secret matches exactly
535
- - Check webhook secret is set in GitHub App settings
536
- - Ensure raw request body is used (not parsed JSON)
537
-
538
- ### Installation State Mismatch
539
-
540
- **Issue:** `invalid-state` error during callback
541
-
542
- **Solution:**
543
-
544
- - Ensure MongoDB is running and accessible
545
- - Check session TTL hasn't expired (default: 10 minutes)
546
- - Verify cookies are enabled
547
- - Check clock synchronization between servers
548
-
549
- ### Token Cache Not Working
550
-
551
- **Issue:** Too many GitHub API calls
552
-
553
- **Solution:**
554
-
555
- - Verify `tokenCacheTTL` is set appropriately (default: 55 minutes)
556
- - Check memory usage (tokens cached in-memory)
557
- - Call `clearTokenCache()` only when necessary
558
-
559
- ### Installation Not Found
560
-
561
- **Issue:** `installation-not-found` error
562
-
563
- **Solution:**
564
-
565
- - Verify user has installed the GitHub App
566
- - Check MongoDB for installation record
567
- - Ensure `userId` matches the one stored during installation
568
-
569
- ## API Reference
570
-
571
- See TypeScript interfaces for complete type definitions:
572
-
573
- - `GitHubAppPluginOptions` - Plugin configuration
574
- - `GitHubAppPluginContext` - Context API methods
575
- - `GitHubInstallation` - Installation model
576
- - `WebhookEvent` - Webhook event model
577
- - `GitHubAPIClient` - API client methods
578
-
579
- ## Examples
580
-
581
- See the `examples/` directory for complete working examples:
582
-
583
- - `basic-installation.ts` - Basic GitHub App installation
584
- - `webhook-handling.ts` - Process webhook events
585
- - `repository-access.ts` - Access repositories via API client
586
- - `create-issue.ts` - Create GitHub issue with permission check
587
- - `with-jwt-auth.ts` - Optional integration with JWT Auth Plugin
588
- - `organization-installation.ts` - Organization-level installation
589
- - `error-handling.ts` - Comprehensive error handling
590
- - `multi-event-webhook.ts` - Handle multiple webhook event types
591
-
592
- ## Production Checklist
593
-
594
- - [ ] GitHub App created with proper permissions
595
- - [ ] Webhook URL configured with HTTPS
596
- - [ ] Private key stored securely in environment variables
597
- - [ ] Webhook secret configured and stored securely
598
- - [ ] MongoDB connection configured and tested
599
- - [ ] `onInstallationSuccess` callback implemented
600
- - [ ] Webhook event handling implemented
601
- - [ ] Error handling configured
602
- - [ ] HTTPS enabled for all endpoints
603
- - [ ] Rate limiting configured (app-level)
604
- - [ ] Monitoring and logging set up
605
- - [ ] Test installation flow end-to-end
606
- - [ ] Test webhook delivery and signature validation
607
-
608
- ## License
609
-
610
- MIT
218
+ ## Plugin Options
219
+
220
+ | Option | Type | Default | Description |
221
+ | ----------------------------- | ---------- | ------------------------ | ------------------------------------------------- |
222
+ | `appId` | `string` | **required** | GitHub App ID |
223
+ | `privateKey` | `string` | **required** | Base64 encoded RSA private key |
224
+ | `webhookSecret` | `string` | **required** | Webhook secret for signature validation |
225
+ | `clientId` | `string` | **required** | GitHub App client ID |
226
+ | `clientSecret` | `string` | **required** | GitHub App client secret |
227
+ | `appSlug` | `string` | Auto-detected | GitHub App slug (used in installation URL) |
228
+ | `baseUrl` | `string` | `https://api.github.com` | GitHub API base URL (for GitHub Enterprise) |
229
+ | `onWebhookEvent` | `Function` | — | Callback for webhook events |
230
+ | `registerRoutes` | `boolean` | `true` | Register webhook HTTP handler |
231
+ | `logWebhookEvents` | `boolean` | `false` | Log webhook events to MongoDB |
232
+ | `sessionsCollectionName` | `string` | `github_app_sessions` | MongoDB collection for sessions |
233
+ | `installationsCollectionName` | `string` | `github_installations` | MongoDB collection for installations |
234
+ | `webhookEventsCollectionName` | `string` | `github_webhook_events` | MongoDB collection for webhook events |
235
+ | `tokenCacheTTL` | `number` | `3300` (55 min) | Installation token cache TTL in seconds |
236
+ | `sessionTTL` | `number` | `600` (10 min) | Session TTL in seconds |
237
+
238
+ ## Custom scan directory
239
+
240
+ ```js
241
+ // flink.config.js
242
+ compilerPlugins: [compilerPlugin({ scanDir: "src/my-github-events" })];
243
+ ```
244
+
245
+ ## Security
246
+
247
+ - **Private key**: Base64 encoded, stored in env vars, never committed. Supports PKCS#1 and PKCS#8.
248
+ - **JWT signing**: RS256 algorithm, tokens expire after 10 minutes.
249
+ - **Webhook validation**: HMAC-SHA256 with constant-time comparison.
250
+ - **CSRF protection**: Cryptographic state parameter with TTL-based sessions.
251
+ - **Token caching**: In-memory only, auto-expires after 55 minutes.