@flink-app/oauth-plugin 0.12.1-alpha.43 → 0.12.1-alpha.44
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/package.json +5 -5
- package/spec/integration.spec.ts +2 -4
- package/GITHUB_APP_PLUGIN_PRD.md +0 -1383
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/oauth-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.44",
|
|
4
4
|
"description": "Flink plugin for OAuth 2.0 authentication with GitHub and Google providers",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"mongodb": "^6.15.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@flink-app/flink": "^0.12.1-alpha.
|
|
23
|
-
"@flink-app/jwt-auth-plugin": "^0.12.1-alpha.
|
|
24
|
-
"@flink-app/test-utils": "^0.12.1-alpha.
|
|
22
|
+
"@flink-app/flink": "^0.12.1-alpha.44",
|
|
23
|
+
"@flink-app/jwt-auth-plugin": "^0.12.1-alpha.44",
|
|
24
|
+
"@flink-app/test-utils": "^0.12.1-alpha.44",
|
|
25
25
|
"@types/jasmine": "^3.7.1",
|
|
26
26
|
"@types/jwt-simple": "^0.5.36",
|
|
27
27
|
"@types/node": "22.13.10",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"tsc-watch": "^4.2.9",
|
|
35
35
|
"typescript": "5.4.5"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113"
|
|
38
38
|
}
|
package/spec/integration.spec.ts
CHANGED
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
* Once Task Groups 1-6 are implemented, these tests should be completed with actual assertions.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createMockJwtAuthPlugin
|
|
12
|
-
import {
|
|
13
|
-
import { beforeEachTest, afterAllTests } from "./helpers/testDatabase";
|
|
14
|
-
import { extractTokenFromUrl } from "./helpers/testHelpers";
|
|
11
|
+
import { createMockJwtAuthPlugin } from "./helpers/mockJwtAuthPlugin";
|
|
12
|
+
import { afterAllTests, beforeEachTest } from "./helpers/testDatabase";
|
|
15
13
|
|
|
16
14
|
describe("OAuth Plugin Integration Tests", () => {
|
|
17
15
|
let mockJwtAuth: any;
|
package/GITHUB_APP_PLUGIN_PRD.md
DELETED
|
@@ -1,1383 +0,0 @@
|
|
|
1
|
-
# Product Requirements Document: GitHub App Plugin
|
|
2
|
-
|
|
3
|
-
**Version:** 1.0
|
|
4
|
-
**Date:** 2025-10-26
|
|
5
|
-
**Status:** Draft
|
|
6
|
-
**Package Name:** `@flink-app/github-app-plugin`
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Table of Contents
|
|
11
|
-
|
|
12
|
-
1. [Overview](#overview)
|
|
13
|
-
2. [Background & Motivation](#background--motivation)
|
|
14
|
-
3. [Goals & Non-Goals](#goals--non-goals)
|
|
15
|
-
4. [Architecture](#architecture)
|
|
16
|
-
5. [GitHub App Authentication Flow](#github-app-authentication-flow)
|
|
17
|
-
6. [Plugin API Specification](#plugin-api-specification)
|
|
18
|
-
7. [Configuration Options](#configuration-options)
|
|
19
|
-
8. [Data Models](#data-models)
|
|
20
|
-
9. [HTTP Handlers](#http-handlers)
|
|
21
|
-
10. [Repositories](#repositories)
|
|
22
|
-
11. [Security Considerations](#security-considerations)
|
|
23
|
-
12. [Context API](#context-api)
|
|
24
|
-
13. [Error Handling](#error-handling)
|
|
25
|
-
14. [Integration with OAuth Plugin](#integration-with-oauth-plugin)
|
|
26
|
-
15. [Examples & Use Cases](#examples--use-cases)
|
|
27
|
-
16. [Development Tasks](#development-tasks)
|
|
28
|
-
17. [Testing Requirements](#testing-requirements)
|
|
29
|
-
18. [Documentation Requirements](#documentation-requirements)
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Overview
|
|
34
|
-
|
|
35
|
-
The GitHub App Plugin is a Flink plugin that enables GitHub App integration for fine-grained repository access. Unlike OAuth Apps which require all-or-nothing repo access, GitHub Apps allow users to selectively grant access to specific repositories.
|
|
36
|
-
|
|
37
|
-
This plugin complements the existing OAuth Plugin by handling repository-level operations while OAuth Plugin continues to handle social login/authentication.
|
|
38
|
-
|
|
39
|
-
### Key Features
|
|
40
|
-
|
|
41
|
-
- GitHub App installation flow with repository selection
|
|
42
|
-
- JWT-based authentication with private key signing
|
|
43
|
-
- Installation access token management with automatic refresh
|
|
44
|
-
- Fine-grained repository permissions
|
|
45
|
-
- Webhook integration support
|
|
46
|
-
- Higher API rate limits (15,000 requests/hour)
|
|
47
|
-
- Support for organization and user installations
|
|
48
|
-
- Encrypted private key storage
|
|
49
|
-
- Repository permission verification
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Background & Motivation
|
|
54
|
-
|
|
55
|
-
### Problem Statement
|
|
56
|
-
|
|
57
|
-
The OAuth Plugin uses GitHub OAuth Apps which have limitations:
|
|
58
|
-
- Users must grant access to ALL repositories or none
|
|
59
|
-
- Lower API rate limits (5,000 requests/hour)
|
|
60
|
-
- No webhook integration
|
|
61
|
-
- Cannot act as a bot/service account
|
|
62
|
-
- Broad permission model
|
|
63
|
-
|
|
64
|
-
### Solution
|
|
65
|
-
|
|
66
|
-
Implement a separate GitHub App plugin that:
|
|
67
|
-
- Allows users to select specific repositories during installation
|
|
68
|
-
- Provides higher rate limits for API operations
|
|
69
|
-
- Enables webhook integration for real-time events
|
|
70
|
-
- Supports fine-grained permissions per repository
|
|
71
|
-
- Can be installed at organization or user level
|
|
72
|
-
|
|
73
|
-
### Why Separate Plugin?
|
|
74
|
-
|
|
75
|
-
GitHub Apps use a **non-OAuth authentication flow** that is incompatible with the OAuth 2.0 standard:
|
|
76
|
-
- JWT signing with RSA private keys (not client secrets)
|
|
77
|
-
- Installation-based token model (not user-based)
|
|
78
|
-
- Different endpoints and flows
|
|
79
|
-
- Installation ID tracking required
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Goals & Non-Goals
|
|
84
|
-
|
|
85
|
-
### Goals
|
|
86
|
-
|
|
87
|
-
✅ Enable fine-grained repository access for Flink applications
|
|
88
|
-
✅ Follow Flink plugin architecture patterns (matching OAuth Plugin)
|
|
89
|
-
✅ Support both user and organization installations
|
|
90
|
-
✅ Provide webhook endpoint handling
|
|
91
|
-
✅ Implement secure private key storage and JWT signing
|
|
92
|
-
✅ Auto-refresh installation access tokens
|
|
93
|
-
✅ Integrate seamlessly with JWT Auth Plugin for user linking
|
|
94
|
-
✅ Provide comprehensive error handling
|
|
95
|
-
✅ Support multiple GitHub App configurations (multi-tenant)
|
|
96
|
-
|
|
97
|
-
### Non-Goals
|
|
98
|
-
|
|
99
|
-
❌ Replace OAuth Plugin for social login (OAuth Plugin handles this)
|
|
100
|
-
❌ Support other Git providers (GitHub-specific)
|
|
101
|
-
❌ Implement GitHub Actions or CI/CD features
|
|
102
|
-
❌ Provide repository cloning/Git operations (app-level concern)
|
|
103
|
-
❌ Handle GitHub Packages or GitHub Container Registry
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## Architecture
|
|
108
|
-
|
|
109
|
-
### Plugin Structure
|
|
110
|
-
|
|
111
|
-
Following the OAuth Plugin pattern:
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
packages/github-app-plugin/
|
|
115
|
-
├── src/
|
|
116
|
-
│ ├── GitHubAppPlugin.ts # Plugin factory function
|
|
117
|
-
│ ├── GitHubAppPluginOptions.ts # Configuration interface
|
|
118
|
-
│ ├── GitHubAppPluginContext.ts # Public context interface
|
|
119
|
-
│ ├── GitHubAppInternalContext.ts # Internal context interface
|
|
120
|
-
│ │
|
|
121
|
-
│ ├── handlers/
|
|
122
|
-
│ │ ├── InitiateInstallation.ts # GET /github-app/install
|
|
123
|
-
│ │ ├── InstallationCallback.ts # GET /github-app/callback
|
|
124
|
-
│ │ ├── WebhookHandler.ts # POST /github-app/webhook
|
|
125
|
-
│ │ └── UninstallHandler.ts # DELETE /github-app/installation/:id
|
|
126
|
-
│ │
|
|
127
|
-
│ ├── repos/
|
|
128
|
-
│ │ ├── GitHubInstallationRepo.ts # Installation storage
|
|
129
|
-
│ │ └── GitHubWebhookEventRepo.ts # Webhook event log (optional)
|
|
130
|
-
│ │
|
|
131
|
-
│ ├── schemas/
|
|
132
|
-
│ │ ├── GitHubInstallation.ts # Installation model
|
|
133
|
-
│ │ ├── WebhookEvent.ts # Webhook event model
|
|
134
|
-
│ │ ├── InstallationCallbackRequest.ts
|
|
135
|
-
│ │ └── WebhookPayload.ts
|
|
136
|
-
│ │
|
|
137
|
-
│ ├── services/
|
|
138
|
-
│ │ ├── GitHubAuthService.ts # JWT signing and token management
|
|
139
|
-
│ │ ├── GitHubAPIClient.ts # API client wrapper
|
|
140
|
-
│ │ └── WebhookValidator.ts # Webhook signature validation
|
|
141
|
-
│ │
|
|
142
|
-
│ ├── utils/
|
|
143
|
-
│ │ ├── jwt-utils.ts # JWT signing with private key
|
|
144
|
-
│ │ ├── token-cache-utils.ts # Installation token caching
|
|
145
|
-
│ │ ├── encryption-utils.ts # Private key encryption (reuse from oauth)
|
|
146
|
-
│ │ ├── webhook-signature-utils.ts # HMAC signature validation
|
|
147
|
-
│ │ └── error-utils.ts # Error handling
|
|
148
|
-
│ │
|
|
149
|
-
│ └── index.ts # Exports
|
|
150
|
-
│
|
|
151
|
-
├── spec/ # Tests
|
|
152
|
-
├── examples/ # Usage examples
|
|
153
|
-
├── README.md
|
|
154
|
-
├── SECURITY.md
|
|
155
|
-
├── package.json
|
|
156
|
-
└── tsconfig.json
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Component Responsibilities
|
|
160
|
-
|
|
161
|
-
| Component | Responsibility |
|
|
162
|
-
|-----------|----------------|
|
|
163
|
-
| **GitHubAppPlugin** | Plugin initialization, handler registration, context setup |
|
|
164
|
-
| **GitHubAuthService** | JWT generation, token exchange, token caching |
|
|
165
|
-
| **GitHubAPIClient** | Wrapper for GitHub API calls with automatic authentication |
|
|
166
|
-
| **WebhookValidator** | Validate webhook signatures and parse payloads |
|
|
167
|
-
| **Repositories** | MongoDB storage for installations and webhook events |
|
|
168
|
-
| **Handlers** | HTTP endpoints for installation flow and webhooks |
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## GitHub App Authentication Flow
|
|
173
|
-
|
|
174
|
-
### 1. Installation Flow
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
User clicks "Install GitHub App"
|
|
178
|
-
↓
|
|
179
|
-
GET /github-app/install?user_id={userId}
|
|
180
|
-
↓
|
|
181
|
-
Redirect to: https://github.com/apps/{app_slug}/installations/new
|
|
182
|
-
↓
|
|
183
|
-
User selects repositories to grant access
|
|
184
|
-
↓
|
|
185
|
-
GitHub redirects to: /github-app/callback?installation_id=123&setup_action=install&state={state}
|
|
186
|
-
↓
|
|
187
|
-
Validate state parameter (CSRF protection)
|
|
188
|
-
↓
|
|
189
|
-
Call onInstallationSuccess callback
|
|
190
|
-
↓
|
|
191
|
-
Store installation_id linked to user_id
|
|
192
|
-
↓
|
|
193
|
-
Redirect to app dashboard
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### 2. API Access Flow
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
App needs to access user's repos
|
|
200
|
-
↓
|
|
201
|
-
Get installation_id from database
|
|
202
|
-
↓
|
|
203
|
-
Check token cache for valid installation token
|
|
204
|
-
↓
|
|
205
|
-
If expired or missing:
|
|
206
|
-
├── Generate JWT signed with app private key (RS256)
|
|
207
|
-
├── Exchange JWT for installation access token
|
|
208
|
-
└── Cache token (expires in 1 hour)
|
|
209
|
-
↓
|
|
210
|
-
Use installation token to call GitHub API
|
|
211
|
-
↓
|
|
212
|
-
Return data to application
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### 3. Webhook Flow
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
GitHub sends webhook event
|
|
219
|
-
↓
|
|
220
|
-
POST /github-app/webhook
|
|
221
|
-
↓
|
|
222
|
-
Validate X-Hub-Signature-256 header
|
|
223
|
-
↓
|
|
224
|
-
Parse webhook payload
|
|
225
|
-
↓
|
|
226
|
-
Call onWebhookEvent callback with event data
|
|
227
|
-
↓
|
|
228
|
-
Application processes event
|
|
229
|
-
↓
|
|
230
|
-
Return 200 OK to GitHub
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## Plugin API Specification
|
|
236
|
-
|
|
237
|
-
### Factory Function
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
import { githubAppPlugin } from '@flink-app/github-app-plugin';
|
|
241
|
-
|
|
242
|
-
const plugin = githubAppPlugin({
|
|
243
|
-
appId: string;
|
|
244
|
-
privateKey: string;
|
|
245
|
-
webhookSecret: string;
|
|
246
|
-
clientId: string;
|
|
247
|
-
clientSecret: string;
|
|
248
|
-
appSlug?: string;
|
|
249
|
-
|
|
250
|
-
onInstallationSuccess: (params: InstallationSuccessParams, ctx: Context) => Promise<InstallationSuccessResponse>;
|
|
251
|
-
onInstallationError?: (params: InstallationErrorParams) => Promise<InstallationErrorResponse>;
|
|
252
|
-
onWebhookEvent?: (params: WebhookEventParams, ctx: Context) => Promise<void>;
|
|
253
|
-
|
|
254
|
-
installationsCollectionName?: string;
|
|
255
|
-
webhookEventsCollectionName?: string;
|
|
256
|
-
tokenCacheTTL?: number;
|
|
257
|
-
registerRoutes?: boolean;
|
|
258
|
-
});
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Callback Interfaces
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
interface InstallationSuccessParams {
|
|
265
|
-
installationId: number;
|
|
266
|
-
repositories: Array<{
|
|
267
|
-
id: number;
|
|
268
|
-
name: string;
|
|
269
|
-
full_name: string;
|
|
270
|
-
private: boolean;
|
|
271
|
-
}>;
|
|
272
|
-
account: {
|
|
273
|
-
id: number;
|
|
274
|
-
login: string;
|
|
275
|
-
type: 'User' | 'Organization';
|
|
276
|
-
avatar_url: string;
|
|
277
|
-
};
|
|
278
|
-
permissions: Record<string, string>;
|
|
279
|
-
events: string[];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
interface InstallationSuccessResponse {
|
|
283
|
-
userId: string;
|
|
284
|
-
redirectUrl?: string;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
interface WebhookEventParams {
|
|
288
|
-
event: string;
|
|
289
|
-
action?: string;
|
|
290
|
-
installationId: number;
|
|
291
|
-
payload: any;
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## Configuration Options
|
|
298
|
-
|
|
299
|
-
### GitHubAppPluginOptions
|
|
300
|
-
|
|
301
|
-
| Option | Type | Required | Default | Description |
|
|
302
|
-
|--------|------|----------|---------|-------------|
|
|
303
|
-
| `appId` | `string` | Yes | - | GitHub App ID |
|
|
304
|
-
| `privateKey` | `string` | Yes | - | RSA private key (PEM format) |
|
|
305
|
-
| `webhookSecret` | `string` | Yes | - | Webhook secret for signature validation |
|
|
306
|
-
| `clientId` | `string` | Yes | - | GitHub App client ID |
|
|
307
|
-
| `clientSecret` | `string` | Yes | - | GitHub App client secret |
|
|
308
|
-
| `appSlug` | `string` | No | - | GitHub App slug (auto-detected if not provided) |
|
|
309
|
-
| `onInstallationSuccess` | `Function` | Yes | - | Callback after successful installation |
|
|
310
|
-
| `onInstallationError` | `Function` | No | - | Callback on installation errors |
|
|
311
|
-
| `onWebhookEvent` | `Function` | No | - | Callback for webhook events |
|
|
312
|
-
| `installationsCollectionName` | `string` | No | `'github_installations'` | MongoDB collection for installations |
|
|
313
|
-
| `webhookEventsCollectionName` | `string` | No | `'github_webhook_events'` | MongoDB collection for webhook logs |
|
|
314
|
-
| `tokenCacheTTL` | `number` | No | `3300` | Token cache TTL in seconds (55 min) |
|
|
315
|
-
| `registerRoutes` | `boolean` | No | `true` | Auto-register HTTP handlers |
|
|
316
|
-
|
|
317
|
-
### Environment Variables
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
# GitHub App credentials
|
|
321
|
-
GITHUB_APP_ID=123456
|
|
322
|
-
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
|
|
323
|
-
GITHUB_APP_WEBHOOK_SECRET=your_webhook_secret
|
|
324
|
-
GITHUB_APP_CLIENT_ID=Iv1.abc123
|
|
325
|
-
GITHUB_APP_CLIENT_SECRET=your_client_secret
|
|
326
|
-
|
|
327
|
-
# Optional
|
|
328
|
-
GITHUB_APP_SLUG=my-app-slug
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
## Data Models
|
|
334
|
-
|
|
335
|
-
### GitHubInstallation
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
interface GitHubInstallation {
|
|
339
|
-
_id?: string;
|
|
340
|
-
userId: string; // Link to application user
|
|
341
|
-
installationId: number; // GitHub installation ID
|
|
342
|
-
accountId: number; // GitHub account ID
|
|
343
|
-
accountLogin: string; // GitHub username or org name
|
|
344
|
-
accountType: 'User' | 'Organization';
|
|
345
|
-
avatarUrl: string;
|
|
346
|
-
|
|
347
|
-
repositories: Array<{
|
|
348
|
-
id: number;
|
|
349
|
-
name: string;
|
|
350
|
-
fullName: string;
|
|
351
|
-
private: boolean;
|
|
352
|
-
}>;
|
|
353
|
-
|
|
354
|
-
permissions: Record<string, string>; // e.g., { contents: 'read', issues: 'write' }
|
|
355
|
-
events: string[]; // Webhook events subscribed
|
|
356
|
-
|
|
357
|
-
suspendedAt?: Date; // If installation suspended
|
|
358
|
-
suspendedBy?: {
|
|
359
|
-
id: number;
|
|
360
|
-
login: string;
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
createdAt: Date;
|
|
364
|
-
updatedAt: Date;
|
|
365
|
-
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### WebhookEvent (Optional - for logging)
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
interface WebhookEvent {
|
|
372
|
-
_id?: string;
|
|
373
|
-
installationId: number;
|
|
374
|
-
event: string; // e.g., 'push', 'pull_request'
|
|
375
|
-
action?: string; // e.g., 'opened', 'closed'
|
|
376
|
-
deliveryId: string; // X-GitHub-Delivery header
|
|
377
|
-
payload: any; // Full webhook payload
|
|
378
|
-
processed: boolean;
|
|
379
|
-
processedAt?: Date;
|
|
380
|
-
error?: string;
|
|
381
|
-
createdAt: Date;
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## HTTP Handlers
|
|
388
|
-
|
|
389
|
-
### 1. InitiateInstallation Handler
|
|
390
|
-
|
|
391
|
-
**Route:** `GET /github-app/install`
|
|
392
|
-
|
|
393
|
-
**Query Parameters:**
|
|
394
|
-
- `user_id` (required) - Application user ID to link installation
|
|
395
|
-
|
|
396
|
-
**Flow:**
|
|
397
|
-
1. Generate secure state parameter for CSRF protection
|
|
398
|
-
2. Store state in session/cache with user_id
|
|
399
|
-
3. Build GitHub App installation URL
|
|
400
|
-
4. Redirect user to GitHub
|
|
401
|
-
|
|
402
|
-
**Implementation:**
|
|
403
|
-
```typescript
|
|
404
|
-
export const Route: RouteProps = {
|
|
405
|
-
path: '/github-app/install',
|
|
406
|
-
method: HttpMethod.get,
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const InitiateInstallation: GetHandler = async ({ ctx, req }) => {
|
|
410
|
-
const { user_id } = req.query;
|
|
411
|
-
|
|
412
|
-
// Validate user exists
|
|
413
|
-
// Generate state
|
|
414
|
-
// Store session
|
|
415
|
-
// Redirect to GitHub
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
status: 302,
|
|
419
|
-
headers: {
|
|
420
|
-
Location: `https://github.com/apps/${appSlug}/installations/new?state=${state}`
|
|
421
|
-
},
|
|
422
|
-
data: {}
|
|
423
|
-
};
|
|
424
|
-
};
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### 2. InstallationCallback Handler
|
|
428
|
-
|
|
429
|
-
**Route:** `GET /github-app/callback`
|
|
430
|
-
|
|
431
|
-
**Query Parameters:**
|
|
432
|
-
- `installation_id` - GitHub installation ID
|
|
433
|
-
- `setup_action` - 'install', 'update', or 'request'
|
|
434
|
-
- `state` - CSRF protection token
|
|
435
|
-
- `code` (optional) - Authorization code (if requesting user token)
|
|
436
|
-
|
|
437
|
-
**Flow:**
|
|
438
|
-
1. Validate state parameter
|
|
439
|
-
2. Exchange code for user access token (if present)
|
|
440
|
-
3. Fetch installation details from GitHub API
|
|
441
|
-
4. Call `onInstallationSuccess` callback
|
|
442
|
-
5. Store installation in database
|
|
443
|
-
6. Redirect to app
|
|
444
|
-
|
|
445
|
-
**Implementation:**
|
|
446
|
-
```typescript
|
|
447
|
-
export const Route: RouteProps = {
|
|
448
|
-
path: '/github-app/callback',
|
|
449
|
-
method: HttpMethod.get,
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const InstallationCallback: GetHandler = async ({ ctx, req }) => {
|
|
453
|
-
const { installation_id, setup_action, state, code } = req.query;
|
|
454
|
-
|
|
455
|
-
// Validate state
|
|
456
|
-
// Fetch installation details
|
|
457
|
-
// Call onInstallationSuccess
|
|
458
|
-
// Store installation
|
|
459
|
-
// Redirect
|
|
460
|
-
};
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### 3. WebhookHandler
|
|
464
|
-
|
|
465
|
-
**Route:** `POST /github-app/webhook`
|
|
466
|
-
|
|
467
|
-
**Headers:**
|
|
468
|
-
- `X-GitHub-Event` - Event type
|
|
469
|
-
- `X-GitHub-Delivery` - Unique delivery ID
|
|
470
|
-
- `X-Hub-Signature-256` - HMAC signature for validation
|
|
471
|
-
|
|
472
|
-
**Flow:**
|
|
473
|
-
1. Validate webhook signature
|
|
474
|
-
2. Parse payload
|
|
475
|
-
3. Optionally log webhook event
|
|
476
|
-
4. Call `onWebhookEvent` callback
|
|
477
|
-
5. Return 200 OK
|
|
478
|
-
|
|
479
|
-
**Implementation:**
|
|
480
|
-
```typescript
|
|
481
|
-
export const Route: RouteProps = {
|
|
482
|
-
path: '/github-app/webhook',
|
|
483
|
-
method: HttpMethod.post,
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
const WebhookHandler: PostHandler = async ({ ctx, req }) => {
|
|
487
|
-
// Validate signature
|
|
488
|
-
// Parse payload
|
|
489
|
-
// Call onWebhookEvent
|
|
490
|
-
// Return 200
|
|
491
|
-
};
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### 4. UninstallHandler (Optional)
|
|
495
|
-
|
|
496
|
-
**Route:** `DELETE /github-app/installation/:installationId`
|
|
497
|
-
|
|
498
|
-
**Auth:** Requires JWT authentication
|
|
499
|
-
|
|
500
|
-
**Flow:**
|
|
501
|
-
1. Verify user owns installation
|
|
502
|
-
2. Delete from database
|
|
503
|
-
3. Optionally revoke on GitHub (requires API call)
|
|
504
|
-
|
|
505
|
-
---
|
|
506
|
-
|
|
507
|
-
## Repositories
|
|
508
|
-
|
|
509
|
-
### GitHubInstallationRepo
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
class GitHubInstallationRepo extends FlinkRepo<any, GitHubInstallation> {
|
|
513
|
-
/**
|
|
514
|
-
* Find installation by user ID and installation ID
|
|
515
|
-
*/
|
|
516
|
-
async findByUserAndInstallationId(
|
|
517
|
-
userId: string,
|
|
518
|
-
installationId: number
|
|
519
|
-
): Promise<GitHubInstallation | null>;
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Find all installations for a user
|
|
523
|
-
*/
|
|
524
|
-
async findByUserId(userId: string): Promise<GitHubInstallation[]>;
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Find installation by installation ID only
|
|
528
|
-
*/
|
|
529
|
-
async findByInstallationId(installationId: number): Promise<GitHubInstallation | null>;
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Update repositories for an installation
|
|
533
|
-
*/
|
|
534
|
-
async updateRepositories(
|
|
535
|
-
installationId: number,
|
|
536
|
-
repositories: Array<{ id: number; name: string; fullName: string; private: boolean }>
|
|
537
|
-
): Promise<void>;
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Mark installation as suspended
|
|
541
|
-
*/
|
|
542
|
-
async suspend(installationId: number, suspendedBy: { id: number; login: string }): Promise<void>;
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Delete installation
|
|
546
|
-
*/
|
|
547
|
-
async deleteByInstallationId(installationId: number): Promise<number>;
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### GitHubWebhookEventRepo (Optional)
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
class GitHubWebhookEventRepo extends FlinkRepo<any, WebhookEvent> {
|
|
555
|
-
/**
|
|
556
|
-
* Find unprocessed webhook events
|
|
557
|
-
*/
|
|
558
|
-
async findUnprocessed(): Promise<WebhookEvent[]>;
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Mark event as processed
|
|
562
|
-
*/
|
|
563
|
-
async markProcessed(eventId: string): Promise<void>;
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Find events by installation ID
|
|
567
|
-
*/
|
|
568
|
-
async findByInstallationId(installationId: number): Promise<WebhookEvent[]>;
|
|
569
|
-
}
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
---
|
|
573
|
-
|
|
574
|
-
## Security Considerations
|
|
575
|
-
|
|
576
|
-
### Private Key Management
|
|
577
|
-
|
|
578
|
-
1. **Storage:**
|
|
579
|
-
- Store private key in environment variables
|
|
580
|
-
- Encrypt at rest if storing in database
|
|
581
|
-
- Use PEM format (PKCS#1 or PKCS#8)
|
|
582
|
-
|
|
583
|
-
2. **Validation:**
|
|
584
|
-
- Validate PEM format on plugin initialization
|
|
585
|
-
- Verify key can sign JWTs successfully
|
|
586
|
-
- Check key permissions (should be 600 or stricter)
|
|
587
|
-
|
|
588
|
-
3. **Best Practices:**
|
|
589
|
-
- Never commit private key to version control
|
|
590
|
-
- Rotate keys periodically
|
|
591
|
-
- Use secret management services in production
|
|
592
|
-
|
|
593
|
-
### JWT Signing
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
// RS256 algorithm with private key
|
|
597
|
-
const jwt = sign({
|
|
598
|
-
iat: Math.floor(Date.now() / 1000),
|
|
599
|
-
exp: Math.floor(Date.now() / 1000) + 600, // 10 minutes
|
|
600
|
-
iss: appId
|
|
601
|
-
}, privateKey, {
|
|
602
|
-
algorithm: 'RS256'
|
|
603
|
-
});
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### Webhook Signature Validation
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
function validateWebhookSignature(
|
|
610
|
-
payload: string,
|
|
611
|
-
signature: string,
|
|
612
|
-
secret: string
|
|
613
|
-
): boolean {
|
|
614
|
-
const hmac = crypto.createHmac('sha256', secret);
|
|
615
|
-
const digest = 'sha256=' + hmac.update(payload).digest('hex');
|
|
616
|
-
return crypto.timingSafeEqual(
|
|
617
|
-
Buffer.from(signature),
|
|
618
|
-
Buffer.from(digest)
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
### CSRF Protection
|
|
624
|
-
|
|
625
|
-
- Use state parameter in installation flow (similar to OAuth)
|
|
626
|
-
- Store state in MongoDB session with TTL
|
|
627
|
-
- Validate state on callback using constant-time comparison
|
|
628
|
-
|
|
629
|
-
### Installation Token Security
|
|
630
|
-
|
|
631
|
-
- Cache installation tokens in memory (not database)
|
|
632
|
-
- Tokens expire after 1 hour
|
|
633
|
-
- Never expose tokens to client
|
|
634
|
-
- Use HTTPS for all API calls
|
|
635
|
-
|
|
636
|
-
---
|
|
637
|
-
|
|
638
|
-
## Context API
|
|
639
|
-
|
|
640
|
-
### Plugin Context
|
|
641
|
-
|
|
642
|
-
The plugin exposes methods via `ctx.plugins.githubApp`:
|
|
643
|
-
|
|
644
|
-
```typescript
|
|
645
|
-
interface GitHubAppPluginContext {
|
|
646
|
-
githubApp: {
|
|
647
|
-
/**
|
|
648
|
-
* Get GitHub API client for an installation
|
|
649
|
-
*/
|
|
650
|
-
getClient(installationId: number): Promise<GitHubAPIClient>;
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Get installation for a user
|
|
654
|
-
*/
|
|
655
|
-
getInstallation(userId: string): Promise<GitHubInstallation | null>;
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Get all installations for a user
|
|
659
|
-
*/
|
|
660
|
-
getInstallations(userId: string): Promise<GitHubInstallation[]>;
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Delete installation
|
|
664
|
-
*/
|
|
665
|
-
deleteInstallation(userId: string, installationId: number): Promise<void>;
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Check if user has access to specific repository
|
|
669
|
-
*/
|
|
670
|
-
hasRepositoryAccess(
|
|
671
|
-
userId: string,
|
|
672
|
-
owner: string,
|
|
673
|
-
repo: string
|
|
674
|
-
): Promise<boolean>;
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Get installation access token (for advanced usage)
|
|
678
|
-
*/
|
|
679
|
-
getInstallationToken(installationId: number): Promise<string>;
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Plugin options (read-only)
|
|
683
|
-
*/
|
|
684
|
-
options: Readonly<GitHubAppPluginOptions>;
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### GitHubAPIClient
|
|
690
|
-
|
|
691
|
-
Wrapper for GitHub API calls with automatic authentication:
|
|
692
|
-
|
|
693
|
-
```typescript
|
|
694
|
-
class GitHubAPIClient {
|
|
695
|
-
/**
|
|
696
|
-
* Get repositories accessible by this installation
|
|
697
|
-
*/
|
|
698
|
-
async getRepositories(): Promise<Repository[]>;
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Get repository details
|
|
702
|
-
*/
|
|
703
|
-
async getRepository(owner: string, repo: string): Promise<Repository>;
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* List repository contents
|
|
707
|
-
*/
|
|
708
|
-
async getContents(owner: string, repo: string, path: string): Promise<Content[]>;
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Create an issue
|
|
712
|
-
*/
|
|
713
|
-
async createIssue(
|
|
714
|
-
owner: string,
|
|
715
|
-
repo: string,
|
|
716
|
-
params: { title: string; body: string }
|
|
717
|
-
): Promise<Issue>;
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Generic API call
|
|
721
|
-
*/
|
|
722
|
-
async request(
|
|
723
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
724
|
-
endpoint: string,
|
|
725
|
-
data?: any
|
|
726
|
-
): Promise<any>;
|
|
727
|
-
}
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
---
|
|
731
|
-
|
|
732
|
-
## Error Handling
|
|
733
|
-
|
|
734
|
-
### Error Codes
|
|
735
|
-
|
|
736
|
-
```typescript
|
|
737
|
-
export const GitHubAppErrorCodes = {
|
|
738
|
-
INVALID_STATE: 'invalid_state',
|
|
739
|
-
INSTALLATION_NOT_FOUND: 'installation_not_found',
|
|
740
|
-
INVALID_PRIVATE_KEY: 'invalid_private_key',
|
|
741
|
-
JWT_SIGNING_FAILED: 'jwt_signing_failed',
|
|
742
|
-
TOKEN_EXCHANGE_FAILED: 'token_exchange_failed',
|
|
743
|
-
WEBHOOK_SIGNATURE_INVALID: 'webhook_signature_invalid',
|
|
744
|
-
REPOSITORY_NOT_ACCESSIBLE: 'repository_not_accessible',
|
|
745
|
-
INSTALLATION_SUSPENDED: 'installation_suspended',
|
|
746
|
-
API_RATE_LIMIT: 'api_rate_limit',
|
|
747
|
-
NETWORK_ERROR: 'network_error',
|
|
748
|
-
} as const;
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
### Error Structure
|
|
752
|
-
|
|
753
|
-
```typescript
|
|
754
|
-
interface GitHubAppError {
|
|
755
|
-
code: string;
|
|
756
|
-
message: string;
|
|
757
|
-
details?: any;
|
|
758
|
-
}
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Error Handling in Callbacks
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
onInstallationError: async ({ error, installationId }) => {
|
|
765
|
-
console.error(`Installation error ${installationId}:`, error);
|
|
766
|
-
|
|
767
|
-
return {
|
|
768
|
-
redirectUrl: `/dashboard?error=${error.code}`
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
---
|
|
774
|
-
|
|
775
|
-
## Integration with OAuth Plugin
|
|
776
|
-
|
|
777
|
-
### Complementary Usage
|
|
778
|
-
|
|
779
|
-
The GitHub App Plugin works **alongside** the OAuth Plugin:
|
|
780
|
-
|
|
781
|
-
```typescript
|
|
782
|
-
const app = new FlinkApp<Context>({
|
|
783
|
-
auth: jwtAuthPlugin({ /* ... */ }),
|
|
784
|
-
|
|
785
|
-
plugins: [
|
|
786
|
-
// OAuth Plugin for social login
|
|
787
|
-
oauthPlugin({
|
|
788
|
-
providers: {
|
|
789
|
-
github: {
|
|
790
|
-
scope: ['user:email'], // Just authentication
|
|
791
|
-
}
|
|
792
|
-
},
|
|
793
|
-
storeTokens: false,
|
|
794
|
-
onAuthSuccess: async ({ profile }, ctx) => {
|
|
795
|
-
// Create user account
|
|
796
|
-
// Generate JWT token
|
|
797
|
-
}
|
|
798
|
-
}),
|
|
799
|
-
|
|
800
|
-
// GitHub App Plugin for repo access
|
|
801
|
-
githubAppPlugin({
|
|
802
|
-
appId: process.env.GITHUB_APP_ID!,
|
|
803
|
-
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
804
|
-
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
805
|
-
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
806
|
-
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
807
|
-
|
|
808
|
-
onInstallationSuccess: async ({ installationId, repositories }, ctx) => {
|
|
809
|
-
// Get current authenticated user
|
|
810
|
-
const userId = ctx.auth.tokenData.userId;
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
userId,
|
|
814
|
-
redirectUrl: '/dashboard/repos'
|
|
815
|
-
};
|
|
816
|
-
},
|
|
817
|
-
|
|
818
|
-
onWebhookEvent: async ({ event, payload }, ctx) => {
|
|
819
|
-
// Handle push events, PR events, etc.
|
|
820
|
-
if (event === 'push') {
|
|
821
|
-
// Process push webhook
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
})
|
|
825
|
-
]
|
|
826
|
-
});
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
### User Flow
|
|
830
|
-
|
|
831
|
-
1. User signs up/logs in via **OAuth Plugin** (social login)
|
|
832
|
-
2. User navigates to "Connect Repositories" in dashboard
|
|
833
|
-
3. User clicks "Install GitHub App"
|
|
834
|
-
4. **GitHub App Plugin** handles installation and repo selection
|
|
835
|
-
5. Application can now access selected repos via GitHub API
|
|
836
|
-
6. Webhooks keep application in sync with repo events
|
|
837
|
-
|
|
838
|
-
---
|
|
839
|
-
|
|
840
|
-
## Examples & Use Cases
|
|
841
|
-
|
|
842
|
-
### Example 1: Basic Installation
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
import { githubAppPlugin } from '@flink-app/github-app-plugin';
|
|
846
|
-
|
|
847
|
-
const plugin = githubAppPlugin({
|
|
848
|
-
appId: process.env.GITHUB_APP_ID!,
|
|
849
|
-
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
850
|
-
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
851
|
-
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
852
|
-
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
853
|
-
|
|
854
|
-
onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
|
|
855
|
-
console.log(`Installed to ${account.login} with ${repositories.length} repos`);
|
|
856
|
-
|
|
857
|
-
// Get current user from JWT auth
|
|
858
|
-
const userId = ctx.auth.tokenData.userId;
|
|
859
|
-
|
|
860
|
-
return {
|
|
861
|
-
userId,
|
|
862
|
-
redirectUrl: '/dashboard'
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
### Example 2: Accessing Repositories
|
|
869
|
-
|
|
870
|
-
```typescript
|
|
871
|
-
// In a handler
|
|
872
|
-
const GetUserRepos: GetHandler = async ({ ctx, auth }) => {
|
|
873
|
-
const userId = auth.tokenData.userId;
|
|
874
|
-
|
|
875
|
-
// Get GitHub App client
|
|
876
|
-
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
877
|
-
|
|
878
|
-
if (!installation) {
|
|
879
|
-
return badRequest('GitHub App not installed');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
883
|
-
const repos = await client.getRepositories();
|
|
884
|
-
|
|
885
|
-
return { status: 200, data: repos };
|
|
886
|
-
};
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### Example 3: Webhook Handling
|
|
890
|
-
|
|
891
|
-
```typescript
|
|
892
|
-
githubAppPlugin({
|
|
893
|
-
// ... config
|
|
894
|
-
|
|
895
|
-
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
|
|
896
|
-
switch (event) {
|
|
897
|
-
case 'push':
|
|
898
|
-
// Handle push events
|
|
899
|
-
await ctx.repos.commitRepo.create({
|
|
900
|
-
installationId,
|
|
901
|
-
repository: payload.repository.full_name,
|
|
902
|
-
commits: payload.commits,
|
|
903
|
-
pusher: payload.pusher
|
|
904
|
-
});
|
|
905
|
-
break;
|
|
906
|
-
|
|
907
|
-
case 'pull_request':
|
|
908
|
-
if (action === 'opened') {
|
|
909
|
-
// Handle new PR
|
|
910
|
-
await ctx.repos.prRepo.create({
|
|
911
|
-
installationId,
|
|
912
|
-
prNumber: payload.pull_request.number,
|
|
913
|
-
title: payload.pull_request.title
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
break;
|
|
917
|
-
|
|
918
|
-
case 'installation':
|
|
919
|
-
if (action === 'deleted') {
|
|
920
|
-
// Handle uninstallation
|
|
921
|
-
await ctx.plugins.githubApp.deleteInstallation(installationId);
|
|
922
|
-
}
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
});
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
### Example 4: Creating Issues
|
|
930
|
-
|
|
931
|
-
```typescript
|
|
932
|
-
const CreateIssue: PostHandler = async ({ ctx, auth, req }) => {
|
|
933
|
-
const { owner, repo, title, body } = req.body;
|
|
934
|
-
const userId = auth.tokenData.userId;
|
|
935
|
-
|
|
936
|
-
// Verify user has access to this repo
|
|
937
|
-
const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(
|
|
938
|
-
userId,
|
|
939
|
-
owner,
|
|
940
|
-
repo
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
if (!hasAccess) {
|
|
944
|
-
return forbidden('No access to this repository');
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Get client and create issue
|
|
948
|
-
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
949
|
-
const client = await ctx.plugins.githubApp.getClient(installation!.installationId);
|
|
950
|
-
|
|
951
|
-
const issue = await client.createIssue(owner, repo, { title, body });
|
|
952
|
-
|
|
953
|
-
return { status: 201, data: issue };
|
|
954
|
-
};
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
---
|
|
958
|
-
|
|
959
|
-
## Development Tasks
|
|
960
|
-
|
|
961
|
-
### Phase 1: Core Infrastructure (Week 1-2)
|
|
962
|
-
|
|
963
|
-
**Task 1.1: Project Setup**
|
|
964
|
-
- [ ] Create package structure in `packages/github-app-plugin/`
|
|
965
|
-
- [ ] Set up TypeScript configuration
|
|
966
|
-
- [ ] Configure build scripts
|
|
967
|
-
- [ ] Set up Jest/Jasmine for testing
|
|
968
|
-
- [ ] Create README.md skeleton
|
|
969
|
-
|
|
970
|
-
**Task 1.2: Data Models & Schemas**
|
|
971
|
-
- [ ] Implement `GitHubInstallation` interface
|
|
972
|
-
- [ ] Implement `WebhookEvent` interface
|
|
973
|
-
- [ ] Implement `InstallationCallbackRequest` interface
|
|
974
|
-
- [ ] Implement `WebhookPayload` interface
|
|
975
|
-
- [ ] Add schema validation (optional)
|
|
976
|
-
|
|
977
|
-
**Task 1.3: Utilities**
|
|
978
|
-
- [ ] Implement `jwt-utils.ts` (JWT signing with RS256)
|
|
979
|
-
- [ ] Implement `token-cache-utils.ts` (in-memory token cache)
|
|
980
|
-
- [ ] Implement `webhook-signature-utils.ts` (HMAC validation)
|
|
981
|
-
- [ ] Implement `error-utils.ts` (error codes and handlers)
|
|
982
|
-
- [ ] Add encryption utilities (reuse from oauth-plugin)
|
|
983
|
-
|
|
984
|
-
### Phase 2: Authentication & Token Management (Week 2-3)
|
|
985
|
-
|
|
986
|
-
**Task 2.1: GitHub Auth Service**
|
|
987
|
-
- [ ] Implement JWT generation with private key
|
|
988
|
-
- [ ] Implement installation token exchange
|
|
989
|
-
- [ ] Implement token caching with TTL
|
|
990
|
-
- [ ] Implement token refresh logic
|
|
991
|
-
- [ ] Add error handling for auth failures
|
|
992
|
-
|
|
993
|
-
**Task 2.2: GitHub API Client**
|
|
994
|
-
- [ ] Implement base API client class
|
|
995
|
-
- [ ] Add automatic token injection
|
|
996
|
-
- [ ] Implement common API methods (repos, issues, contents)
|
|
997
|
-
- [ ] Add rate limit handling
|
|
998
|
-
- [ ] Add retry logic with exponential backoff
|
|
999
|
-
|
|
1000
|
-
**Task 2.3: Private Key Validation**
|
|
1001
|
-
- [ ] Validate PEM format on initialization
|
|
1002
|
-
- [ ] Test JWT signing on startup
|
|
1003
|
-
- [ ] Add helpful error messages for invalid keys
|
|
1004
|
-
- [ ] Support both PKCS#1 and PKCS#8 formats
|
|
1005
|
-
|
|
1006
|
-
### Phase 3: HTTP Handlers (Week 3-4)
|
|
1007
|
-
|
|
1008
|
-
**Task 3.1: Installation Initiation Handler**
|
|
1009
|
-
- [ ] Implement `InitiateInstallation.ts`
|
|
1010
|
-
- [ ] Add state generation (CSRF protection)
|
|
1011
|
-
- [ ] Store session in MongoDB
|
|
1012
|
-
- [ ] Build GitHub installation URL
|
|
1013
|
-
- [ ] Add error handling
|
|
1014
|
-
|
|
1015
|
-
**Task 3.2: Installation Callback Handler**
|
|
1016
|
-
- [ ] Implement `InstallationCallback.ts`
|
|
1017
|
-
- [ ] Validate state parameter
|
|
1018
|
-
- [ ] Fetch installation details from GitHub API
|
|
1019
|
-
- [ ] Call `onInstallationSuccess` callback
|
|
1020
|
-
- [ ] Store installation in database
|
|
1021
|
-
- [ ] Handle errors and edge cases
|
|
1022
|
-
|
|
1023
|
-
**Task 3.3: Webhook Handler**
|
|
1024
|
-
- [ ] Implement `WebhookHandler.ts`
|
|
1025
|
-
- [ ] Validate webhook signature
|
|
1026
|
-
- [ ] Parse webhook payload
|
|
1027
|
-
- [ ] Call `onWebhookEvent` callback
|
|
1028
|
-
- [ ] Add webhook event logging (optional)
|
|
1029
|
-
- [ ] Return proper status codes
|
|
1030
|
-
|
|
1031
|
-
**Task 3.4: Uninstall Handler (Optional)**
|
|
1032
|
-
- [ ] Implement `UninstallHandler.ts`
|
|
1033
|
-
- [ ] Verify user ownership
|
|
1034
|
-
- [ ] Delete from database
|
|
1035
|
-
- [ ] Optionally revoke on GitHub
|
|
1036
|
-
|
|
1037
|
-
### Phase 4: Repositories (Week 4)
|
|
1038
|
-
|
|
1039
|
-
**Task 4.1: GitHubInstallationRepo**
|
|
1040
|
-
- [ ] Implement base repository extending FlinkRepo
|
|
1041
|
-
- [ ] Add `findByUserAndInstallationId` method
|
|
1042
|
-
- [ ] Add `findByUserId` method
|
|
1043
|
-
- [ ] Add `findByInstallationId` method
|
|
1044
|
-
- [ ] Add `updateRepositories` method
|
|
1045
|
-
- [ ] Add `suspend` method
|
|
1046
|
-
- [ ] Add `deleteByInstallationId` method
|
|
1047
|
-
|
|
1048
|
-
**Task 4.2: GitHubWebhookEventRepo (Optional)**
|
|
1049
|
-
- [ ] Implement base repository
|
|
1050
|
-
- [ ] Add `findUnprocessed` method
|
|
1051
|
-
- [ ] Add `markProcessed` method
|
|
1052
|
-
- [ ] Add `findByInstallationId` method
|
|
1053
|
-
- [ ] Add TTL index for auto-cleanup
|
|
1054
|
-
|
|
1055
|
-
### Phase 5: Plugin Core (Week 5)
|
|
1056
|
-
|
|
1057
|
-
**Task 5.1: Plugin Factory**
|
|
1058
|
-
- [ ] Implement `githubAppPlugin()` factory function
|
|
1059
|
-
- [ ] Add configuration validation
|
|
1060
|
-
- [ ] Initialize repositories
|
|
1061
|
-
- [ ] Register HTTP handlers
|
|
1062
|
-
- [ ] Set up webhook endpoint
|
|
1063
|
-
- [ ] Initialize services (auth, API client)
|
|
1064
|
-
|
|
1065
|
-
**Task 5.2: Plugin Context**
|
|
1066
|
-
- [ ] Implement `GitHubAppPluginContext` interface
|
|
1067
|
-
- [ ] Add `getClient()` method
|
|
1068
|
-
- [ ] Add `getInstallation()` method
|
|
1069
|
-
- [ ] Add `getInstallations()` method
|
|
1070
|
-
- [ ] Add `deleteInstallation()` method
|
|
1071
|
-
- [ ] Add `hasRepositoryAccess()` method
|
|
1072
|
-
- [ ] Add `getInstallationToken()` method
|
|
1073
|
-
|
|
1074
|
-
**Task 5.3: Plugin Options**
|
|
1075
|
-
- [ ] Implement `GitHubAppPluginOptions` interface
|
|
1076
|
-
- [ ] Add validation for required fields
|
|
1077
|
-
- [ ] Set default values
|
|
1078
|
-
- [ ] Add TypeScript type exports
|
|
1079
|
-
|
|
1080
|
-
### Phase 6: Testing (Week 5-6)
|
|
1081
|
-
|
|
1082
|
-
**Task 6.1: Unit Tests**
|
|
1083
|
-
- [ ] Test JWT signing utilities
|
|
1084
|
-
- [ ] Test webhook signature validation
|
|
1085
|
-
- [ ] Test token caching logic
|
|
1086
|
-
- [ ] Test error handling utilities
|
|
1087
|
-
|
|
1088
|
-
**Task 6.2: Integration Tests**
|
|
1089
|
-
- [ ] Test installation flow end-to-end
|
|
1090
|
-
- [ ] Test webhook processing
|
|
1091
|
-
- [ ] Test API client methods
|
|
1092
|
-
- [ ] Test repository operations
|
|
1093
|
-
- [ ] Mock GitHub API responses
|
|
1094
|
-
|
|
1095
|
-
**Task 6.3: Security Tests**
|
|
1096
|
-
- [ ] Test CSRF protection
|
|
1097
|
-
- [ ] Test webhook signature validation
|
|
1098
|
-
- [ ] Test private key validation
|
|
1099
|
-
- [ ] Test token expiration handling
|
|
1100
|
-
|
|
1101
|
-
### Phase 7: Documentation (Week 6)
|
|
1102
|
-
|
|
1103
|
-
**Task 7.1: README.md**
|
|
1104
|
-
- [ ] Add installation instructions
|
|
1105
|
-
- [ ] Document GitHub App setup process
|
|
1106
|
-
- [ ] Add configuration examples
|
|
1107
|
-
- [ ] Document all callback functions
|
|
1108
|
-
- [ ] Add usage examples
|
|
1109
|
-
- [ ] Create troubleshooting section
|
|
1110
|
-
|
|
1111
|
-
**Task 7.2: SECURITY.md**
|
|
1112
|
-
- [ ] Document private key management
|
|
1113
|
-
- [ ] Document webhook security
|
|
1114
|
-
- [ ] Document CSRF protection
|
|
1115
|
-
- [ ] Add security checklist
|
|
1116
|
-
- [ ] Document rate limiting
|
|
1117
|
-
|
|
1118
|
-
**Task 7.3: Examples**
|
|
1119
|
-
- [ ] Create basic installation example
|
|
1120
|
-
- [ ] Create webhook handling example
|
|
1121
|
-
- [ ] Create API client usage example
|
|
1122
|
-
- [ ] Create multi-tenant example
|
|
1123
|
-
|
|
1124
|
-
**Task 7.4: API Documentation**
|
|
1125
|
-
- [ ] Document all public interfaces
|
|
1126
|
-
- [ ] Add JSDoc comments
|
|
1127
|
-
- [ ] Generate TypeDoc documentation (optional)
|
|
1128
|
-
|
|
1129
|
-
### Phase 8: Polish & Release (Week 7)
|
|
1130
|
-
|
|
1131
|
-
**Task 8.1: Code Review**
|
|
1132
|
-
- [ ] Review all code for consistency
|
|
1133
|
-
- [ ] Check error handling coverage
|
|
1134
|
-
- [ ] Verify security implementations
|
|
1135
|
-
- [ ] Check TypeScript types
|
|
1136
|
-
|
|
1137
|
-
**Task 8.2: Package Preparation**
|
|
1138
|
-
- [ ] Update package.json
|
|
1139
|
-
- [ ] Add LICENSE file
|
|
1140
|
-
- [ ] Add CHANGELOG.md
|
|
1141
|
-
- [ ] Test npm package build
|
|
1142
|
-
|
|
1143
|
-
**Task 8.3: Integration Testing**
|
|
1144
|
-
- [ ] Test with demo Flink app
|
|
1145
|
-
- [ ] Test with OAuth plugin integration
|
|
1146
|
-
- [ ] Test webhook delivery
|
|
1147
|
-
- [ ] Test rate limiting
|
|
1148
|
-
|
|
1149
|
-
**Task 8.4: Release**
|
|
1150
|
-
- [ ] Version bump (0.1.0-alpha.1)
|
|
1151
|
-
- [ ] Publish to npm
|
|
1152
|
-
- [ ] Create GitHub release
|
|
1153
|
-
- [ ] Update main Flink documentation
|
|
1154
|
-
|
|
1155
|
-
---
|
|
1156
|
-
|
|
1157
|
-
## Testing Requirements
|
|
1158
|
-
|
|
1159
|
-
### Unit Tests
|
|
1160
|
-
|
|
1161
|
-
Required test coverage:
|
|
1162
|
-
|
|
1163
|
-
1. **JWT Utilities**
|
|
1164
|
-
- Valid JWT generation
|
|
1165
|
-
- Invalid private key handling
|
|
1166
|
-
- Token expiration
|
|
1167
|
-
- Algorithm validation
|
|
1168
|
-
|
|
1169
|
-
2. **Webhook Validation**
|
|
1170
|
-
- Valid signature verification
|
|
1171
|
-
- Invalid signature rejection
|
|
1172
|
-
- Constant-time comparison
|
|
1173
|
-
- Missing signature handling
|
|
1174
|
-
|
|
1175
|
-
3. **Token Cache**
|
|
1176
|
-
- Cache hit/miss
|
|
1177
|
-
- TTL expiration
|
|
1178
|
-
- Cache invalidation
|
|
1179
|
-
|
|
1180
|
-
4. **Error Utilities**
|
|
1181
|
-
- Error code mapping
|
|
1182
|
-
- User-friendly messages
|
|
1183
|
-
- Details sanitization
|
|
1184
|
-
|
|
1185
|
-
### Integration Tests
|
|
1186
|
-
|
|
1187
|
-
1. **Installation Flow**
|
|
1188
|
-
- Full installation process
|
|
1189
|
-
- State validation
|
|
1190
|
-
- Callback handling
|
|
1191
|
-
- Database storage
|
|
1192
|
-
|
|
1193
|
-
2. **Webhook Processing**
|
|
1194
|
-
- Signature validation
|
|
1195
|
-
- Event parsing
|
|
1196
|
-
- Callback execution
|
|
1197
|
-
- Event logging
|
|
1198
|
-
|
|
1199
|
-
3. **API Client**
|
|
1200
|
-
- Token injection
|
|
1201
|
-
- API calls
|
|
1202
|
-
- Error handling
|
|
1203
|
-
- Rate limit handling
|
|
1204
|
-
|
|
1205
|
-
### Security Tests
|
|
1206
|
-
|
|
1207
|
-
1. **CSRF Protection**
|
|
1208
|
-
- State parameter validation
|
|
1209
|
-
- Replay attack prevention
|
|
1210
|
-
- State expiration
|
|
1211
|
-
|
|
1212
|
-
2. **Webhook Security**
|
|
1213
|
-
- Signature verification
|
|
1214
|
-
- Timing attack resistance
|
|
1215
|
-
- Payload validation
|
|
1216
|
-
|
|
1217
|
-
3. **Token Security**
|
|
1218
|
-
- Expiration handling
|
|
1219
|
-
- Cache security
|
|
1220
|
-
- No token leakage
|
|
1221
|
-
|
|
1222
|
-
---
|
|
1223
|
-
|
|
1224
|
-
## Documentation Requirements
|
|
1225
|
-
|
|
1226
|
-
### README.md Sections
|
|
1227
|
-
|
|
1228
|
-
1. **Overview** - What the plugin does
|
|
1229
|
-
2. **Installation** - npm install instructions
|
|
1230
|
-
3. **Prerequisites** - GitHub App setup guide
|
|
1231
|
-
4. **Quick Start** - Minimal working example
|
|
1232
|
-
5. **Configuration** - All options documented
|
|
1233
|
-
6. **GitHub App Setup** - Step-by-step guide
|
|
1234
|
-
7. **Installation Flow** - How users install the app
|
|
1235
|
-
8. **Webhook Setup** - How to configure webhooks
|
|
1236
|
-
9. **Context API** - All available methods
|
|
1237
|
-
10. **Examples** - Real-world usage examples
|
|
1238
|
-
11. **Security** - Security considerations
|
|
1239
|
-
12. **Troubleshooting** - Common issues and solutions
|
|
1240
|
-
13. **API Reference** - Full TypeScript API docs
|
|
1241
|
-
|
|
1242
|
-
### SECURITY.md Sections
|
|
1243
|
-
|
|
1244
|
-
1. **Private Key Management**
|
|
1245
|
-
2. **JWT Signing Security**
|
|
1246
|
-
3. **Webhook Signature Validation**
|
|
1247
|
-
4. **CSRF Protection**
|
|
1248
|
-
5. **Token Caching Security**
|
|
1249
|
-
6. **HTTPS Requirements**
|
|
1250
|
-
7. **Secrets Management**
|
|
1251
|
-
8. **Security Checklist**
|
|
1252
|
-
9. **Reporting Security Issues**
|
|
1253
|
-
|
|
1254
|
-
### Code Examples Required
|
|
1255
|
-
|
|
1256
|
-
1. Basic installation flow
|
|
1257
|
-
2. Webhook event handling
|
|
1258
|
-
3. Repository access
|
|
1259
|
-
4. Creating issues
|
|
1260
|
-
5. File content access
|
|
1261
|
-
6. Multi-tenant setup
|
|
1262
|
-
7. Error handling
|
|
1263
|
-
8. Integration with OAuth plugin
|
|
1264
|
-
|
|
1265
|
-
---
|
|
1266
|
-
|
|
1267
|
-
## Success Criteria
|
|
1268
|
-
|
|
1269
|
-
The GitHub App Plugin will be considered complete when:
|
|
1270
|
-
|
|
1271
|
-
✅ Users can install GitHub Apps and select specific repositories
|
|
1272
|
-
✅ Application can access selected repositories via GitHub API
|
|
1273
|
-
✅ Webhooks are processed with signature validation
|
|
1274
|
-
✅ Private keys are securely stored and validated
|
|
1275
|
-
✅ Installation tokens are automatically refreshed
|
|
1276
|
-
✅ Integration with OAuth Plugin works seamlessly
|
|
1277
|
-
✅ Comprehensive error handling is implemented
|
|
1278
|
-
✅ All tests pass with >80% coverage
|
|
1279
|
-
✅ Documentation is complete and accurate
|
|
1280
|
-
✅ Security best practices are followed
|
|
1281
|
-
|
|
1282
|
-
---
|
|
1283
|
-
|
|
1284
|
-
## Future Enhancements (Out of Scope for v1)
|
|
1285
|
-
|
|
1286
|
-
- Support for GitHub Enterprise Server
|
|
1287
|
-
- Proactive token refresh before expiration
|
|
1288
|
-
- Installation token revocation
|
|
1289
|
-
- Repository permission auditing
|
|
1290
|
-
- Webhook event replay mechanism
|
|
1291
|
-
- GitHub Actions integration
|
|
1292
|
-
- GraphQL API support
|
|
1293
|
-
- Multi-region deployment support
|
|
1294
|
-
- Advanced rate limit handling with queuing
|
|
1295
|
-
- Webhook event transformation/filtering
|
|
1296
|
-
|
|
1297
|
-
---
|
|
1298
|
-
|
|
1299
|
-
## Questions & Decisions
|
|
1300
|
-
|
|
1301
|
-
### Open Questions
|
|
1302
|
-
|
|
1303
|
-
1. Should we support GitHub Enterprise Server in v1?
|
|
1304
|
-
- **Decision:** No, add in v2 if needed
|
|
1305
|
-
|
|
1306
|
-
2. Should webhook events be logged by default?
|
|
1307
|
-
- **Decision:** Optional, controlled by config flag
|
|
1308
|
-
|
|
1309
|
-
3. Should we cache installation details or always fetch fresh?
|
|
1310
|
-
- **Decision:** Cache with configurable TTL
|
|
1311
|
-
|
|
1312
|
-
4. How to handle installation updates (repo add/remove)?
|
|
1313
|
-
- **Decision:** Process via webhooks
|
|
1314
|
-
|
|
1315
|
-
5. Should we support multiple GitHub Apps per Flink app?
|
|
1316
|
-
- **Decision:** Yes, via multi-tenant pattern
|
|
1317
|
-
|
|
1318
|
-
### Resolved Decisions
|
|
1319
|
-
|
|
1320
|
-
- ✅ Use RS256 algorithm for JWT signing
|
|
1321
|
-
- ✅ Cache installation tokens in memory (not database)
|
|
1322
|
-
- ✅ Follow OAuth plugin architecture patterns
|
|
1323
|
-
- ✅ Use FlinkRepo for database operations
|
|
1324
|
-
- ✅ Integrate with JWT Auth Plugin for user linking
|
|
1325
|
-
- ✅ Support both user and organization installations
|
|
1326
|
-
- ✅ Provide webhook handler out of the box
|
|
1327
|
-
|
|
1328
|
-
---
|
|
1329
|
-
|
|
1330
|
-
## Appendix
|
|
1331
|
-
|
|
1332
|
-
### GitHub App Permissions
|
|
1333
|
-
|
|
1334
|
-
Common permission configurations:
|
|
1335
|
-
|
|
1336
|
-
**Read-Only Access:**
|
|
1337
|
-
```yaml
|
|
1338
|
-
contents: read
|
|
1339
|
-
metadata: read
|
|
1340
|
-
```
|
|
1341
|
-
|
|
1342
|
-
**Issue Management:**
|
|
1343
|
-
```yaml
|
|
1344
|
-
contents: read
|
|
1345
|
-
issues: write
|
|
1346
|
-
metadata: read
|
|
1347
|
-
```
|
|
1348
|
-
|
|
1349
|
-
**Full Repository Access:**
|
|
1350
|
-
```yaml
|
|
1351
|
-
contents: write
|
|
1352
|
-
issues: write
|
|
1353
|
-
pull_requests: write
|
|
1354
|
-
metadata: read
|
|
1355
|
-
```
|
|
1356
|
-
|
|
1357
|
-
### GitHub App Events
|
|
1358
|
-
|
|
1359
|
-
Common webhook events:
|
|
1360
|
-
|
|
1361
|
-
- `push` - Code pushed to repository
|
|
1362
|
-
- `pull_request` - PR opened/closed/merged
|
|
1363
|
-
- `issues` - Issue opened/closed/commented
|
|
1364
|
-
- `installation` - App installed/uninstalled
|
|
1365
|
-
- `installation_repositories` - Repos added/removed
|
|
1366
|
-
- `member` - Collaborator added/removed
|
|
1367
|
-
- `release` - Release published
|
|
1368
|
-
|
|
1369
|
-
### Useful GitHub API Endpoints
|
|
1370
|
-
|
|
1371
|
-
```
|
|
1372
|
-
GET /app/installations - List installations
|
|
1373
|
-
GET /installation/repositories - List accessible repos
|
|
1374
|
-
GET /repos/{owner}/{repo} - Get repository details
|
|
1375
|
-
GET /repos/{owner}/{repo}/contents/{path} - Get file contents
|
|
1376
|
-
POST /repos/{owner}/{repo}/issues - Create issue
|
|
1377
|
-
GET /repos/{owner}/{repo}/issues - List issues
|
|
1378
|
-
POST /repos/{owner}/{repo}/pulls - Create pull request
|
|
1379
|
-
```
|
|
1380
|
-
|
|
1381
|
-
---
|
|
1382
|
-
|
|
1383
|
-
**End of PRD**
|