@flink-app/github-app-plugin 0.12.1-alpha.38 → 0.12.1-alpha.40
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 +106 -163
- package/dist/GitHubAppInternalContext.d.ts +4 -33
- package/dist/GitHubAppPlugin.js +16 -7
- package/dist/GitHubAppPluginContext.d.ts +57 -0
- package/dist/GitHubAppPluginOptions.d.ts +10 -53
- package/dist/handlers/InstallationCallback.d.ts +3 -9
- package/dist/handlers/WebhookHandler.d.ts +3 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +3 -1
- package/dist/services/InstallationService.d.ts +109 -0
- package/dist/services/InstallationService.js +224 -0
- package/package.json +4 -4
- package/spec/handlers.spec.ts +1 -130
- package/spec/integration-and-security.spec.ts +45 -47
- package/spec/plugin-core.spec.ts +6 -10
- package/spec/project-setup.spec.ts +0 -2
- package/src/GitHubAppInternalContext.ts +16 -0
- package/src/GitHubAppPlugin.ts +23 -7
- package/src/GitHubAppPluginContext.ts +58 -0
- package/src/GitHubAppPluginOptions.ts +11 -55
- package/src/handlers/WebhookHandler.ts +3 -11
- package/src/index.ts +14 -10
- package/src/services/InstallationService.ts +302 -0
- package/src/handlers/InstallationCallback.ts +0 -292
- package/src/schemas/InstallationCallbackRequest.ts +0 -10
package/README.md
CHANGED
|
@@ -82,6 +82,8 @@ The plugin requires MongoDB to store installation data and sessions.
|
|
|
82
82
|
|
|
83
83
|
## Quick Start
|
|
84
84
|
|
|
85
|
+
### 1. Configure the Plugin
|
|
86
|
+
|
|
85
87
|
```typescript
|
|
86
88
|
import { FlinkApp } from "@flink-app/flink";
|
|
87
89
|
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
@@ -102,45 +104,13 @@ const app = new FlinkApp<Context>({
|
|
|
102
104
|
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
103
105
|
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
104
106
|
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
105
|
-
|
|
106
|
-
// Optional: App slug for installation URL
|
|
107
107
|
appSlug: "my-flink-app",
|
|
108
108
|
|
|
109
|
-
// Required: Callback after successful installation
|
|
110
|
-
onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
|
|
111
|
-
// Your app determines how to get userId
|
|
112
|
-
// This could come from session, custom auth, or any other source
|
|
113
|
-
const userId = getUserIdFromSession(); // Your implementation
|
|
114
|
-
|
|
115
|
-
console.log(`User ${userId} installed app on ${account.login}`);
|
|
116
|
-
console.log(`Granted access to ${repositories.length} repositories`);
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
userId, // Link installation to this user
|
|
120
|
-
redirectUrl: "/dashboard/repos", // Where to redirect after installation
|
|
121
|
-
};
|
|
122
|
-
},
|
|
123
|
-
|
|
124
109
|
// Optional: Handle webhook events
|
|
125
110
|
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
|
|
126
|
-
console.log(`Received ${event} event for installation ${installationId}`);
|
|
127
|
-
|
|
128
|
-
// Handle specific events
|
|
129
111
|
if (event === "push") {
|
|
130
112
|
console.log(`Push to ${payload.repository.full_name}`);
|
|
131
113
|
}
|
|
132
|
-
|
|
133
|
-
if (event === "pull_request" && action === "opened") {
|
|
134
|
-
console.log(`New PR #${payload.pull_request.number}`);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
// Optional: Handle installation errors
|
|
139
|
-
onInstallationError: async ({ error, installationId }) => {
|
|
140
|
-
console.error(`Installation error:`, error);
|
|
141
|
-
return {
|
|
142
|
-
redirectUrl: "/error?message=installation-failed",
|
|
143
|
-
};
|
|
144
114
|
},
|
|
145
115
|
}),
|
|
146
116
|
],
|
|
@@ -149,6 +119,54 @@ const app = new FlinkApp<Context>({
|
|
|
149
119
|
await app.start();
|
|
150
120
|
```
|
|
151
121
|
|
|
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
|
+
};
|
|
134
|
+
|
|
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
|
+
};
|
|
166
|
+
|
|
167
|
+
export default GetGitHubInstallCallback;
|
|
168
|
+
```
|
|
169
|
+
|
|
152
170
|
## Configuration
|
|
153
171
|
|
|
154
172
|
### GitHubAppPluginOptions
|
|
@@ -162,8 +180,6 @@ await app.start();
|
|
|
162
180
|
| `clientSecret` | `string` | Yes | - | GitHub App client secret |
|
|
163
181
|
| `appSlug` | `string` | No | Auto-detected | GitHub App slug (used in installation URL) |
|
|
164
182
|
| `baseUrl` | `string` | No | `https://api.github.com` | GitHub API base URL (for GitHub Enterprise) |
|
|
165
|
-
| `onInstallationSuccess` | `Function` | Yes | - | Callback after successful installation |
|
|
166
|
-
| `onInstallationError` | `Function` | No | - | Callback on installation errors |
|
|
167
183
|
| `onWebhookEvent` | `Function` | No | - | Callback for webhook events |
|
|
168
184
|
| `sessionsCollectionName` | `string` | No | `github_app_sessions` | MongoDB collection for sessions |
|
|
169
185
|
| `installationsCollectionName` | `string` | No | `github_installations` | MongoDB collection for installations |
|
|
@@ -175,73 +191,6 @@ await app.start();
|
|
|
175
191
|
|
|
176
192
|
### Callback Functions
|
|
177
193
|
|
|
178
|
-
#### onInstallationSuccess (Required)
|
|
179
|
-
|
|
180
|
-
Called when a user successfully installs the GitHub App.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
onInstallationSuccess: async (
|
|
184
|
-
params: {
|
|
185
|
-
installationId: number;
|
|
186
|
-
account: {
|
|
187
|
-
id: number;
|
|
188
|
-
login: string;
|
|
189
|
-
type: "User" | "Organization";
|
|
190
|
-
avatarUrl?: string;
|
|
191
|
-
};
|
|
192
|
-
repositories: {
|
|
193
|
-
id: number;
|
|
194
|
-
name: string;
|
|
195
|
-
fullName: string;
|
|
196
|
-
private: boolean;
|
|
197
|
-
}[];
|
|
198
|
-
permissions: Record<string, string>;
|
|
199
|
-
events: string[];
|
|
200
|
-
},
|
|
201
|
-
ctx: Context
|
|
202
|
-
) =>
|
|
203
|
-
Promise<{
|
|
204
|
-
userId: string;
|
|
205
|
-
redirectUrl?: string;
|
|
206
|
-
}>;
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
**Example:**
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
|
|
213
|
-
// Get userId from your authentication system
|
|
214
|
-
const userId = ctx.session?.userId || "anonymous";
|
|
215
|
-
|
|
216
|
-
// Optionally notify user
|
|
217
|
-
console.log(`Installation ${installationId} linked to user ${userId}`);
|
|
218
|
-
console.log(`Access granted to ${repositories.length} repositories`);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
userId, // Required: Link installation to this user
|
|
222
|
-
redirectUrl: "/dashboard", // Optional: Redirect after installation
|
|
223
|
-
};
|
|
224
|
-
};
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
#### onInstallationError (Optional)
|
|
228
|
-
|
|
229
|
-
Called when installation fails.
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
onInstallationError: async (params: {
|
|
233
|
-
error: {
|
|
234
|
-
code: string;
|
|
235
|
-
message: string;
|
|
236
|
-
details?: any;
|
|
237
|
-
};
|
|
238
|
-
installationId?: number;
|
|
239
|
-
}) =>
|
|
240
|
-
Promise<{
|
|
241
|
-
redirectUrl?: string;
|
|
242
|
-
}>;
|
|
243
|
-
```
|
|
244
|
-
|
|
245
194
|
#### onWebhookEvent (Optional)
|
|
246
195
|
|
|
247
196
|
Called when a webhook event is received.
|
|
@@ -411,6 +360,24 @@ if (!hasAccess) {
|
|
|
411
360
|
}
|
|
412
361
|
```
|
|
413
362
|
|
|
363
|
+
### completeInstallation(params)
|
|
364
|
+
|
|
365
|
+
Complete GitHub App installation after callback from GitHub.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
const result = await ctx.plugins.githubApp.completeInstallation({
|
|
369
|
+
installationId: 12345,
|
|
370
|
+
state: "csrf-state-token",
|
|
371
|
+
userId: "user-123",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (result.success) {
|
|
375
|
+
console.log("Installation completed:", result.installation);
|
|
376
|
+
} else {
|
|
377
|
+
console.error("Installation failed:", result.error);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
414
381
|
### getInstallationToken(installationId)
|
|
415
382
|
|
|
416
383
|
Get raw installation access token (for advanced usage).
|
|
@@ -454,80 +421,56 @@ const issue = await client.createIssue("facebook", "react", {
|
|
|
454
421
|
const response = await client.request("GET", "/rate_limit");
|
|
455
422
|
```
|
|
456
423
|
|
|
457
|
-
##
|
|
424
|
+
## Authentication Integration
|
|
458
425
|
|
|
459
|
-
This plugin is **
|
|
426
|
+
This plugin is **auth-agnostic** and works with any authentication system. You implement your own installation callback handler with your own auth logic.
|
|
460
427
|
|
|
461
|
-
### Example with
|
|
428
|
+
### Example with Session-Based Auth
|
|
462
429
|
|
|
463
430
|
```typescript
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (!userId) {
|
|
472
|
-
throw new Error("User not authenticated");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
userId,
|
|
477
|
-
redirectUrl: "/dashboard",
|
|
478
|
-
};
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
```
|
|
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
|
+
}
|
|
482
438
|
|
|
483
|
-
|
|
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
|
+
});
|
|
484
445
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
onInstallationSuccess: async ({ installationId, repositories }, ctx) => {
|
|
490
|
-
// Get userId from your custom auth token
|
|
491
|
-
const authHeader = ctx.req.headers.authorization;
|
|
492
|
-
const userId = parseAuthToken(authHeader); // Your function
|
|
493
|
-
|
|
494
|
-
return {
|
|
495
|
-
userId,
|
|
496
|
-
redirectUrl: "/dashboard",
|
|
497
|
-
};
|
|
498
|
-
},
|
|
499
|
-
});
|
|
446
|
+
return result.success
|
|
447
|
+
? redirect("/dashboard")
|
|
448
|
+
: redirect(`/error?code=${result.error.code}`);
|
|
449
|
+
};
|
|
500
450
|
```
|
|
501
451
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
While the plugin works standalone, you can optionally use it with the JWT Auth Plugin:
|
|
452
|
+
### Example with JWT Auth Plugin
|
|
505
453
|
|
|
506
454
|
```typescript
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
auth
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
plugins: [
|
|
516
|
-
githubAppPlugin({
|
|
517
|
-
// ... config
|
|
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
|
+
}
|
|
518
462
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
});
|
|
522
469
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
},
|
|
528
|
-
}),
|
|
529
|
-
],
|
|
530
|
-
});
|
|
470
|
+
return result.success
|
|
471
|
+
? redirect("/dashboard/github")
|
|
472
|
+
: redirect(`/error?code=${result.error.code}`);
|
|
473
|
+
};
|
|
531
474
|
```
|
|
532
475
|
|
|
533
476
|
## Security Considerations
|
|
@@ -1,44 +1,15 @@
|
|
|
1
1
|
import { FlinkContext } from "@flink-app/flink";
|
|
2
|
-
import {
|
|
3
|
-
import { WebhookValidator } from "./services/WebhookValidator";
|
|
2
|
+
import { GitHubAppPluginContext } from "./GitHubAppPluginContext";
|
|
4
3
|
import GitHubAppSessionRepo from "./repos/GitHubAppSessionRepo";
|
|
5
4
|
import GitHubInstallationRepo from "./repos/GitHubInstallationRepo";
|
|
6
5
|
import GitHubWebhookEventRepo from "./repos/GitHubWebhookEventRepo";
|
|
7
|
-
import { GitHubAppPluginOptions } from "./GitHubAppPluginOptions";
|
|
8
6
|
/**
|
|
9
|
-
* Internal context
|
|
10
|
-
*
|
|
11
|
-
* This interface extends the base FlinkContext with GitHub App Plugin-specific
|
|
12
|
-
* repositories, services, and configuration. It's used internally by handlers,
|
|
13
|
-
* services, and the plugin itself.
|
|
7
|
+
* Internal context used for plugin handlers.
|
|
14
8
|
*/
|
|
15
|
-
export interface GitHubAppInternalContext extends FlinkContext {
|
|
16
|
-
|
|
17
|
-
* GitHub App Session Repository
|
|
18
|
-
* Manages temporary installation sessions for CSRF protection
|
|
19
|
-
*/
|
|
20
|
-
repos: FlinkContext['repos'] & {
|
|
9
|
+
export interface GitHubAppInternalContext extends FlinkContext<GitHubAppPluginContext> {
|
|
10
|
+
repos: {
|
|
21
11
|
githubAppSessionRepo: GitHubAppSessionRepo;
|
|
22
12
|
githubInstallationRepo: GitHubInstallationRepo;
|
|
23
13
|
githubWebhookEventRepo?: GitHubWebhookEventRepo;
|
|
24
14
|
};
|
|
25
|
-
/**
|
|
26
|
-
* Internal plugin state and services
|
|
27
|
-
*/
|
|
28
|
-
_githubAppInternal: {
|
|
29
|
-
/**
|
|
30
|
-
* GitHub Authentication Service
|
|
31
|
-
* Handles JWT generation and installation token management
|
|
32
|
-
*/
|
|
33
|
-
authService: GitHubAuthService;
|
|
34
|
-
/**
|
|
35
|
-
* Webhook Validator
|
|
36
|
-
* Validates webhook signatures and parses payloads
|
|
37
|
-
*/
|
|
38
|
-
webhookValidator: WebhookValidator;
|
|
39
|
-
/**
|
|
40
|
-
* Plugin configuration options
|
|
41
|
-
*/
|
|
42
|
-
options: GitHubAppPluginOptions;
|
|
43
|
-
};
|
|
44
15
|
}
|
package/dist/GitHubAppPlugin.js
CHANGED
|
@@ -34,9 +34,9 @@ const GitHubWebhookEventRepo_1 = __importDefault(require("./repos/GitHubWebhookE
|
|
|
34
34
|
const GitHubAuthService_1 = require("./services/GitHubAuthService");
|
|
35
35
|
const GitHubAPIClient_1 = require("./services/GitHubAPIClient");
|
|
36
36
|
const WebhookValidator_1 = require("./services/WebhookValidator");
|
|
37
|
+
const InstallationService_1 = require("./services/InstallationService");
|
|
37
38
|
const error_utils_1 = require("./utils/error-utils");
|
|
38
39
|
const state_utils_1 = require("./utils/state-utils");
|
|
39
|
-
const InstallationCallback = __importStar(require("./handlers/InstallationCallback"));
|
|
40
40
|
const WebhookHandler = __importStar(require("./handlers/WebhookHandler"));
|
|
41
41
|
/**
|
|
42
42
|
* GitHub App Plugin Factory Function
|
|
@@ -97,9 +97,6 @@ function githubAppPlugin(options) {
|
|
|
97
97
|
if (!options.clientSecret) {
|
|
98
98
|
throw new Error("GitHub App Plugin: clientSecret is required");
|
|
99
99
|
}
|
|
100
|
-
if (!options.onInstallationSuccess) {
|
|
101
|
-
throw new Error("GitHub App Plugin: onInstallationSuccess callback is required");
|
|
102
|
-
}
|
|
103
100
|
// Determine configuration defaults
|
|
104
101
|
const baseUrl = options.baseUrl || "https://api.github.com";
|
|
105
102
|
const tokenCacheTTL = options.tokenCacheTTL || 3300; // 55 minutes
|
|
@@ -109,6 +106,7 @@ function githubAppPlugin(options) {
|
|
|
109
106
|
let flinkApp;
|
|
110
107
|
let authService;
|
|
111
108
|
let webhookValidator;
|
|
109
|
+
let installationService;
|
|
112
110
|
let sessionRepo;
|
|
113
111
|
let installationRepo;
|
|
114
112
|
let webhookEventRepo;
|
|
@@ -143,6 +141,8 @@ function githubAppPlugin(options) {
|
|
|
143
141
|
// Add repositories to FlinkApp
|
|
144
142
|
flinkApp.addRepo("githubAppSessionRepo", sessionRepo);
|
|
145
143
|
flinkApp.addRepo("githubInstallationRepo", installationRepo);
|
|
144
|
+
// Initialize InstallationService
|
|
145
|
+
installationService = new InstallationService_1.InstallationService(authService, sessionRepo, installationRepo, baseUrl);
|
|
146
146
|
// Conditionally initialize webhook event repo if logging is enabled
|
|
147
147
|
if (logWebhookEvents) {
|
|
148
148
|
webhookEventRepo = new GitHubWebhookEventRepo_1.default(flinkApp.ctx, db, webhookEventsCollectionName);
|
|
@@ -160,11 +160,10 @@ function githubAppPlugin(options) {
|
|
|
160
160
|
await db.collection(webhookEventsCollectionName).createIndex({ createdAt: 1 }, { expireAfterSeconds: webhookEventTTL });
|
|
161
161
|
flink_1.log.info(`GitHub App Plugin: Created TTL index on ${webhookEventsCollectionName} with ${webhookEventTTL}s expiration`);
|
|
162
162
|
}
|
|
163
|
-
// Conditionally register handlers (only
|
|
163
|
+
// Conditionally register handlers (only webhook handler)
|
|
164
164
|
if (registerRoutes) {
|
|
165
|
-
flinkApp.addHandler(InstallationCallback);
|
|
166
165
|
flinkApp.addHandler(WebhookHandler);
|
|
167
|
-
flink_1.log.info("GitHub App Plugin: Registered
|
|
166
|
+
flink_1.log.info("GitHub App Plugin: Registered webhook handler");
|
|
168
167
|
}
|
|
169
168
|
else {
|
|
170
169
|
flink_1.log.info("GitHub App Plugin: Skipped handler registration (routes disabled)");
|
|
@@ -213,6 +212,15 @@ function githubAppPlugin(options) {
|
|
|
213
212
|
sessionId,
|
|
214
213
|
};
|
|
215
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Complete GitHub App installation
|
|
217
|
+
*/
|
|
218
|
+
async function completeInstallation(params) {
|
|
219
|
+
if (!installationService) {
|
|
220
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
221
|
+
}
|
|
222
|
+
return installationService.completeInstallation(params);
|
|
223
|
+
}
|
|
216
224
|
/**
|
|
217
225
|
* Uninstalls GitHub App for a user
|
|
218
226
|
*/
|
|
@@ -343,6 +351,7 @@ function githubAppPlugin(options) {
|
|
|
343
351
|
*/
|
|
344
352
|
const pluginCtx = {
|
|
345
353
|
initiateInstallation,
|
|
354
|
+
completeInstallation,
|
|
346
355
|
uninstall,
|
|
347
356
|
getClient,
|
|
348
357
|
getInstallation,
|
|
@@ -3,6 +3,7 @@ import GitHubInstallation from "./schemas/GitHubInstallation";
|
|
|
3
3
|
import { GitHubAppPluginOptions } from "./GitHubAppPluginOptions";
|
|
4
4
|
import { GitHubAuthService } from "./services/GitHubAuthService";
|
|
5
5
|
import { WebhookValidator } from "./services/WebhookValidator";
|
|
6
|
+
import { CompleteInstallationResult } from "./services/InstallationService";
|
|
6
7
|
/**
|
|
7
8
|
* Public context interface exposed via ctx.plugins.githubApp
|
|
8
9
|
*
|
|
@@ -50,6 +51,62 @@ export interface GitHubAppPluginContext {
|
|
|
50
51
|
state: string;
|
|
51
52
|
sessionId: string;
|
|
52
53
|
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Complete GitHub App installation
|
|
56
|
+
*
|
|
57
|
+
* Handles all the boilerplate of completing a GitHub App installation:
|
|
58
|
+
* 1. Validates the state parameter (CSRF protection)
|
|
59
|
+
* 2. Fetches installation details from GitHub API
|
|
60
|
+
* 3. Stores the installation in database
|
|
61
|
+
*
|
|
62
|
+
* The host application should:
|
|
63
|
+
* - Check if user is authenticated
|
|
64
|
+
* - Parse query parameters from the callback URL
|
|
65
|
+
* - Call this method with userId
|
|
66
|
+
* - Handle the response and redirect
|
|
67
|
+
*
|
|
68
|
+
* @param params - Installation completion parameters
|
|
69
|
+
* @param params.installationId - GitHub installation ID from query params
|
|
70
|
+
* @param params.state - State parameter from query params (CSRF protection)
|
|
71
|
+
* @param params.userId - Application user ID (from auth system)
|
|
72
|
+
* @returns Result with installation details or error
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // In your custom callback handler
|
|
77
|
+
* export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
|
|
78
|
+
* // 1. Check authentication (your way)
|
|
79
|
+
* if (!ctx.auth?.tokenData?.userId) {
|
|
80
|
+
* return unauthorized('Please log in first');
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* // 2. Parse query params
|
|
84
|
+
* const { installation_id, state } = req.query;
|
|
85
|
+
* if (!installation_id || !state) {
|
|
86
|
+
* return badRequest('Missing required parameters');
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* // 3. Complete installation
|
|
90
|
+
* const result = await ctx.plugins.githubApp.completeInstallation({
|
|
91
|
+
* installationId: parseInt(installation_id),
|
|
92
|
+
* state,
|
|
93
|
+
* userId: ctx.auth.tokenData.userId
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* // 4. Handle response (your way)
|
|
97
|
+
* if (!result.success) {
|
|
98
|
+
* return redirect(`/error?code=${result.error.code}`);
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* return redirect('/dashboard/github');
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
completeInstallation(params: {
|
|
106
|
+
installationId: number;
|
|
107
|
+
state: string;
|
|
108
|
+
userId: string;
|
|
109
|
+
}): Promise<CompleteInstallationResult>;
|
|
53
110
|
/**
|
|
54
111
|
* Uninstalls GitHub App for a user
|
|
55
112
|
*
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
export interface InstallationSuccessParams {
|
|
8
8
|
/** GitHub installation ID */
|
|
9
9
|
installationId: number;
|
|
10
|
+
/** Setup action type: 'install' for new installations, 'update' for repository selection updates */
|
|
11
|
+
setupAction: string;
|
|
10
12
|
/** GitHub account (user or organization) where app was installed */
|
|
11
13
|
account: {
|
|
12
14
|
/** Account ID on GitHub */
|
|
@@ -134,8 +136,11 @@ export interface WebhookEventParams {
|
|
|
134
136
|
/**
|
|
135
137
|
* Configuration options for GitHub App Plugin
|
|
136
138
|
*
|
|
137
|
-
* Configure the plugin with your GitHub App credentials
|
|
138
|
-
*
|
|
139
|
+
* Configure the plugin with your GitHub App credentials and optional settings
|
|
140
|
+
* for database, caching, and features.
|
|
141
|
+
*
|
|
142
|
+
* Note: Installation callback handler must be implemented by the host app.
|
|
143
|
+
* Use ctx.plugins.githubApp.completeInstallation() in your own handler.
|
|
139
144
|
*
|
|
140
145
|
* @example
|
|
141
146
|
* ```typescript
|
|
@@ -146,12 +151,7 @@ export interface WebhookEventParams {
|
|
|
146
151
|
* webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
147
152
|
* clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
148
153
|
* clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
149
|
-
*
|
|
150
|
-
* // Required: Installation success callback
|
|
151
|
-
* onInstallationSuccess: async ({ installationId, repositories }, ctx) => {
|
|
152
|
-
* const userId = getUserId(ctx); // Your auth system
|
|
153
|
-
* return { userId, redirectUrl: '/dashboard' };
|
|
154
|
-
* },
|
|
154
|
+
* appSlug: 'my-app',
|
|
155
155
|
*
|
|
156
156
|
* // Optional: Handle webhook events
|
|
157
157
|
* onWebhookEvent: async ({ event, payload }, ctx) => {
|
|
@@ -235,50 +235,6 @@ export interface GitHubAppPluginOptions {
|
|
|
235
235
|
* @example "https://github.mycompany.com/api/v3"
|
|
236
236
|
*/
|
|
237
237
|
baseUrl?: string;
|
|
238
|
-
/**
|
|
239
|
-
* Callback invoked after successful installation
|
|
240
|
-
*
|
|
241
|
-
* REQUIRED. Called when user completes GitHub App installation.
|
|
242
|
-
* Return userId to link installation and optional redirectUrl.
|
|
243
|
-
*
|
|
244
|
-
* @param params - Installation data from GitHub
|
|
245
|
-
* @param ctx - Flink context with repos and plugins
|
|
246
|
-
* @returns Object with userId and optional redirectUrl
|
|
247
|
-
*
|
|
248
|
-
* @throws Should not throw - errors are caught and passed to onInstallationError
|
|
249
|
-
*
|
|
250
|
-
* @example
|
|
251
|
-
* ```typescript
|
|
252
|
-
* async ({ installationId, repositories, account }, ctx) => {
|
|
253
|
-
* const userId = ctx.auth?.tokenData?.userId || 'anonymous';
|
|
254
|
-
* return {
|
|
255
|
-
* userId,
|
|
256
|
-
* redirectUrl: '/dashboard/github'
|
|
257
|
-
* };
|
|
258
|
-
* }
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
onInstallationSuccess: (params: InstallationSuccessParams, ctx: any) => Promise<InstallationSuccessResponse>;
|
|
262
|
-
/**
|
|
263
|
-
* Callback invoked when installation fails (optional)
|
|
264
|
-
*
|
|
265
|
-
* Called when installation encounters an error (invalid state,
|
|
266
|
-
* expired session, GitHub API failure, etc.)
|
|
267
|
-
*
|
|
268
|
-
* @param params - Error information
|
|
269
|
-
* @returns Object with optional redirectUrl
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* ```typescript
|
|
273
|
-
* async ({ error, installationId }) => {
|
|
274
|
-
* console.error('Installation error:', error);
|
|
275
|
-
* return {
|
|
276
|
-
* redirectUrl: `/error?code=${error.code}`
|
|
277
|
-
* };
|
|
278
|
-
* }
|
|
279
|
-
* ```
|
|
280
|
-
*/
|
|
281
|
-
onInstallationError?: (params: InstallationErrorParams) => Promise<InstallationErrorResponse>;
|
|
282
238
|
/**
|
|
283
239
|
* Callback invoked when webhook event is received (optional)
|
|
284
240
|
*
|
|
@@ -351,8 +307,9 @@ export interface GitHubAppPluginOptions {
|
|
|
351
307
|
/**
|
|
352
308
|
* Register HTTP handlers automatically (optional)
|
|
353
309
|
*
|
|
354
|
-
* If true, registers
|
|
310
|
+
* If true, registers the webhook handler.
|
|
355
311
|
* Set to false if you want to implement custom handlers.
|
|
312
|
+
* Note: Installation callback handler must be implemented by the host app.
|
|
356
313
|
*
|
|
357
314
|
* @default true
|
|
358
315
|
*/
|
|
@@ -11,13 +11,7 @@
|
|
|
11
11
|
* Route: GET /github-app/callback?installation_id=...&setup_action=...&state=...
|
|
12
12
|
*/
|
|
13
13
|
import { RouteProps } from "@flink-app/flink";
|
|
14
|
-
|
|
15
|
-
* Context with GitHub App Plugin
|
|
16
|
-
*
|
|
17
|
-
* Note: Using 'any' for simplicity. In a real-world scenario, you'd want to properly
|
|
18
|
-
* type the context with both FlinkContext and GitHubAppPluginContext including the repos.
|
|
19
|
-
*/
|
|
20
|
-
type InstallationCallbackContext = any;
|
|
14
|
+
import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
|
|
21
15
|
/**
|
|
22
16
|
* Route configuration
|
|
23
17
|
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
@@ -30,12 +24,12 @@ export declare const Route: RouteProps;
|
|
|
30
24
|
* details, calling the app's callback, and storing the installation.
|
|
31
25
|
*/
|
|
32
26
|
declare const InstallationCallback: ({ ctx, req }: {
|
|
33
|
-
ctx:
|
|
27
|
+
ctx: GitHubAppInternalContext;
|
|
34
28
|
req: any;
|
|
35
29
|
}) => Promise<import("@flink-app/flink").FlinkResponse<undefined> | {
|
|
36
30
|
status: number;
|
|
37
31
|
headers: {
|
|
38
|
-
Location:
|
|
32
|
+
Location: string;
|
|
39
33
|
};
|
|
40
34
|
data: {};
|
|
41
35
|
}>;
|
|
@@ -10,15 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Route: POST /github-app/webhook
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
/**
|
|
16
|
-
* Context with GitHub App Plugin
|
|
17
|
-
*
|
|
18
|
-
* Note: Using 'any' for simplicity. In a real-world scenario, you'd want to properly
|
|
19
|
-
* type the context with both FlinkContext and GitHubAppPluginContext including the repos.
|
|
20
|
-
*/
|
|
21
|
-
type WebhookHandlerContext = FlinkContext<GitHubAppPluginContext>;
|
|
13
|
+
import { RouteProps } from "@flink-app/flink";
|
|
14
|
+
import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
|
|
22
15
|
/**
|
|
23
16
|
* Route configuration
|
|
24
17
|
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
@@ -30,7 +23,7 @@ export declare const Route: RouteProps;
|
|
|
30
23
|
* Validates webhook signatures and processes GitHub webhook events.
|
|
31
24
|
*/
|
|
32
25
|
declare const WebhookHandler: ({ ctx, req }: {
|
|
33
|
-
ctx:
|
|
26
|
+
ctx: GitHubAppInternalContext;
|
|
34
27
|
req: any;
|
|
35
28
|
}) => Promise<{
|
|
36
29
|
status: number;
|