@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/LICENSE +21 -0
  3. package/README.md +667 -0
  4. package/SECURITY.md +498 -0
  5. package/dist/GitHubAppInternalContext.d.ts +44 -0
  6. package/dist/GitHubAppInternalContext.js +2 -0
  7. package/dist/GitHubAppPlugin.d.ts +45 -0
  8. package/dist/GitHubAppPlugin.js +367 -0
  9. package/dist/GitHubAppPluginContext.d.ts +242 -0
  10. package/dist/GitHubAppPluginContext.js +2 -0
  11. package/dist/GitHubAppPluginOptions.d.ts +369 -0
  12. package/dist/GitHubAppPluginOptions.js +2 -0
  13. package/dist/handlers/InitiateInstallation.d.ts +32 -0
  14. package/dist/handlers/InitiateInstallation.js +66 -0
  15. package/dist/handlers/InstallationCallback.d.ts +42 -0
  16. package/dist/handlers/InstallationCallback.js +248 -0
  17. package/dist/handlers/UninstallHandler.d.ts +37 -0
  18. package/dist/handlers/UninstallHandler.js +153 -0
  19. package/dist/handlers/WebhookHandler.d.ts +54 -0
  20. package/dist/handlers/WebhookHandler.js +157 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.js +23 -0
  23. package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
  24. package/dist/repos/GitHubAppSessionRepo.js +32 -0
  25. package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
  26. package/dist/repos/GitHubInstallationRepo.js +83 -0
  27. package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
  28. package/dist/repos/GitHubWebhookEventRepo.js +42 -0
  29. package/dist/schemas/GitHubAppSession.d.ts +13 -0
  30. package/dist/schemas/GitHubAppSession.js +2 -0
  31. package/dist/schemas/GitHubInstallation.d.ts +28 -0
  32. package/dist/schemas/GitHubInstallation.js +2 -0
  33. package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
  34. package/dist/schemas/InstallationCallbackRequest.js +2 -0
  35. package/dist/schemas/WebhookEvent.d.ts +16 -0
  36. package/dist/schemas/WebhookEvent.js +2 -0
  37. package/dist/schemas/WebhookPayload.d.ts +35 -0
  38. package/dist/schemas/WebhookPayload.js +2 -0
  39. package/dist/services/GitHubAPIClient.d.ts +143 -0
  40. package/dist/services/GitHubAPIClient.js +167 -0
  41. package/dist/services/GitHubAuthService.d.ts +85 -0
  42. package/dist/services/GitHubAuthService.js +160 -0
  43. package/dist/services/WebhookValidator.d.ts +93 -0
  44. package/dist/services/WebhookValidator.js +123 -0
  45. package/dist/utils/error-utils.d.ts +67 -0
  46. package/dist/utils/error-utils.js +121 -0
  47. package/dist/utils/jwt-utils.d.ts +35 -0
  48. package/dist/utils/jwt-utils.js +67 -0
  49. package/dist/utils/state-utils.d.ts +38 -0
  50. package/dist/utils/state-utils.js +74 -0
  51. package/dist/utils/token-cache-utils.d.ts +47 -0
  52. package/dist/utils/token-cache-utils.js +74 -0
  53. package/dist/utils/webhook-signature-utils.d.ts +22 -0
  54. package/dist/utils/webhook-signature-utils.js +57 -0
  55. package/examples/basic-installation.ts +246 -0
  56. package/examples/create-issue.ts +392 -0
  57. package/examples/error-handling.ts +396 -0
  58. package/examples/multi-event-webhook.ts +367 -0
  59. package/examples/organization-installation.ts +316 -0
  60. package/examples/repository-access.ts +480 -0
  61. package/examples/webhook-handling.ts +343 -0
  62. package/examples/with-jwt-auth.ts +319 -0
  63. package/package.json +41 -0
  64. package/spec/core-utilities.spec.ts +243 -0
  65. package/spec/handlers.spec.ts +216 -0
  66. package/spec/helpers/reporter.ts +41 -0
  67. package/spec/integration-and-security.spec.ts +483 -0
  68. package/spec/plugin-core.spec.ts +258 -0
  69. package/spec/project-setup.spec.ts +56 -0
  70. package/spec/repos-and-schemas.spec.ts +288 -0
  71. package/spec/services.spec.ts +108 -0
  72. package/spec/support/jasmine.json +7 -0
  73. package/src/GitHubAppPlugin.ts +411 -0
  74. package/src/GitHubAppPluginContext.ts +254 -0
  75. package/src/GitHubAppPluginOptions.ts +412 -0
  76. package/src/handlers/InstallationCallback.ts +292 -0
  77. package/src/handlers/WebhookHandler.ts +179 -0
  78. package/src/index.ts +29 -0
  79. package/src/repos/GitHubAppSessionRepo.ts +36 -0
  80. package/src/repos/GitHubInstallationRepo.ts +95 -0
  81. package/src/repos/GitHubWebhookEventRepo.ts +48 -0
  82. package/src/schemas/GitHubAppSession.ts +13 -0
  83. package/src/schemas/GitHubInstallation.ts +28 -0
  84. package/src/schemas/InstallationCallbackRequest.ts +10 -0
  85. package/src/schemas/WebhookEvent.ts +16 -0
  86. package/src/schemas/WebhookPayload.ts +35 -0
  87. package/src/services/GitHubAPIClient.ts +244 -0
  88. package/src/services/GitHubAuthService.ts +188 -0
  89. package/src/services/WebhookValidator.ts +159 -0
  90. package/src/utils/error-utils.ts +148 -0
  91. package/src/utils/jwt-utils.ts +64 -0
  92. package/src/utils/state-utils.ts +72 -0
  93. package/src/utils/token-cache-utils.ts +89 -0
  94. package/src/utils/webhook-signature-utils.ts +57 -0
  95. package/tsconfig.dist.json +4 -0
  96. package/tsconfig.json +24 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Multi-Event Webhook Handling Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Handling multiple webhook event types
