@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,292 @@
1
+ /**
2
+ * GitHub App Installation Callback Handler
3
+ *
4
+ * Handles the callback from GitHub after app installation by:
5
+ * 1. Validating the state parameter to prevent CSRF attacks
6
+ * 2. Fetching installation details from GitHub API
7
+ * 3. Calling the onInstallationSuccess callback to link installation to user
8
+ * 4. Storing the installation in the database
9
+ * 5. Redirecting to the application
10
+ *
11
+ * Route: GET /github-app/callback?installation_id=...&setup_action=...&state=...
12
+ */
13
+
14
+ import { badRequest, HttpMethod, internalServerError, log, RouteProps } from "@flink-app/flink";
15
+ import { createGitHubAppError, GitHubAppErrorCodes } from "../utils/error-utils";
16
+ import { validateState } from "../utils/state-utils";
17
+
18
+ /**
19
+ * Context with GitHub App Plugin
20
+ *
21
+ * Note: Using 'any' for simplicity. In a real-world scenario, you'd want to properly
22
+ * type the context with both FlinkContext and GitHubAppPluginContext including the repos.
23
+ */
24
+ type InstallationCallbackContext = any;
25
+
26
+ /**
27
+ * Route configuration
28
+ * Registered programmatically by the plugin if registerRoutes is enabled
29
+ */
30
+ export const Route: RouteProps = {
31
+ path: "/github-app/callback",
32
+ method: HttpMethod.get,
33
+ };
34
+
35
+ /**
36
+ * GitHub App Installation Callback Handler
37
+ *
38
+ * Completes the installation flow by validating state, fetching installation
39
+ * details, calling the app's callback, and storing the installation.
40
+ */
41
+ const InstallationCallback = async ({ ctx, req }: { ctx: InstallationCallbackContext; req: any }) => {
42
+ const { installation_id, setup_action, state, code } = req.query;
43
+
44
+ try {
45
+ // Validate required parameters
46
+ if (!installation_id || !setup_action || !state) {
47
+ return badRequest("Missing required parameters: installation_id, setup_action, or state");
48
+ }
49
+
50
+ // Find installation session by state
51
+ const session = await ctx.repos.githubAppSessionRepo.getOne({ state });
52
+
53
+ if (!session) {
54
+ const error = createGitHubAppError(GitHubAppErrorCodes.SESSION_EXPIRED, "Installation session not found or expired. Please try again.", {
55
+ state: state.substring(0, 10) + "...",
56
+ });
57
+
58
+ // Call onInstallationError callback if provided
59
+ const { options } = ctx.plugins.githubApp;
60
+ if (options.onInstallationError) {
61
+ const errorResult = await options.onInstallationError({ error, installationId: installation_id });
62
+ if (errorResult.redirectUrl) {
63
+ return {
64
+ status: 302,
65
+ headers: { Location: errorResult.redirectUrl },
66
+ data: {},
67
+ };
68
+ }
69
+ }
70
+
71
+ return badRequest(error.message);
72
+ }
73
+
74
+ // Validate state parameter using constant-time comparison (CSRF protection)
75
+ if (!validateState(state, session.state)) {
76
+ const error = createGitHubAppError(GitHubAppErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
77
+ providedState: state.substring(0, 10) + "...",
78
+ });
79
+
80
+ // Call onInstallationError callback if provided
81
+ const { options } = ctx.plugins.githubApp;
82
+ if (options.onInstallationError) {
83
+ const errorResult = await options.onInstallationError({ error, installationId: installation_id });
84
+ if (errorResult.redirectUrl) {
85
+ return {
86
+ status: 302,
87
+ headers: { Location: errorResult.redirectUrl },
88
+ data: {},
89
+ };
90
+ }
91
+ }
92
+
93
+ return badRequest(error.message);
94
+ }
95
+
96
+ // Delete session immediately after validation (one-time use)
97
+ await ctx.repos.githubAppSessionRepo.deleteBySessionId(session.sessionId);
98
+
99
+ // Get plugin options and auth service
100
+ const { options, authService } = ctx.plugins.githubApp;
101
+
102
+ // Generate GitHub App JWT
103
+ const jwt = authService.generateAppJWT();
104
+
105
+ // Fetch installation details from GitHub API
106
+ const installationIdNum = parseInt(installation_id, 10);
107
+ const installationDetails = await fetchInstallationDetails(installationIdNum, jwt, options.baseUrl);
108
+
109
+ // Fetch repositories accessible by this installation
110
+ const repositories = await fetchInstallationRepositories(installationIdNum, jwt, options.baseUrl);
111
+
112
+ // Call onInstallationSuccess callback to get userId and redirect URL
113
+ let callbackResult;
114
+ try {
115
+ callbackResult = await options.onInstallationSuccess(
116
+ {
117
+ installationId: installationIdNum,
118
+ setupAction: setup_action,
119
+ account: installationDetails.account,
120
+ repositories: repositories,
121
+ permissions: installationDetails.permissions || {},
122
+ events: installationDetails.events || [],
123
+ },
124
+ ctx
125
+ );
126
+ } catch (error: any) {
127
+ log.error("GitHub App onInstallationSuccess callback failed:", error);
128
+
129
+ const callbackError = createGitHubAppError(GitHubAppErrorCodes.SERVER_ERROR, "Failed to complete installation. Please try again.", {
130
+ originalError: error.message,
131
+ });
132
+
133
+ // Call onInstallationError callback if provided
134
+ if (options.onInstallationError) {
135
+ const errorResult = await options.onInstallationError({
136
+ error: callbackError,
137
+ installationId: installation_id,
138
+ });
139
+ if (errorResult.redirectUrl) {
140
+ return {
141
+ status: 302,
142
+ headers: { Location: errorResult.redirectUrl },
143
+ data: {},
144
+ };
145
+ }
146
+ }
147
+
148
+ return internalServerError("Installation failed. Please try again.");
149
+ }
150
+
151
+ // Extract userId and redirectUrl from callback result
152
+ const { userId, redirectUrl } = callbackResult;
153
+
154
+ if (!userId) {
155
+ return badRequest("onInstallationSuccess callback must return userId");
156
+ }
157
+
158
+ // Store installation in database
159
+ await ctx.repos.githubInstallationRepo.create({
160
+ userId,
161
+ installationId: installationIdNum,
162
+ accountId: installationDetails.account.id,
163
+ accountLogin: installationDetails.account.login,
164
+ accountType: installationDetails.account.type,
165
+ avatarUrl: installationDetails.account.avatar_url,
166
+ repositories: repositories.map((repo: any) => ({
167
+ id: repo.id,
168
+ name: repo.name,
169
+ fullName: repo.full_name,
170
+ private: repo.private,
171
+ })),
172
+ permissions: installationDetails.permissions || {},
173
+ events: installationDetails.events || [],
174
+ createdAt: new Date(),
175
+ updatedAt: new Date(),
176
+ });
177
+
178
+ // Redirect to app using callback's redirectUrl or default to root
179
+ const finalRedirectUrl = redirectUrl || "/";
180
+
181
+ return {
182
+ status: 302,
183
+ headers: {
184
+ Location: finalRedirectUrl,
185
+ },
186
+ data: {},
187
+ };
188
+ } catch (error: any) {
189
+ log.error("GitHub App installation callback error:", error);
190
+
191
+ // Call onInstallationError callback if provided
192
+ const { options } = ctx.plugins.githubApp;
193
+ if (options.onInstallationError) {
194
+ try {
195
+ const errorResult = await options.onInstallationError({
196
+ error: error,
197
+ installationId: installation_id,
198
+ });
199
+ if (errorResult.redirectUrl) {
200
+ return {
201
+ status: 302,
202
+ headers: { Location: errorResult.redirectUrl },
203
+ data: {},
204
+ };
205
+ }
206
+ } catch (callbackError) {
207
+ log.error("onInstallationError callback failed:", callbackError);
208
+ }
209
+ }
210
+
211
+ return internalServerError(error.message || "Installation callback failed. Please try again.");
212
+ }
213
+ };
214
+
215
+ /**
216
+ * Fetch installation details from GitHub API
217
+ *
218
+ * @param installationId - GitHub installation ID
219
+ * @param jwt - GitHub App JWT
220
+ * @param baseUrl - GitHub API base URL
221
+ * @returns Installation details
222
+ */
223
+ async function fetchInstallationDetails(installationId: number, jwt: string, baseUrl: string = "https://api.github.com"): Promise<any> {
224
+ const url = `${baseUrl}/app/installations/${installationId}`;
225
+
226
+ const response = await fetch(url, {
227
+ method: "GET",
228
+ headers: {
229
+ Authorization: `Bearer ${jwt}`,
230
+ Accept: "application/vnd.github+json",
231
+ "User-Agent": "Flink-GitHub-App-Plugin",
232
+ },
233
+ });
234
+
235
+ if (!response.ok) {
236
+ const errorBody = await response.text();
237
+ throw new Error(`Failed to fetch installation details: ${response.statusText} - ${errorBody}`);
238
+ }
239
+
240
+ return await response.json();
241
+ }
242
+
243
+ /**
244
+ * Fetch repositories accessible by installation
245
+ *
246
+ * @param installationId - GitHub installation ID
247
+ * @param jwt - GitHub App JWT
248
+ * @param baseUrl - GitHub API base URL
249
+ * @returns Array of repositories
250
+ */
251
+ async function fetchInstallationRepositories(installationId: number, jwt: string, baseUrl: string = "https://api.github.com"): Promise<any[]> {
252
+ const url = `${baseUrl}/installation/repositories`;
253
+
254
+ // First get an installation token
255
+ const tokenUrl = `${baseUrl}/app/installations/${installationId}/access_tokens`;
256
+ const tokenResponse = await fetch(tokenUrl, {
257
+ method: "POST",
258
+ headers: {
259
+ Authorization: `Bearer ${jwt}`,
260
+ Accept: "application/vnd.github+json",
261
+ "User-Agent": "Flink-GitHub-App-Plugin",
262
+ },
263
+ });
264
+
265
+ if (!tokenResponse.ok) {
266
+ const errorBody = await tokenResponse.text();
267
+ throw new Error(`Failed to get installation token: ${tokenResponse.statusText} - ${errorBody}`);
268
+ }
269
+
270
+ const tokenData: any = await tokenResponse.json();
271
+ const token = tokenData.token;
272
+
273
+ // Now fetch repositories with the installation token
274
+ const reposResponse = await fetch(url, {
275
+ method: "GET",
276
+ headers: {
277
+ Authorization: `Bearer ${token}`,
278
+ Accept: "application/vnd.github+json",
279
+ "User-Agent": "Flink-GitHub-App-Plugin",
280
+ },
281
+ });
282
+
283
+ if (!reposResponse.ok) {
284
+ const errorBody = await reposResponse.text();
285
+ throw new Error(`Failed to fetch repositories: ${reposResponse.statusText} - ${errorBody}`);
286
+ }
287
+
288
+ const data: any = await reposResponse.json();
289
+ return data.repositories || [];
290
+ }
291
+
292
+ export default InstallationCallback;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * GitHub App Webhook Handler
3
+ *
4
+ * Processes GitHub webhook events by:
5
+ * 1. Validating the webhook signature using HMAC-SHA256
6
+ * 2. Parsing the webhook payload
7
+ * 3. Optionally logging the event to the database
8
+ * 4. Calling the onWebhookEvent callback for processing
9
+ * 5. Returning 200 OK to GitHub
10
+ *
11
+ * Route: POST /github-app/webhook
12
+ */
13
+
14
+ import { FlinkContext, HttpMethod, RouteProps, log } from "@flink-app/flink";
15
+ import { GitHubAppPluginContext } from "../GitHubAppPluginContext";
16
+
17
+ /**
18
+ * Context with GitHub App Plugin
19
+ *
20
+ * Note: Using 'any' for simplicity. In a real-world scenario, you'd want to properly
21
+ * type the context with both FlinkContext and GitHubAppPluginContext including the repos.
22
+ */
23
+ type WebhookHandlerContext = FlinkContext<GitHubAppPluginContext>;
24
+
25
+ /**
26
+ * Route configuration
27
+ * Registered programmatically by the plugin if registerRoutes is enabled
28
+ */
29
+ export const Route: RouteProps = {
30
+ path: "/github-app/webhook",
31
+ method: HttpMethod.post,
32
+ };
33
+
34
+ /**
35
+ * GitHub App Webhook Handler
36
+ *
37
+ * Validates webhook signatures and processes GitHub webhook events.
38
+ */
39
+ const WebhookHandler = async ({ ctx, req }: { ctx: WebhookHandlerContext; req: any }) => {
40
+ try {
41
+ // Extract headers
42
+ const signature = req.headers["x-hub-signature-256"] as string;
43
+ const event = req.headers["x-github-event"] as string;
44
+ const deliveryId = req.headers["x-github-delivery"] as string;
45
+
46
+ // Get raw request body
47
+ // In Express, if you use express.json(), the raw body is lost
48
+ // We need to access the raw body for signature validation
49
+ const rawBody = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
50
+
51
+ // Validate webhook signature using constant-time comparison
52
+ const { webhookValidator, options } = ctx.plugins.githubApp;
53
+
54
+ const isValid = webhookValidator.validateSignature(rawBody, signature);
55
+
56
+ if (!isValid) {
57
+ log.warn("GitHub webhook signature validation failed", {
58
+ event,
59
+ deliveryId,
60
+ hasSignature: !!signature,
61
+ });
62
+
63
+ return {
64
+ status: 401,
65
+ data: { error: "Invalid webhook signature" },
66
+ };
67
+ }
68
+
69
+ // Parse webhook payload
70
+ let payload;
71
+ try {
72
+ payload = webhookValidator.parsePayload(rawBody);
73
+ } catch (error: any) {
74
+ log.error("Failed to parse webhook payload", {
75
+ event,
76
+ deliveryId,
77
+ error: error.message,
78
+ });
79
+
80
+ return {
81
+ status: 400,
82
+ data: { error: "Invalid webhook payload" },
83
+ };
84
+ }
85
+
86
+ // Extract event data
87
+ const action = payload.action;
88
+ const installationId = webhookValidator.extractInstallationId(payload);
89
+
90
+ log.info("GitHub webhook received", {
91
+ event,
92
+ action,
93
+ installationId,
94
+ deliveryId,
95
+ });
96
+
97
+ // Optionally log webhook event to database
98
+ if (options.logWebhookEvents && ctx.repos.githubWebhookEventRepo) {
99
+ try {
100
+ await ctx.repos.githubWebhookEventRepo.create({
101
+ installationId: installationId || 0,
102
+ event,
103
+ action,
104
+ deliveryId,
105
+ payload,
106
+ processed: false,
107
+ createdAt: new Date(),
108
+ });
109
+ } catch (error: any) {
110
+ // Don't fail the webhook if logging fails
111
+ log.error("Failed to log webhook event to database", {
112
+ event,
113
+ deliveryId,
114
+ error: error.message,
115
+ });
116
+ }
117
+ }
118
+
119
+ // Call onWebhookEvent callback if provided
120
+ if (options.onWebhookEvent && installationId) {
121
+ try {
122
+ await options.onWebhookEvent(
123
+ {
124
+ event,
125
+ action,
126
+ payload,
127
+ installationId,
128
+ deliveryId,
129
+ },
130
+ ctx
131
+ );
132
+ } catch (error: any) {
133
+ // Log the error but still return 200 to GitHub to prevent retries
134
+ log.error("Error in onWebhookEvent callback", {
135
+ event,
136
+ action,
137
+ installationId,
138
+ deliveryId,
139
+ error: error.message,
140
+ });
141
+
142
+ // Update webhook event log if enabled
143
+ if (options.logWebhookEvents && ctx.repos.githubWebhookEventRepo) {
144
+ try {
145
+ const webhookEvent = await ctx.repos.githubWebhookEventRepo.getOne({ deliveryId });
146
+ if (webhookEvent && webhookEvent._id) {
147
+ await ctx.repos.githubWebhookEventRepo.updateOne(webhookEvent._id, {
148
+ processed: true,
149
+ processedAt: new Date(),
150
+ error: error.message,
151
+ });
152
+ }
153
+ } catch (updateError) {
154
+ // Ignore errors updating the log
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // Return 200 OK to GitHub to acknowledge receipt
161
+ return {
162
+ status: 200,
163
+ data: { received: true },
164
+ };
165
+ } catch (error: any) {
166
+ log.error("Unexpected error in webhook handler", {
167
+ error: error.message,
168
+ });
169
+
170
+ // Still return 200 to GitHub to prevent infinite retries
171
+ // The error is logged for debugging
172
+ return {
173
+ status: 200,
174
+ data: { received: true, error: "Internal processing error" },
175
+ };
176
+ }
177
+ };
178
+
179
+ export default WebhookHandler;
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GitHub App Plugin for Flink Framework
3
+ *
4
+ * Provides GitHub App integration with:
5
+ * - Installation management
6
+ * - JWT-based authentication
7
+ * - Webhook handling with signature validation
8
+ * - API client wrapper
9
+ */
10
+
11
+ // Plugin factory
12
+ export { githubAppPlugin } from './GitHubAppPlugin';
13
+
14
+ // Type exports
15
+ export type { GitHubAppPluginOptions, InstallationSuccessParams, InstallationSuccessResponse, InstallationErrorParams, InstallationErrorResponse, WebhookEventParams } from './GitHubAppPluginOptions';
16
+ export type { GitHubAppPluginContext } from './GitHubAppPluginContext';
17
+
18
+ // Schema exports
19
+ export type { default as GitHubInstallation } from './schemas/GitHubInstallation';
20
+ export type { default as GitHubAppSession } from './schemas/GitHubAppSession';
21
+ export type { default as WebhookEvent } from './schemas/WebhookEvent';
22
+ export type { default as WebhookPayload } from './schemas/WebhookPayload';
23
+
24
+ // Service exports (for advanced usage)
25
+ export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from './services/GitHubAPIClient';
26
+ export { GitHubAuthService } from './services/GitHubAuthService';
27
+
28
+ // Error utilities
29
+ export { GitHubAppErrorCodes } from './utils/error-utils';
@@ -0,0 +1,36 @@
1
+ import { FlinkRepo } from "@flink-app/flink";
2
+ import { Db } from "mongodb";
3
+ import GitHubAppSession from "../schemas/GitHubAppSession";
4
+
5
+ /**
6
+ * Repository for managing temporary GitHub App installation sessions with TTL.
7
+ * Sessions are used during the installation flow to prevent CSRF attacks.
8
+ */
9
+ class GitHubAppSessionRepo extends FlinkRepo<any, GitHubAppSession> {
10
+ constructor(ctx: any, db: Db, collectionName: string = "github_app_sessions") {
11
+ super(collectionName, db);
12
+ this.ctx = ctx;
13
+ }
14
+
15
+ /**
16
+ * Find a GitHub App session by its unique session ID.
17
+ * @param sessionId - The unique session identifier
18
+ * @returns The session if found, null otherwise
19
+ */
20
+ async findBySessionId(sessionId: string): Promise<GitHubAppSession | null> {
21
+ return this.getOne({ sessionId });
22
+ }
23
+
24
+ /**
25
+ * Delete a GitHub App session by its unique session ID.
26
+ * Used after the installation callback is processed to prevent session reuse.
27
+ * @param sessionId - The unique session identifier
28
+ * @returns The number of deleted sessions (0 or 1)
29
+ */
30
+ async deleteBySessionId(sessionId: string): Promise<number> {
31
+ const { deletedCount } = await this.collection.deleteOne({ sessionId });
32
+ return deletedCount || 0;
33
+ }
34
+ }
35
+
36
+ export default GitHubAppSessionRepo;
@@ -0,0 +1,95 @@
1
+ import { FlinkRepo } from "@flink-app/flink";
2
+ import { Db } from "mongodb";
3
+ import GitHubInstallation from "../schemas/GitHubInstallation";
4
+
5
+ /**
6
+ * Repository for managing GitHub App installations.
7
+ * Stores installation-to-user mappings and repository access information.
8
+ */
9
+ class GitHubInstallationRepo extends FlinkRepo<any, GitHubInstallation> {
10
+ constructor(ctx: any, db: Db, collectionName: string = "github_installations") {
11
+ super(collectionName, db);
12
+ this.ctx = ctx;
13
+ }
14
+
15
+ /**
16
+ * Find an installation by user ID and installation ID.
17
+ * @param userId - The application user ID
18
+ * @param installationId - The GitHub installation ID
19
+ * @returns The installation if found, null otherwise
20
+ */
21
+ async findByUserAndInstallationId(userId: string, installationId: number): Promise<GitHubInstallation | null> {
22
+ return this.getOne({ userId, installationId });
23
+ }
24
+
25
+ /**
26
+ * Find all installations for a specific user.
27
+ * @param userId - The application user ID
28
+ * @returns Array of installations for the user
29
+ */
30
+ async findByUserId(userId: string): Promise<GitHubInstallation[]> {
31
+ return this.findAll({ userId });
32
+ }
33
+
34
+ /**
35
+ * Find an installation by its GitHub installation ID.
36
+ * @param installationId - The GitHub installation ID
37
+ * @returns The installation if found, null otherwise
38
+ */
39
+ async findByInstallationId(installationId: number): Promise<GitHubInstallation | null> {
40
+ return this.getOne({ installationId });
41
+ }
42
+
43
+ /**
44
+ * Update the repositories list for an installation.
45
+ * @param installationId - The GitHub installation ID
46
+ * @param repositories - The new repositories list
47
+ * @returns The updated installation
48
+ */
49
+ async updateRepositories(installationId: number, repositories: GitHubInstallation["repositories"]): Promise<GitHubInstallation | null> {
50
+ const installation = await this.getOne({ installationId });
51
+ if (!installation) {
52
+ return null;
53
+ }
54
+
55
+ const updated = await this.updateOne(installation._id!, {
56
+ repositories,
57
+ updatedAt: new Date(),
58
+ });
59
+
60
+ return updated;
61
+ }
62
+
63
+ /**
64
+ * Suspend an installation.
65
+ * @param installationId - The GitHub installation ID
66
+ * @param suspendedBy - Information about who suspended the installation
67
+ * @returns The updated installation
68
+ */
69
+ async suspend(installationId: number, suspendedBy: { id: number; login: string }): Promise<GitHubInstallation | null> {
70
+ const installation = await this.getOne({ installationId });
71
+ if (!installation) {
72
+ return null;
73
+ }
74
+
75
+ const updated = await this.updateOne(installation._id!, {
76
+ suspendedAt: new Date(),
77
+ suspendedBy,
78
+ updatedAt: new Date(),
79
+ });
80
+
81
+ return updated;
82
+ }
83
+
84
+ /**
85
+ * Delete an installation by its GitHub installation ID.
86
+ * @param installationId - The GitHub installation ID
87
+ * @returns The number of deleted installations (0 or 1)
88
+ */
89
+ async deleteByInstallationId(installationId: number): Promise<number> {
90
+ const { deletedCount } = await this.collection.deleteOne({ installationId });
91
+ return deletedCount || 0;
92
+ }
93
+ }
94
+
95
+ export default GitHubInstallationRepo;
@@ -0,0 +1,48 @@
1
+ import { FlinkRepo } from "@flink-app/flink";
2
+ import { Db } from "mongodb";
3
+ import WebhookEvent from "../schemas/WebhookEvent";
4
+
5
+ /**
6
+ * Repository for managing GitHub webhook events.
7
+ * Only used if logWebhookEvents config option is enabled.
8
+ * Provides webhook event logging for debugging and auditing.
9
+ */
10
+ class GitHubWebhookEventRepo extends FlinkRepo<any, WebhookEvent> {
11
+ constructor(ctx: any, db: Db, collectionName: string = "github_webhook_events") {
12
+ super(collectionName, db);
13
+ this.ctx = ctx;
14
+ }
15
+
16
+ /**
17
+ * Find all unprocessed webhook events.
18
+ * @returns Array of unprocessed events
19
+ */
20
+ async findUnprocessed(): Promise<WebhookEvent[]> {
21
+ return this.findAll({ processed: false });
22
+ }
23
+
24
+ /**
25
+ * Mark a webhook event as processed.
26
+ * @param eventId - The webhook event ID
27
+ * @returns The updated event
28
+ */
29
+ async markProcessed(eventId: string): Promise<WebhookEvent | null> {
30
+ const updated = await this.updateOne(eventId, {
31
+ processed: true,
32
+ processedAt: new Date(),
33
+ });
34
+
35
+ return updated;
36
+ }
37
+
38
+ /**
39
+ * Find all webhook events for a specific installation.
40
+ * @param installationId - The GitHub installation ID
41
+ * @returns Array of webhook events for the installation
42
+ */
43
+ async findByInstallationId(installationId: number): Promise<WebhookEvent[]> {
44
+ return this.findAll({ installationId });
45
+ }
46
+ }
47
+
48
+ export default GitHubWebhookEventRepo;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Temporary session storage for GitHub App installation flow.
3
+ * Used for CSRF protection via state parameter.
4
+ * Sessions automatically expire after TTL (default 10 minutes).
5
+ */
6
+ export default interface GitHubAppSession {
7
+ _id?: string;
8
+ sessionId: string;
9
+ state: string;
10
+ userId?: string;
11
+ metadata?: Record<string, any>;
12
+ createdAt: Date;
13
+ }