@flink-app/github-app-plugin 0.12.1-alpha.38
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 +209 -0
- package/LICENSE +21 -0
- package/README.md +667 -0
- package/SECURITY.md +498 -0
- package/dist/GitHubAppInternalContext.d.ts +44 -0
- package/dist/GitHubAppInternalContext.js +2 -0
- package/dist/GitHubAppPlugin.d.ts +45 -0
- package/dist/GitHubAppPlugin.js +367 -0
- package/dist/GitHubAppPluginContext.d.ts +242 -0
- package/dist/GitHubAppPluginContext.js +2 -0
- package/dist/GitHubAppPluginOptions.d.ts +369 -0
- package/dist/GitHubAppPluginOptions.js +2 -0
- package/dist/handlers/InitiateInstallation.d.ts +32 -0
- package/dist/handlers/InitiateInstallation.js +66 -0
- package/dist/handlers/InstallationCallback.d.ts +42 -0
- package/dist/handlers/InstallationCallback.js +248 -0
- package/dist/handlers/UninstallHandler.d.ts +37 -0
- package/dist/handlers/UninstallHandler.js +153 -0
- package/dist/handlers/WebhookHandler.d.ts +54 -0
- package/dist/handlers/WebhookHandler.js +157 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +23 -0
- package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
- package/dist/repos/GitHubAppSessionRepo.js +32 -0
- package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
- package/dist/repos/GitHubInstallationRepo.js +83 -0
- package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
- package/dist/repos/GitHubWebhookEventRepo.js +42 -0
- package/dist/schemas/GitHubAppSession.d.ts +13 -0
- package/dist/schemas/GitHubAppSession.js +2 -0
- package/dist/schemas/GitHubInstallation.d.ts +28 -0
- package/dist/schemas/GitHubInstallation.js +2 -0
- package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
- package/dist/schemas/InstallationCallbackRequest.js +2 -0
- package/dist/schemas/WebhookEvent.d.ts +16 -0
- package/dist/schemas/WebhookEvent.js +2 -0
- package/dist/schemas/WebhookPayload.d.ts +35 -0
- package/dist/schemas/WebhookPayload.js +2 -0
- package/dist/services/GitHubAPIClient.d.ts +143 -0
- package/dist/services/GitHubAPIClient.js +167 -0
- package/dist/services/GitHubAuthService.d.ts +85 -0
- package/dist/services/GitHubAuthService.js +160 -0
- package/dist/services/WebhookValidator.d.ts +93 -0
- package/dist/services/WebhookValidator.js +123 -0
- package/dist/utils/error-utils.d.ts +67 -0
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/jwt-utils.d.ts +35 -0
- package/dist/utils/jwt-utils.js +67 -0
- package/dist/utils/state-utils.d.ts +38 -0
- package/dist/utils/state-utils.js +74 -0
- package/dist/utils/token-cache-utils.d.ts +47 -0
- package/dist/utils/token-cache-utils.js +74 -0
- package/dist/utils/webhook-signature-utils.d.ts +22 -0
- package/dist/utils/webhook-signature-utils.js +57 -0
- package/examples/basic-installation.ts +246 -0
- package/examples/create-issue.ts +392 -0
- package/examples/error-handling.ts +396 -0
- package/examples/multi-event-webhook.ts +367 -0
- package/examples/organization-installation.ts +316 -0
- package/examples/repository-access.ts +480 -0
- package/examples/webhook-handling.ts +343 -0
- package/examples/with-jwt-auth.ts +319 -0
- package/package.json +41 -0
- package/spec/core-utilities.spec.ts +243 -0
- package/spec/handlers.spec.ts +216 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/integration-and-security.spec.ts +483 -0
- package/spec/plugin-core.spec.ts +258 -0
- package/spec/project-setup.spec.ts +56 -0
- package/spec/repos-and-schemas.spec.ts +288 -0
- package/spec/services.spec.ts +108 -0
- package/spec/support/jasmine.json +7 -0
- package/src/GitHubAppPlugin.ts +411 -0
- package/src/GitHubAppPluginContext.ts +254 -0
- package/src/GitHubAppPluginOptions.ts +412 -0
- package/src/handlers/InstallationCallback.ts +292 -0
- package/src/handlers/WebhookHandler.ts +179 -0
- package/src/index.ts +29 -0
- package/src/repos/GitHubAppSessionRepo.ts +36 -0
- package/src/repos/GitHubInstallationRepo.ts +95 -0
- package/src/repos/GitHubWebhookEventRepo.ts +48 -0
- package/src/schemas/GitHubAppSession.ts +13 -0
- package/src/schemas/GitHubInstallation.ts +28 -0
- package/src/schemas/InstallationCallbackRequest.ts +10 -0
- package/src/schemas/WebhookEvent.ts +16 -0
- package/src/schemas/WebhookPayload.ts +35 -0
- package/src/services/GitHubAPIClient.ts +244 -0
- package/src/services/GitHubAuthService.ts +188 -0
- package/src/services/WebhookValidator.ts +159 -0
- package/src/utils/error-utils.ts +148 -0
- package/src/utils/jwt-utils.ts +64 -0
- package/src/utils/state-utils.ts +72 -0
- package/src/utils/token-cache-utils.ts +89 -0
- package/src/utils/webhook-signature-utils.ts +57 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Handling Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Processing webhook events (push, pull request, installation)
|
|
6
|
+
* - Webhook signature validation (automatic)
|
|
7
|
+
* - Accessing webhook payload data
|
|
8
|
+
* - Using GitHub API client in webhook handlers
|
|
9
|
+
* - Responding to different event types
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FlinkApp, FlinkContext } from "@flink-app/flink";
|
|
13
|
+
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
14
|
+
|
|
15
|
+
interface AppContext extends FlinkContext {
|
|
16
|
+
plugins: {
|
|
17
|
+
githubApp: any;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function start() {
|
|
22
|
+
const app = new FlinkApp<AppContext>({
|
|
23
|
+
name: "GitHub App Webhook Example",
|
|
24
|
+
port: 3333,
|
|
25
|
+
|
|
26
|
+
db: {
|
|
27
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-webhook-example",
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
plugins: [
|
|
31
|
+
githubAppPlugin({
|
|
32
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
33
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
34
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
35
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
36
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
37
|
+
|
|
38
|
+
onInstallationSuccess: async ({ installationId, account }) => {
|
|
39
|
+
console.log(`Installation ${installationId} created for ${account.login}`);
|
|
40
|
+
return {
|
|
41
|
+
userId: "demo-user",
|
|
42
|
+
redirectUrl: "/dashboard",
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Webhook event handler
|
|
47
|
+
onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
|
|
48
|
+
console.log(`\n=== Webhook Received ===`);
|
|
49
|
+
console.log(`Event: ${event}`);
|
|
50
|
+
console.log(`Action: ${action || "N/A"}`);
|
|
51
|
+
console.log(`Installation ID: ${installationId}`);
|
|
52
|
+
console.log(`Delivery ID: ${deliveryId}`);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Get API client for this installation
|
|
56
|
+
const client = await ctx.plugins.githubApp.getClient(installationId);
|
|
57
|
+
|
|
58
|
+
// Handle different event types
|
|
59
|
+
switch (event) {
|
|
60
|
+
case "push":
|
|
61
|
+
await handlePushEvent(payload, client);
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case "pull_request":
|
|
65
|
+
await handlePullRequestEvent(payload, action, client);
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case "issues":
|
|
69
|
+
await handleIssuesEvent(payload, action, client);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case "installation":
|
|
73
|
+
await handleInstallationEvent(payload, action, ctx);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case "installation_repositories":
|
|
77
|
+
await handleRepositoriesEvent(payload, action, ctx);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case "pull_request_review":
|
|
81
|
+
await handlePullRequestReviewEvent(payload, action, client);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case "issue_comment":
|
|
85
|
+
await handleIssueCommentEvent(payload, action, client);
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
default:
|
|
89
|
+
console.log(`Unhandled event type: ${event}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`Webhook processed successfully`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`Error processing webhook:`, error);
|
|
95
|
+
// Don't throw - return 200 to GitHub to prevent retries
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Optional: Log webhook events to MongoDB for debugging
|
|
100
|
+
logWebhookEvents: true,
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await app.start();
|
|
106
|
+
|
|
107
|
+
console.log(`
|
|
108
|
+
=================================
|
|
109
|
+
Webhook Handling Example Started
|
|
110
|
+
=================================
|
|
111
|
+
|
|
112
|
+
Webhook endpoint: http://localhost:3333/github-app/webhook
|
|
113
|
+
|
|
114
|
+
For local development, use ngrok to create HTTPS tunnel:
|
|
115
|
+
ngrok http 3333
|
|
116
|
+
|
|
117
|
+
Then configure webhook URL in GitHub App settings:
|
|
118
|
+
https://your-ngrok-url.ngrok.io/github-app/webhook
|
|
119
|
+
|
|
120
|
+
Events will be logged to console when received.
|
|
121
|
+
|
|
122
|
+
=================================
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle push events
|
|
127
|
+
async function handlePushEvent(payload: any, client: any) {
|
|
128
|
+
const repo = payload.repository;
|
|
129
|
+
const pusher = payload.pusher;
|
|
130
|
+
const commits = payload.commits || [];
|
|
131
|
+
const branch = payload.ref.replace("refs/heads/", "");
|
|
132
|
+
|
|
133
|
+
console.log(`\n📦 Push Event:`);
|
|
134
|
+
console.log(` Repository: ${repo.full_name}`);
|
|
135
|
+
console.log(` Branch: ${branch}`);
|
|
136
|
+
console.log(` Pusher: ${pusher.name}`);
|
|
137
|
+
console.log(` Commits: ${commits.length}`);
|
|
138
|
+
|
|
139
|
+
// Log commit messages
|
|
140
|
+
commits.forEach((commit: any) => {
|
|
141
|
+
console.log(` - ${commit.message.split("\n")[0]} (${commit.id.substring(0, 7)})`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Example: Create issue if commit message contains "[bug]"
|
|
145
|
+
const hasBugCommit = commits.some((commit: any) => commit.message.toLowerCase().includes("[bug]"));
|
|
146
|
+
|
|
147
|
+
if (hasBugCommit) {
|
|
148
|
+
console.log(` 🐛 Bug commit detected, creating issue...`);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const issue = await client.createIssue(repo.owner.login, repo.name, {
|
|
152
|
+
title: `Bug fix needed in ${branch}`,
|
|
153
|
+
body: `A commit with [bug] tag was pushed to ${branch}.\n\nCommits:\n${commits
|
|
154
|
+
.map((c: any) => `- ${c.message.split("\n")[0]}`)
|
|
155
|
+
.join("\n")}`,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log(` ✅ Issue created: #${issue.number}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(` ❌ Failed to create issue:`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle pull request events
|
|
166
|
+
async function handlePullRequestEvent(payload: any, action: string | undefined, client: any) {
|
|
167
|
+
const pr = payload.pull_request;
|
|
168
|
+
const repo = payload.repository;
|
|
169
|
+
|
|
170
|
+
console.log(`\n🔀 Pull Request Event:`);
|
|
171
|
+
console.log(` Action: ${action}`);
|
|
172
|
+
console.log(` Repository: ${repo.full_name}`);
|
|
173
|
+
console.log(` PR: #${pr.number} - ${pr.title}`);
|
|
174
|
+
console.log(` Author: ${pr.user.login}`);
|
|
175
|
+
console.log(` Base: ${pr.base.ref} <- Head: ${pr.head.ref}`);
|
|
176
|
+
|
|
177
|
+
if (action === "opened") {
|
|
178
|
+
// Thank contributor for opening PR
|
|
179
|
+
console.log(` 💬 Welcoming new contributor...`);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
183
|
+
body: `Thanks for opening this pull request, @${pr.user.login}! We'll review it soon.`,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
console.log(` ✅ Comment added to PR`);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(` ❌ Failed to add comment:`, error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (action === "closed" && pr.merged) {
|
|
193
|
+
console.log(` ✅ PR merged!`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Handle issues events
|
|
198
|
+
async function handleIssuesEvent(payload: any, action: string | undefined, client: any) {
|
|
199
|
+
const issue = payload.issue;
|
|
200
|
+
const repo = payload.repository;
|
|
201
|
+
|
|
202
|
+
console.log(`\n🐛 Issue Event:`);
|
|
203
|
+
console.log(` Action: ${action}`);
|
|
204
|
+
console.log(` Repository: ${repo.full_name}`);
|
|
205
|
+
console.log(` Issue: #${issue.number} - ${issue.title}`);
|
|
206
|
+
console.log(` Author: ${issue.user.login}`);
|
|
207
|
+
|
|
208
|
+
if (action === "opened") {
|
|
209
|
+
// Auto-label issues based on content
|
|
210
|
+
const labels: string[] = [];
|
|
211
|
+
|
|
212
|
+
if (issue.title.toLowerCase().includes("bug")) {
|
|
213
|
+
labels.push("bug");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (issue.title.toLowerCase().includes("feature")) {
|
|
217
|
+
labels.push("enhancement");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (labels.length > 0) {
|
|
221
|
+
console.log(` 🏷️ Auto-labeling issue with: ${labels.join(", ")}`);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/labels`, {
|
|
225
|
+
labels,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
console.log(` ✅ Labels added`);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(` ❌ Failed to add labels:`, error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle installation events
|
|
237
|
+
async function handleInstallationEvent(payload: any, action: string | undefined, ctx: any) {
|
|
238
|
+
const installation = payload.installation;
|
|
239
|
+
const account = installation.account;
|
|
240
|
+
|
|
241
|
+
console.log(`\n⚙️ Installation Event:`);
|
|
242
|
+
console.log(` Action: ${action}`);
|
|
243
|
+
console.log(` Installation ID: ${installation.id}`);
|
|
244
|
+
console.log(` Account: ${account.login} (${account.type})`);
|
|
245
|
+
|
|
246
|
+
if (action === "created") {
|
|
247
|
+
console.log(` ✅ New installation created`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (action === "deleted") {
|
|
251
|
+
console.log(` ❌ Installation deleted`);
|
|
252
|
+
|
|
253
|
+
// Clean up installation data
|
|
254
|
+
const installations = await ctx.repos.githubInstallationRepo.findByInstallationId(installation.id);
|
|
255
|
+
|
|
256
|
+
if (installations.length > 0) {
|
|
257
|
+
const inst = installations[0];
|
|
258
|
+
console.log(` 🗑️ Cleaning up installation data for user ${inst.userId}`);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await ctx.plugins.githubApp.deleteInstallation(inst.userId, installation.id);
|
|
262
|
+
console.log(` ✅ Installation data cleaned up`);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(` ❌ Failed to clean up:`, error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (action === "suspend") {
|
|
270
|
+
console.log(` ⏸️ Installation suspended`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (action === "unsuspend") {
|
|
274
|
+
console.log(` ▶️ Installation unsuspended`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle installation repositories events
|
|
279
|
+
async function handleRepositoriesEvent(payload: any, action: string | undefined, ctx: any) {
|
|
280
|
+
const installation = payload.installation;
|
|
281
|
+
const addedRepos = payload.repositories_added || [];
|
|
282
|
+
const removedRepos = payload.repositories_removed || [];
|
|
283
|
+
|
|
284
|
+
console.log(`\n📚 Installation Repositories Event:`);
|
|
285
|
+
console.log(` Action: ${action}`);
|
|
286
|
+
console.log(` Installation ID: ${installation.id}`);
|
|
287
|
+
|
|
288
|
+
if (addedRepos.length > 0) {
|
|
289
|
+
console.log(` ➕ Added repositories:`);
|
|
290
|
+
addedRepos.forEach((repo: any) => {
|
|
291
|
+
console.log(` - ${repo.full_name}`);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (removedRepos.length > 0) {
|
|
296
|
+
console.log(` ➖ Removed repositories:`);
|
|
297
|
+
removedRepos.forEach((repo: any) => {
|
|
298
|
+
console.log(` - ${repo.full_name}`);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Handle pull request review events
|
|
304
|
+
async function handlePullRequestReviewEvent(payload: any, action: string | undefined, client: any) {
|
|
305
|
+
const review = payload.review;
|
|
306
|
+
const pr = payload.pull_request;
|
|
307
|
+
const repo = payload.repository;
|
|
308
|
+
|
|
309
|
+
console.log(`\n👀 Pull Request Review Event:`);
|
|
310
|
+
console.log(` Action: ${action}`);
|
|
311
|
+
console.log(` Repository: ${repo.full_name}`);
|
|
312
|
+
console.log(` PR: #${pr.number}`);
|
|
313
|
+
console.log(` Reviewer: ${review.user.login}`);
|
|
314
|
+
console.log(` State: ${review.state}`);
|
|
315
|
+
|
|
316
|
+
if (review.state === "approved") {
|
|
317
|
+
console.log(` ✅ PR approved!`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (review.state === "changes_requested") {
|
|
321
|
+
console.log(` 📝 Changes requested`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Handle issue comment events
|
|
326
|
+
async function handleIssueCommentEvent(payload: any, action: string | undefined, client: any) {
|
|
327
|
+
const comment = payload.comment;
|
|
328
|
+
const issue = payload.issue;
|
|
329
|
+
const repo = payload.repository;
|
|
330
|
+
|
|
331
|
+
console.log(`\n💬 Issue Comment Event:`);
|
|
332
|
+
console.log(` Action: ${action}`);
|
|
333
|
+
console.log(` Repository: ${repo.full_name}`);
|
|
334
|
+
console.log(` Issue: #${issue.number}`);
|
|
335
|
+
console.log(` Commenter: ${comment.user.login}`);
|
|
336
|
+
console.log(` Comment: ${comment.body.substring(0, 100)}...`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Handle errors
|
|
340
|
+
start().catch((error) => {
|
|
341
|
+
console.error("Failed to start application:", error);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App with JWT Auth Plugin Integration (Optional)
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Optional integration with JWT Auth Plugin
|
|
6
|
+
* - Using JWT-based authentication with GitHub App
|
|
7
|
+
* - Protecting endpoints with JWT authentication
|
|
8
|
+
* - Accessing GitHub App context with authenticated user
|
|
9
|
+
*
|
|
10
|
+
* Note: This integration is OPTIONAL. The GitHub App Plugin works standalone
|
|
11
|
+
* without JWT Auth Plugin.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { FlinkApp, FlinkContext, GetHandler, FlinkRepo } from "@flink-app/flink";
|
|
15
|
+
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
16
|
+
// Note: jwtAuthPlugin is optional - install separately if needed
|
|
17
|
+
// import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
|
|
18
|
+
|
|
19
|
+
interface User {
|
|
20
|
+
_id?: string;
|
|
21
|
+
email: string;
|
|
22
|
+
name: string;
|
|
23
|
+
roles: string[];
|
|
24
|
+
githubInstallations: number[];
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class UserRepo extends FlinkRepo<AppContext, User> {
|
|
29
|
+
async findByEmail(email: string) {
|
|
30
|
+
return this.getOne({ email });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AppContext extends FlinkContext {
|
|
35
|
+
repos: {
|
|
36
|
+
userRepo: UserRepo;
|
|
37
|
+
};
|
|
38
|
+
plugins: {
|
|
39
|
+
githubApp: any;
|
|
40
|
+
// jwtAuth would be available if JWT Auth Plugin is installed
|
|
41
|
+
jwtAuth?: any;
|
|
42
|
+
};
|
|
43
|
+
// JWT Auth Plugin adds this to context
|
|
44
|
+
auth?: {
|
|
45
|
+
tokenData?: {
|
|
46
|
+
userId: string;
|
|
47
|
+
email: string;
|
|
48
|
+
roles: string[];
|
|
49
|
+
};
|
|
50
|
+
user?: User;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function start() {
|
|
55
|
+
const app = new FlinkApp<AppContext>({
|
|
56
|
+
name: "GitHub App with JWT Auth Example",
|
|
57
|
+
port: 3333,
|
|
58
|
+
|
|
59
|
+
db: {
|
|
60
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-jwt-example",
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// OPTIONAL: Configure JWT Auth Plugin
|
|
64
|
+
// Uncomment if you want to use JWT authentication
|
|
65
|
+
/*
|
|
66
|
+
auth: jwtAuthPlugin({
|
|
67
|
+
secret: process.env.JWT_SECRET || "your-jwt-secret",
|
|
68
|
+
|
|
69
|
+
getUser: async (tokenData) => {
|
|
70
|
+
const user = await app.ctx.repos.userRepo.getById(tokenData.userId);
|
|
71
|
+
if (!user) {
|
|
72
|
+
throw new Error("User not found");
|
|
73
|
+
}
|
|
74
|
+
return user;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
rolePermissions: {
|
|
78
|
+
user: ["read:own", "write:own"],
|
|
79
|
+
admin: ["read:all", "write:all"],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
expiresIn: "7d",
|
|
83
|
+
}),
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
plugins: [
|
|
87
|
+
githubAppPlugin({
|
|
88
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
89
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
90
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
91
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
92
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
93
|
+
|
|
94
|
+
onInstallationSuccess: async ({ installationId, repositories, account }, ctx) => {
|
|
95
|
+
// If JWT Auth Plugin is available, get userId from JWT token
|
|
96
|
+
// Otherwise, get userId from your custom auth system
|
|
97
|
+
const userId = ctx.auth?.tokenData?.userId || ctx.session?.userId || "demo-user";
|
|
98
|
+
|
|
99
|
+
console.log(`\nInstallation successful for user: ${userId}`);
|
|
100
|
+
console.log(`Account: ${account.login}`);
|
|
101
|
+
console.log(`Repositories: ${repositories.length}`);
|
|
102
|
+
|
|
103
|
+
// Update user's GitHub installations
|
|
104
|
+
try {
|
|
105
|
+
const user = await ctx.repos.userRepo.getById(userId);
|
|
106
|
+
if (user) {
|
|
107
|
+
const installations = user.githubInstallations || [];
|
|
108
|
+
if (!installations.includes(installationId)) {
|
|
109
|
+
await ctx.repos.userRepo.updateOne(userId, {
|
|
110
|
+
githubInstallations: [...installations, installationId],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Failed to update user installations:", error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
userId,
|
|
120
|
+
redirectUrl: "/dashboard/github",
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await app.start();
|
|
128
|
+
|
|
129
|
+
console.log(`
|
|
130
|
+
=================================
|
|
131
|
+
JWT Auth Integration Example
|
|
132
|
+
=================================
|
|
133
|
+
|
|
134
|
+
This example shows OPTIONAL integration with JWT Auth Plugin.
|
|
135
|
+
|
|
136
|
+
The GitHub App Plugin works standalone without JWT Auth Plugin.
|
|
137
|
+
|
|
138
|
+
If JWT Auth Plugin is installed:
|
|
139
|
+
- Installation links to authenticated JWT user
|
|
140
|
+
- Endpoints can be protected with JWT authentication
|
|
141
|
+
- User context available via ctx.auth
|
|
142
|
+
|
|
143
|
+
If JWT Auth Plugin is NOT installed:
|
|
144
|
+
- Use your own authentication system
|
|
145
|
+
- Get userId from session, custom tokens, etc.
|
|
146
|
+
- Plugin works exactly the same
|
|
147
|
+
|
|
148
|
+
Available endpoints:
|
|
149
|
+
- GET /github/repos (protected if JWT Auth enabled)
|
|
150
|
+
- GET /github/installations (protected if JWT Auth enabled)
|
|
151
|
+
|
|
152
|
+
=================================
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handler: List user's GitHub repositories
|
|
157
|
+
// This handler works with or without JWT Auth Plugin
|
|
158
|
+
const GetUserGitHubRepos: GetHandler = async ({ ctx }) => {
|
|
159
|
+
// Get userId from JWT Auth context if available,
|
|
160
|
+
// otherwise from your custom auth system
|
|
161
|
+
const userId = ctx.auth?.tokenData?.userId || ctx.session?.userId || "demo-user";
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Get user's GitHub installation
|
|
165
|
+
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
166
|
+
|
|
167
|
+
if (!installation) {
|
|
168
|
+
return {
|
|
169
|
+
status: 404,
|
|
170
|
+
data: {
|
|
171
|
+
error: "github-app-not-installed",
|
|
172
|
+
message: "GitHub App is not installed",
|
|
173
|
+
hint: "Install the GitHub App to access your repositories",
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get API client
|
|
179
|
+
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
180
|
+
|
|
181
|
+
// Fetch repositories
|
|
182
|
+
const repos = await client.getRepositories();
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
status: 200,
|
|
186
|
+
data: {
|
|
187
|
+
user: {
|
|
188
|
+
id: userId,
|
|
189
|
+
// Include user data if JWT Auth is available
|
|
190
|
+
email: ctx.auth?.user?.email,
|
|
191
|
+
name: ctx.auth?.user?.name,
|
|
192
|
+
},
|
|
193
|
+
installation: {
|
|
194
|
+
id: installation.installationId,
|
|
195
|
+
account: installation.accountLogin,
|
|
196
|
+
type: installation.accountType,
|
|
197
|
+
},
|
|
198
|
+
repositories: repos.map((repo: any) => ({
|
|
199
|
+
id: repo.id,
|
|
200
|
+
name: repo.name,
|
|
201
|
+
fullName: repo.full_name,
|
|
202
|
+
private: repo.private,
|
|
203
|
+
url: repo.html_url,
|
|
204
|
+
})),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
console.error("Failed to list repositories:", error);
|
|
209
|
+
return {
|
|
210
|
+
status: 500,
|
|
211
|
+
data: {
|
|
212
|
+
error: "repository-access-failed",
|
|
213
|
+
message: error.message,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export { GetUserGitHubRepos };
|
|
220
|
+
|
|
221
|
+
// Handler: Get user's GitHub installations
|
|
222
|
+
const GetUserInstallations: GetHandler = async ({ ctx }) => {
|
|
223
|
+
const userId = ctx.auth?.tokenData?.userId || ctx.session?.userId || "demo-user";
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Get all installations for user
|
|
227
|
+
const installations = await ctx.plugins.githubApp.getInstallations(userId);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
status: 200,
|
|
231
|
+
data: {
|
|
232
|
+
user: {
|
|
233
|
+
id: userId,
|
|
234
|
+
email: ctx.auth?.user?.email,
|
|
235
|
+
},
|
|
236
|
+
installations: installations.map((inst) => ({
|
|
237
|
+
installationId: inst.installationId,
|
|
238
|
+
account: {
|
|
239
|
+
id: inst.accountId,
|
|
240
|
+
login: inst.accountLogin,
|
|
241
|
+
type: inst.accountType,
|
|
242
|
+
avatarUrl: inst.avatarUrl,
|
|
243
|
+
},
|
|
244
|
+
repositories: inst.repositories,
|
|
245
|
+
permissions: inst.permissions,
|
|
246
|
+
events: inst.events,
|
|
247
|
+
createdAt: inst.createdAt,
|
|
248
|
+
suspendedAt: inst.suspendedAt,
|
|
249
|
+
})),
|
|
250
|
+
total: installations.length,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
} catch (error: any) {
|
|
254
|
+
console.error("Failed to get installations:", error);
|
|
255
|
+
return {
|
|
256
|
+
status: 500,
|
|
257
|
+
data: {
|
|
258
|
+
error: "installations-access-failed",
|
|
259
|
+
message: error.message,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export { GetUserInstallations };
|
|
266
|
+
|
|
267
|
+
// Example: Protected handler (if JWT Auth Plugin is enabled)
|
|
268
|
+
// If JWT Auth is not enabled, this will work without authentication
|
|
269
|
+
const GetProtectedResource: GetHandler = async ({ ctx }) => {
|
|
270
|
+
// This check only applies if JWT Auth Plugin is installed
|
|
271
|
+
if (!ctx.auth?.tokenData) {
|
|
272
|
+
// If no JWT Auth Plugin, allow access anyway
|
|
273
|
+
// Or implement your own authentication check
|
|
274
|
+
console.log("No JWT authentication, allowing access");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const userId = ctx.auth?.tokenData?.userId || "anonymous";
|
|
278
|
+
|
|
279
|
+
// Check if user has GitHub App installed
|
|
280
|
+
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
status: 200,
|
|
284
|
+
data: {
|
|
285
|
+
message: "Protected resource accessed",
|
|
286
|
+
authenticated: !!ctx.auth?.tokenData,
|
|
287
|
+
userId,
|
|
288
|
+
hasGitHubApp: !!installation,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export { GetProtectedResource };
|
|
294
|
+
|
|
295
|
+
// Start application
|
|
296
|
+
start().catch((error) => {
|
|
297
|
+
console.error("Failed to start application:", error);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* SUMMARY:
|
|
303
|
+
*
|
|
304
|
+
* The GitHub App Plugin is STANDALONE and does NOT require JWT Auth Plugin.
|
|
305
|
+
*
|
|
306
|
+
* Benefits of using with JWT Auth Plugin:
|
|
307
|
+
* - Automatic user context from JWT tokens
|
|
308
|
+
* - Protected endpoints with JWT authentication
|
|
309
|
+
* - Consistent authentication across all features
|
|
310
|
+
* - User data available via ctx.auth
|
|
311
|
+
*
|
|
312
|
+
* Benefits of using standalone:
|
|
313
|
+
* - No dependency on specific auth system
|
|
314
|
+
* - Works with any authentication mechanism
|
|
315
|
+
* - Simpler setup for custom auth systems
|
|
316
|
+
* - More flexibility in user management
|
|
317
|
+
*
|
|
318
|
+
* Choose the approach that fits your application architecture.
|
|
319
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flink-app/github-app-plugin",
|
|
3
|
+
"version": "0.12.1-alpha.38",
|
|
4
|
+
"description": "Flink plugin for GitHub App integration with installation management and webhook handling",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
7
|
+
"test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=./spec/support/jasmine.json'",
|
|
8
|
+
"prepare": "tsc --project tsconfig.dist.json",
|
|
9
|
+
"build": "tsc --project tsconfig.dist.json",
|
|
10
|
+
"watch": "tsc-watch --project tsconfig.dist.json"
|
|
11
|
+
},
|
|
12
|
+
"author": "joel@frost.se",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"mongodb": "^6.15.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"jsonwebtoken": "^9.0.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@flink-app/flink": "^0.12.1-alpha.35",
|
|
27
|
+
"@flink-app/test-utils": "^0.12.1-alpha.38",
|
|
28
|
+
"@types/jasmine": "^3.7.1",
|
|
29
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
30
|
+
"@types/node": "22.13.10",
|
|
31
|
+
"jasmine": "^3.7.0",
|
|
32
|
+
"jasmine-spec-reporter": "^7.0.0",
|
|
33
|
+
"mongodb": "6.15.0",
|
|
34
|
+
"mongodb-memory-server": "^10.2.3",
|
|
35
|
+
"nodemon": "^2.0.7",
|
|
36
|
+
"ts-node": "^9.1.1",
|
|
37
|
+
"tsc-watch": "^4.2.9",
|
|
38
|
+
"typescript": "5.4.5"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "bba579788683fe0729399a56152eba4974f29bb6"
|
|
41
|
+
}
|