6
+ * - Event filtering and routing
7
+ * - Webhook event logging
8
+ * - Async webhook processing
9
+ * - Event-driven workflows
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
+ repos: {
20
+ githubWebhookEventRepo: any;
21
+ };
22
+ }
23
+
24
+ // Event processor interface
25
+ interface EventProcessor {
26
+ event: string;
27
+ actions?: string[];
28
+ process: (payload: any, client: any, ctx: AppContext) => Promise<void>;
29
+ }
30
+
31
+ // Event processors registry
32
+ const eventProcessors: EventProcessor[] = [
33
+ {
34
+ event: "push",
35
+ process: async (payload, client, ctx) => {
36
+ const repo = payload.repository;
37
+ const branch = payload.ref.replace("refs/heads/", "");
38
+ const commits = payload.commits || [];
39
+
40
+ console.log(`\n[PUSH] ${repo.full_name}:${branch} - ${commits.length} commits`);
41
+
42
+ // Auto-create issue for commits with [bug] tag
43
+ for (const commit of commits) {
44
+ if (commit.message.toLowerCase().includes("[bug]")) {
45
+ await client.createIssue(repo.owner.login, repo.name, {
46
+ title: `Bug fix needed: ${commit.message.split("\n")[0]}`,
47
+ body: `Commit ${commit.id.substring(0, 7)} contains a [bug] tag.\n\nMessage: ${commit.message}`,
48
+ });
49
+ console.log(` Created issue for bug commit`);
50
+ }
51
+ }
52
+ },
53
+ },
54
+ {
55
+ event: "pull_request",
56
+ actions: ["opened", "closed", "reopened", "synchronize"],
57
+ process: async (payload, client, ctx) => {
58
+ const pr = payload.pull_request;
59
+ const action = payload.action;
60
+ const repo = payload.repository;
61
+
62
+ console.log(`\n[PR] ${repo.full_name}#${pr.number} - ${action}`);
63
+
64
+ if (action === "opened") {
65
+ // Welcome new contributors
66
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
67
+ body: `Thanks for your pull request, @${pr.user.login}! We'll review it soon.`,
68
+ });
69
+ console.log(` Added welcome comment`);
70
+ }
71
+
72
+ if (action === "closed" && pr.merged) {
73
+ // Thank for merged PR
74
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
75
+ body: `Thanks for your contribution, @${pr.user.login}! Your changes have been merged.`,
76
+ });
77
+ console.log(` Added thank you comment`);
78
+ }
79
+ },
80
+ },
81
+ {
82
+ event: "issues",
83
+ actions: ["opened", "edited", "closed"],
84
+ process: async (payload, client, ctx) => {
85
+ const issue = payload.issue;
86
+ const action = payload.action;
87
+ const repo = payload.repository;
88
+
89
+ console.log(`\n[ISSUE] ${repo.full_name}#${issue.number} - ${action}`);
90
+
91
+ if (action === "opened") {
92
+ // Auto-label based on title
93
+ const labels: string[] = [];
94
+
95
+ if (issue.title.toLowerCase().includes("bug")) labels.push("bug");
96
+ if (issue.title.toLowerCase().includes("feature")) labels.push("enhancement");
97
+ if (issue.title.toLowerCase().includes("question")) labels.push("question");
98
+ if (issue.title.toLowerCase().includes("urgent")) labels.push("priority: high");
99
+
100
+ if (labels.length > 0) {
101
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/labels`, {
102
+ labels,
103
+ });
104
+ console.log(` Added labels: ${labels.join(", ")}`);
105
+ }
106
+ }
107
+ },
108
+ },
109
+ {
110
+ event: "issue_comment",
111
+ actions: ["created"],
112
+ process: async (payload, client, ctx) => {
113
+ const comment = payload.comment;
114
+ const issue = payload.issue;
115
+ const repo = payload.repository;
116
+
117
+ console.log(`\n[COMMENT] ${repo.full_name}#${issue.number}`);
118
+
119
+ // Check for commands in comments (e.g., "/label bug")
120
+ const commandMatch = comment.body.match(/^\/(\w+)\s*(.*)$/m);
121
+
122
+ if (commandMatch) {
123
+ const [, command, args] = commandMatch;
124
+
125
+ console.log(` Command detected: /${command} ${args}`);
126
+
127
+ switch (command) {
128
+ case "label":
129
+ const labels = args.split(/\s+/).filter(Boolean);
130
+ if (labels.length > 0) {
131
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/labels`, {
132
+ labels,
133
+ });
134
+ console.log(` Added labels: ${labels.join(", ")}`);
135
+ }
136
+ break;
137
+
138
+ case "close":
139
+ await client.request("PATCH", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}`, {
140
+ state: "closed",
141
+ });
142
+ console.log(` Closed issue`);
143
+ break;
144
+
145
+ case "assign":
146
+ const assignees = args.split(/\s+/).filter(Boolean);
147
+ if (assignees.length > 0) {
148
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/assignees`, {
149
+ assignees,
150
+ });
151
+ console.log(` Assigned: ${assignees.join(", ")}`);
152
+ }
153
+ break;
154
+ }
155
+ }
156
+ },
157
+ },
158
+ {
159
+ event: "pull_request_review",
160
+ actions: ["submitted"],
161
+ process: async (payload, client, ctx) => {
162
+ const review = payload.review;
163
+ const pr = payload.pull_request;
164
+ const repo = payload.repository;
165
+
166
+ console.log(`\n[PR REVIEW] ${repo.full_name}#${pr.number} - ${review.state}`);
167
+
168
+ if (review.state === "approved") {
169
+ // Thank reviewer
170
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
171
+ body: `Thanks for the review, @${review.user.login}!`,
172
+ });
173
+ console.log(` Thanked reviewer`);
174
+ }
175
+
176
+ if (review.state === "changes_requested") {
177
+ // Notify author
178
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
179
+ body: `@${pr.user.login}, please address the requested changes.`,
180
+ });
181
+ console.log(` Notified author of requested changes`);
182
+ }
183
+ },
184
+ },
185
+ {
186
+ event: "release",
187
+ actions: ["published"],
188
+ process: async (payload, client, ctx) => {
189
+ const release = payload.release;
190
+ const repo = payload.repository;
191
+
192
+ console.log(`\n[RELEASE] ${repo.full_name} - ${release.tag_name}`);
193
+
194
+ // Create announcement issue
195
+ await client.createIssue(repo.owner.login, repo.name, {
196
+ title: `Release ${release.tag_name} is now available`,
197
+ body: `A new release has been published!\n\n**${release.name}**\n\n${release.body}\n\n[View Release](${release.html_url})`,
198
+ });
199
+ console.log(` Created announcement issue`);
200
+ },
201
+ },
202
+ {
203
+ event: "installation",
204
+ actions: ["created", "deleted", "suspend", "unsuspend"],
205
+ process: async (payload, client, ctx) => {
206
+ const installation = payload.installation;
207
+ const action = payload.action;
208
+
209
+ console.log(`\n[INSTALLATION] ${installation.account.login} - ${action}`);
210
+
211
+ if (action === "deleted") {
212
+ // Clean up installation data
213
+ const installations = await ctx.repos.githubInstallationRepo.findByInstallationId(installation.id);
214
+
215
+ for (const inst of installations) {
216
+ await ctx.plugins.githubApp.deleteInstallation(inst.userId, installation.id);
217
+ console.log(` Cleaned up installation for user ${inst.userId}`);
218
+ }
219
+ }
220
+ },
221
+ },
222
+ {
223
+ event: "installation_repositories",
224
+ actions: ["added", "removed"],
225
+ process: async (payload, client, ctx) => {
226
+ const installation = payload.installation;
227
+ const added = payload.repositories_added || [];
228
+ const removed = payload.repositories_removed || [];
229
+
230
+ console.log(`\n[INSTALLATION REPOS] ${installation.account.login}`);
231
+
232
+ if (added.length > 0) {
233
+ console.log(` Added: ${added.map((r: any) => r.full_name).join(", ")}`);
234
+ }
235
+
236
+ if (removed.length > 0) {
237
+ console.log(` Removed: ${removed.map((r: any) => r.full_name).join(", ")}`);
238
+ }
239
+
240
+ // Update installation in database
241
+ // (Plugin handles this automatically, but you can add custom logic)
242
+ },
243
+ },
244
+ {
245
+ event: "check_suite",
246
+ actions: ["completed"],
247
+ process: async (payload, client, ctx) => {
248
+ const checkSuite = payload.check_suite;
249
+ const repo = payload.repository;
250
+
251
+ console.log(`\n[CHECK SUITE] ${repo.full_name} - ${checkSuite.conclusion}`);
252
+
253
+ if (checkSuite.conclusion === "failure") {
254
+ // Create issue for failed checks
255
+ const prs = checkSuite.pull_requests || [];
256
+ if (prs.length > 0) {
257
+ const pr = prs[0];
258
+ await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
259
+ body: `Check suite failed. Please review the errors and fix them.`,
260
+ });
261
+ console.log(` Notified PR author of failed checks`);
262
+ }
263
+ }
264
+ },
265
+ },
266
+ ];
267
+
268
+ async function start() {
269
+ const app = new FlinkApp<AppContext>({
270
+ name: "GitHub App Multi-Event Webhook Example",
271
+ port: 3333,
272
+
273
+ db: {
274
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-multi-webhook",
275
+ },
276
+
277
+ plugins: [
278
+ githubAppPlugin({
279
+ appId: process.env.GITHUB_APP_ID!,
280
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
281
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
282
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
283
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
284
+
285
+ onInstallationSuccess: async ({ installationId, account }) => {
286
+ console.log(`Installation: ${installationId} on ${account.login}`);
287
+ return {
288
+ userId: "demo-user",
289
+ redirectUrl: "/dashboard",
290
+ };
291
+ },
292
+
293
+ // Multi-event webhook handler
294
+ onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
295
+ console.log(`\n========================================`);
296
+ console.log(`Webhook Delivery: ${deliveryId}`);
297
+ console.log(`Event: ${event}${action ? ` (${action})` : ""}`);
298
+ console.log(`Installation: ${installationId}`);
299
+ console.log(`========================================`);
300
+
301
+ try {
302
+ // Find matching processor
303
+ const processor = eventProcessors.find((p) => {
304
+ if (p.event !== event) return false;
305
+ if (p.actions && action && !p.actions.includes(action)) return false;
306
+ return true;
307
+ });
308
+
309
+ if (processor) {
310
+ // Get API client
311
+ const client = await ctx.plugins.githubApp.getClient(installationId);
312
+
313
+ // Process event
314
+ await processor.process(payload, client, ctx);
315
+
316
+ console.log(`\n✅ Event processed successfully`);
317
+ } else {
318
+ console.log(`\n⚠️ No processor registered for ${event}${action ? ` (${action})` : ""}`);
319
+ }
320
+ } catch (error: any) {
321
+ console.error(`\n❌ Error processing webhook:`, error.message);
322
+ // Don't throw - return 200 to GitHub to prevent retries
323
+ }
324
+
325
+ console.log(`========================================\n`);
326
+ },
327
+
328
+ // Enable webhook event logging
329
+ logWebhookEvents: true,
330
+ }),
331
+ ],
332
+ });
333
+
334
+ await app.start();
335
+
336
+ console.log(`
337
+ =================================
338
+ Multi-Event Webhook Example
339
+ =================================
340
+
341
+ This example handles multiple webhook event types:
342
+
343
+ ✓ push - Create issues for [bug] commits
344
+ ✓ pull_request - Welcome contributors, thank for merges
345
+ ✓ issues - Auto-label based on title
346
+ ✓ issue_comment - Process commands (/label, /close, /assign)
347
+ ✓ pull_request_review - Thank reviewers, notify authors
348
+ ✓ release - Create announcement issues
349
+ ✓ installation - Clean up on deletion
350
+ ✓ installation_repositories - Track repo changes
351
+ ✓ check_suite - Notify on failed checks
352
+
353
+ Webhook endpoint: /github-app/webhook
354
+
355
+ Configure in GitHub App settings:
356
+ - Webhook URL: https://your-domain.com/github-app/webhook
357
+ - Subscribe to events you want to handle
358
+
359
+ =================================
360
+ `);
361
+ }
362
+
363
+ // Start application
364
+ start().catch((error) => {
365
+ console.error("Failed to start application:", error);
366
+ process.exit(1);
367
+ });
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Organization-Level Installation Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Installing GitHub App at organization level
6
+ * - Handling organization installations vs user installations
7
+ * - Managing multiple installations per user (personal + orgs)
8
+ * - Switching between organization contexts
9
+ */
10
+
11
+ import { FlinkApp, FlinkContext, GetHandler } from "@flink-app/flink";
12
+ import { githubAppPlugin } from "@flink-app/github-app-plugin";
13
+
14
+ interface AppContext extends FlinkContext {
15
+ plugins: {
16
+ githubApp: any;
17
+ };
18
+ session?: {
19
+ userId?: string;
20
+ };
21
+ }
22
+
23
+ async function start() {
24
+ const app = new FlinkApp<AppContext>({
25
+ name: "GitHub App Organization Example",
26
+ port: 3333,
27
+
28
+ db: {
29
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-org-example",
30
+ },
31
+
32
+ plugins: [
33
+ githubAppPlugin({
34
+ appId: process.env.GITHUB_APP_ID!,
35
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
36
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
37
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
38
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
39
+
40
+ onInstallationSuccess: async ({ installationId, account, repositories }, ctx) => {
41
+ const userId = ctx.session?.userId || "demo-user";
42
+
43
+ console.log(`\n=== Installation Success ===`);
44
+ console.log(`Installation ID: ${installationId}`);
45
+ console.log(`Account Type: ${account.type}`);
46
+ console.log(`Account: ${account.login}`);
47
+
48
+ if (account.type === "Organization") {
49
+ console.log(`\nOrganization installation detected!`);
50
+ console.log(`Repositories in org: ${repositories.length}`);
51
+ } else {
52
+ console.log(`\nPersonal account installation`);
53
+ }
54
+
55
+ return {
56
+ userId,
57
+ redirectUrl: `/dashboard/installations?account=${account.login}`,
58
+ };
59
+ },
60
+ }),
61
+ ],
62
+ });
63
+
64
+ await app.start();
65
+
66
+ console.log(`
67
+ =================================
68
+ Organization Installation Example
69
+ =================================
70
+
71
+ Users can install the GitHub App on:
72
+ 1. Their personal account
73
+ 2. Organizations they belong to
74
+
75
+ Install on personal account:
76
+ http://localhost:3333/github-app/install?user_id=demo-user
77
+
78
+ Install on organization:
79
+ - Navigate to the install URL above
80
+ - Select "Organization" instead of personal account
81
+ - Choose which organization
82
+ - Select repositories
83
+
84
+ Available endpoints:
85
+ - GET /installations/list - List all installations (personal + orgs)
86
+ - GET /installations/:installationId - Get specific installation details
87
+ - GET /installations/:installationId/repos - List repos for installation
88
+
89
+ =================================
90
+ `);
91
+ }
92
+
93
+ // Handler: List all installations for user
94
+ const ListAllInstallations: GetHandler = async ({ ctx }) => {
95
+ const userId = ctx.session?.userId || "demo-user";
96
+
97
+ try {
98
+ const installations = await ctx.plugins.githubApp.getInstallations(userId);
99
+
100
+ // Group by type
101
+ const personal = installations.filter((inst: any) => inst.accountType === "User");
102
+ const organizations = installations.filter((inst: any) => inst.accountType === "Organization");
103
+
104
+ return {
105
+ status: 200,
106
+ data: {
107
+ total: installations.length,
108
+ personal: personal.map((inst: any) => ({
109
+ installationId: inst.installationId,
110
+ account: inst.accountLogin,
111
+ repositoryCount: inst.repositories.length,
112
+ createdAt: inst.createdAt,
113
+ })),
114
+ organizations: organizations.map((inst: any) => ({
115
+ installationId: inst.installationId,
116
+ organization: inst.accountLogin,
117
+ repositoryCount: inst.repositories.length,
118
+ permissions: inst.permissions,
119
+ createdAt: inst.createdAt,
120
+ })),
121
+ },
122
+ };
123
+ } catch (error: any) {
124
+ return {
125
+ status: 500,
126
+ data: {
127
+ error: "failed-to-list-installations",
128
+ message: error.message,
129
+ },
130
+ };
131
+ }
132
+ };
133
+
134
+ export { ListAllInstallations };
135
+
136
+ // Handler: Get installation details
137
+ const GetInstallationDetails: GetHandler<any, any, { installationId: string }> = async ({ ctx, params }) => {
138
+ const userId = ctx.session?.userId || "demo-user";
139
+ const installationId = parseInt(params.installationId, 10);
140
+
141
+ try {
142
+ const installations = await ctx.plugins.githubApp.getInstallations(userId);
143
+ const installation = installations.find((inst: any) => inst.installationId === installationId);
144
+
145
+ if (!installation) {
146
+ return {
147
+ status: 404,
148
+ data: {
149
+ error: "installation-not-found",
150
+ message: "Installation not found or you do not have access",
151
+ },
152
+ };
153
+ }
154
+
155
+ return {
156
+ status: 200,
157
+ data: {
158
+ installationId: installation.installationId,
159
+ account: {
160
+ id: installation.accountId,
161
+ login: installation.accountLogin,
162
+ type: installation.accountType,
163
+ avatarUrl: installation.avatarUrl,
164
+ },
165
+ repositories: installation.repositories.map((repo: any) => ({
166
+ id: repo.id,
167
+ name: repo.name,
168
+ fullName: repo.fullName,
169
+ private: repo.private,
170
+ })),
171
+ permissions: installation.permissions,
172
+ events: installation.events,
173
+ createdAt: installation.createdAt,
174
+ updatedAt: installation.updatedAt,
175
+ suspended: !!installation.suspendedAt,
176
+ suspendedAt: installation.suspendedAt,
177
+ suspendedBy: installation.suspendedBy,
178
+ },
179
+ };
180
+ } catch (error: any) {
181
+ return {
182
+ status: 500,
183
+ data: {
184
+ error: "failed-to-get-installation",
185
+ message: error.message,
186
+ },
187
+ };
188
+ }
189
+ };
190
+
191
+ export { GetInstallationDetails };
192
+
193
+ // Handler: List repositories for specific installation
194
+ const ListInstallationRepos: GetHandler<any, any, { installationId: string }> = async ({ ctx, params }) => {
195
+ const userId = ctx.session?.userId || "demo-user";
196
+ const installationId = parseInt(params.installationId, 10);
197
+
198
+ try {
199
+ // Verify user owns this installation
200
+ const installations = await ctx.plugins.githubApp.getInstallations(userId);
201
+ const installation = installations.find((inst: any) => inst.installationId === installationId);
202
+
203
+ if (!installation) {
204
+ return {
205
+ status: 403,
206
+ data: {
207
+ error: "access-denied",
208
+ message: "You do not have access to this installation",
209
+ },
210
+ };
211
+ }
212
+
213
+ // Get API client for this installation
214
+ const client = await ctx.plugins.githubApp.getClient(installationId);
215
+
216
+ // Fetch repositories from GitHub
217
+ const repos = await client.getRepositories();
218
+
219
+ return {
220
+ status: 200,
221
+ data: {
222
+ installation: {
223
+ id: installationId,
224
+ account: installation.accountLogin,
225
+ type: installation.accountType,
226
+ },
227
+ repositories: repos.map((repo: any) => ({
228
+ id: repo.id,
229
+ name: repo.name,
230
+ fullName: repo.full_name,
231
+ private: repo.private,
232
+ description: repo.description,
233
+ url: repo.html_url,
234
+ language: repo.language,
235
+ stargazersCount: repo.stargazers_count,
236
+ defaultBranch: repo.default_branch,
237
+ })),
238
+ total: repos.length,
239
+ },
240
+ };
241
+ } catch (error: any) {
242
+ return {
243
+ status: 500,
244
+ data: {
245
+ error: "failed-to-list-repos",
246
+ message: error.message,
247
+ },
248
+ };
249
+ }
250
+ };
251
+
252
+ export { ListInstallationRepos };
253
+
254
+ // Handler: Switch organization context
255
+ const SwitchOrganization: GetHandler<any, any, any, { org: string }> = async ({ ctx, query }) => {
256
+ const userId = ctx.session?.userId || "demo-user";
257
+ const orgLogin = query.org;
258
+
259
+ if (!orgLogin) {
260
+ return {
261
+ status: 400,
262
+ data: {
263
+ error: "missing-org",
264
+ message: "org query parameter is required",
265
+ },
266
+ };
267
+ }
268
+
269
+ try {
270
+ const installations = await ctx.plugins.githubApp.getInstallations(userId);
271
+ const orgInstallation = installations.find(
272
+ (inst: any) => inst.accountType === "Organization" && inst.accountLogin === orgLogin
273
+ );
274
+
275
+ if (!orgInstallation) {
276
+ return {
277
+ status: 404,
278
+ data: {
279
+ error: "org-not-found",
280
+ message: `No installation found for organization ${orgLogin}`,
281
+ availableOrgs: installations
282
+ .filter((inst: any) => inst.accountType === "Organization")
283
+ .map((inst: any) => inst.accountLogin),
284
+ },
285
+ };
286
+ }
287
+
288
+ return {
289
+ status: 200,
290
+ data: {
291
+ message: `Switched to organization: ${orgLogin}`,
292
+ installation: {
293
+ installationId: orgInstallation.installationId,
294
+ organization: orgInstallation.accountLogin,
295
+ repositories: orgInstallation.repositories.length,
296
+ },
297
+ },
298
+ };
299
+ } catch (error: any) {
300
+ return {
301
+ status: 500,
302
+ data: {
303
+ error: "switch-org-failed",
304
+ message: error.message,
305
+ },
306
+ };
307
+ }
308
+ };
309
+
310
+ export { SwitchOrganization };
311
+
312
+ // Start application
313
+ start().catch((error) => {
314
+ console.error("Failed to start application:", error);
315
+ process.exit(1);
316
+ });