@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/CHANGELOG.md +6 -0
- package/README.md +161 -520
- package/dist/autoRegisteredGitHubHandlers.d.ts +6 -0
- package/dist/autoRegisteredGitHubHandlers.js +8 -0
- package/dist/compiler.d.ts +25 -0
- package/dist/compiler.js +26 -0
- package/dist/githubEventRouter.d.ts +6 -0
- package/dist/githubEventRouter.js +59 -0
- package/dist/handlers/WebhookHandler.js +66 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +18 -0
- package/dist/types/GitHubEventHandler.d.ts +28 -0
- package/dist/types/GitHubEventHandler.js +2 -0
- package/package.json +14 -4
- package/spec/githubEventRouter.spec.ts +122 -0
- package/src/autoRegisteredGitHubHandlers.ts +7 -0
- package/src/compiler.ts +32 -0
- package/src/githubEventRouter.ts +53 -0
- package/src/handlers/WebhookHandler.ts +48 -1
- package/src/index.ts +5 -0
- package/src/types/GitHubEventHandler.ts +31 -0
package/README.md
CHANGED
|
@@ -1,610 +1,251 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flink-app/github-app-plugin
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
+
pnpm add @flink-app/github-app-plugin
|
|
22
11
|
```
|
|
23
12
|
|
|
24
|
-
|
|
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
|
-
|
|
15
|
+
```js
|
|
16
|
+
// flink.config.js
|
|
17
|
+
const { compilerPlugin } = require("@flink-app/github-app-plugin/compiler");
|
|
71
18
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
19
|
+
module.exports = {
|
|
20
|
+
compilerPlugins: [compilerPlugin()],
|
|
21
|
+
};
|
|
75
22
|
```
|
|
76
23
|
|
|
77
|
-
|
|
24
|
+
This tells the Flink compiler to scan `src/github-events/` and auto-register every file that contains `GitHubEventHandler`.
|
|
78
25
|
|
|
79
|
-
### 3.
|
|
26
|
+
### 3. Extend your app context type
|
|
80
27
|
|
|
81
|
-
|
|
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
|
-
|
|
33
|
+
export interface Ctx extends FlinkContext<GitHubAppPluginContext> {
|
|
34
|
+
repos: {
|
|
35
|
+
// your repos
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
```
|
|
84
39
|
|
|
85
|
-
###
|
|
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!,
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
70
|
+
```bash
|
|
71
|
+
base64 -i private-key.pem | tr -d '\n'
|
|
168
72
|
```
|
|
169
73
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
### GitHubAppPluginOptions
|
|
74
|
+
Store the base64 string as `GITHUB_APP_PRIVATE_KEY_BASE64` in your environment.
|
|
173
75
|
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
85
|
+
export const Route: GitHubEventRouteProps = { event: "push" };
|
|
212
86
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
113
|
+
// Multiple events
|
|
114
|
+
export const Route: GitHubEventRouteProps = { event: ["push", "pull_request"] };
|
|
295
115
|
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
119
|
+
// Specific repository
|
|
120
|
+
export const Route: GitHubEventRouteProps = { event: "push", repository: "myorg/myrepo" };
|
|
307
121
|
|
|
308
|
-
|
|
122
|
+
// Regex on repository
|
|
123
|
+
export const Route: GitHubEventRouteProps = { repository: /^myorg\// };
|
|
309
124
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
317
|
-
const
|
|
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
|
-
###
|
|
322
|
-
|
|
323
|
-
Get installation for a user (returns first installation if multiple exist).
|
|
134
|
+
### Handler arguments
|
|
324
135
|
|
|
325
136
|
```typescript
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
150
|
+
## onWebhookEvent callback (alternative)
|
|
333
151
|
|
|
334
|
-
|
|
152
|
+
For simpler setups, use the `onWebhookEvent` callback instead of handler files:
|
|
335
153
|
|
|
336
154
|
```typescript
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
166
|
+
Both approaches can be used simultaneously — the callback runs first, then auto-registered handlers.
|
|
344
167
|
|
|
345
|
-
|
|
168
|
+
## Context API (ctx.plugins.githubApp)
|
|
346
169
|
|
|
347
|
-
|
|
348
|
-
await ctx.plugins.githubApp.deleteInstallation("user-123", 12345);
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### hasRepositoryAccess(userId, owner, repo)
|
|
170
|
+
### getClient(installationId)
|
|
352
171
|
|
|
353
|
-
|
|
172
|
+
Get an authenticated GitHub API client for an installation:
|
|
354
173
|
|
|
355
174
|
```typescript
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
###
|
|
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
|
|
195
|
+
state: "csrf-state",
|
|
371
196
|
userId: "user-123",
|
|
372
197
|
});
|
|
373
198
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
const
|
|
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
|
-
###
|
|
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
|
-
##
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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.
|