@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,248 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub App Installation Callback Handler
4
+ *
5
+ * Handles the callback from GitHub after app installation by:
6
+ * 1. Validating the state parameter to prevent CSRF attacks
7
+ * 2. Fetching installation details from GitHub API
8
+ * 3. Calling the onInstallationSuccess callback to link installation to user
9
+ * 4. Storing the installation in the database
10
+ * 5. Redirecting to the application
11
+ *
12
+ * Route: GET /github-app/callback?installation_id=...&setup_action=...&state=...
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Route = void 0;
16
+ const flink_1 = require("@flink-app/flink");
17
+ const error_utils_1 = require("../utils/error-utils");
18
+ const state_utils_1 = require("../utils/state-utils");
19
+ /**
20
+ * Route configuration
21
+ * Registered programmatically by the plugin if registerRoutes is enabled
22
+ */
23
+ exports.Route = {
24
+ path: "/github-app/callback",
25
+ method: flink_1.HttpMethod.get,
26
+ };
27
+ /**
28
+ * GitHub App Installation Callback Handler
29
+ *
30
+ * Completes the installation flow by validating state, fetching installation
31
+ * details, calling the app's callback, and storing the installation.
32
+ */
33
+ const InstallationCallback = async ({ ctx, req }) => {
34
+ const { installation_id, setup_action, state, code } = req.query;
35
+ try {
36
+ // Validate required parameters
37
+ if (!installation_id || !setup_action || !state) {
38
+ return (0, flink_1.badRequest)("Missing required parameters: installation_id, setup_action, or state");
39
+ }
40
+ // Find installation session by state
41
+ const session = await ctx.repos.githubAppSessionRepo.getOne({ state });
42
+ if (!session) {
43
+ const error = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SESSION_EXPIRED, "Installation session not found or expired. Please try again.", {
44
+ state: state.substring(0, 10) + "...",
45
+ });
46
+ // Call onInstallationError callback if provided
47
+ const { options } = ctx.plugins.githubApp;
48
+ if (options.onInstallationError) {
49
+ const errorResult = await options.onInstallationError({ error, installationId: installation_id });
50
+ if (errorResult.redirectUrl) {
51
+ return {
52
+ status: 302,
53
+ headers: { Location: errorResult.redirectUrl },
54
+ data: {},
55
+ };
56
+ }
57
+ }
58
+ return (0, flink_1.badRequest)(error.message);
59
+ }
60
+ // Validate state parameter using constant-time comparison (CSRF protection)
61
+ if (!(0, state_utils_1.validateState)(state, session.state)) {
62
+ const error = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
63
+ providedState: state.substring(0, 10) + "...",
64
+ });
65
+ // Call onInstallationError callback if provided
66
+ const { options } = ctx.plugins.githubApp;
67
+ if (options.onInstallationError) {
68
+ const errorResult = await options.onInstallationError({ error, installationId: installation_id });
69
+ if (errorResult.redirectUrl) {
70
+ return {
71
+ status: 302,
72
+ headers: { Location: errorResult.redirectUrl },
73
+ data: {},
74
+ };
75
+ }
76
+ }
77
+ return (0, flink_1.badRequest)(error.message);
78
+ }
79
+ // Delete session immediately after validation (one-time use)
80
+ await ctx.repos.githubAppSessionRepo.deleteBySessionId(session.sessionId);
81
+ // Get plugin options and auth service
82
+ const { options, authService } = ctx.plugins.githubApp;
83
+ // Generate GitHub App JWT
84
+ const jwt = authService.generateAppJWT();
85
+ // Fetch installation details from GitHub API
86
+ const installationIdNum = parseInt(installation_id, 10);
87
+ const installationDetails = await fetchInstallationDetails(installationIdNum, jwt, options.baseUrl);
88
+ // Fetch repositories accessible by this installation
89
+ const repositories = await fetchInstallationRepositories(installationIdNum, jwt, options.baseUrl);
90
+ // Call onInstallationSuccess callback to get userId and redirect URL
91
+ let callbackResult;
92
+ try {
93
+ callbackResult = await options.onInstallationSuccess({
94
+ installationId: installationIdNum,
95
+ setupAction: setup_action,
96
+ account: installationDetails.account,
97
+ repositories: repositories,
98
+ permissions: installationDetails.permissions || {},
99
+ events: installationDetails.events || [],
100
+ }, ctx);
101
+ }
102
+ catch (error) {
103
+ flink_1.log.error("GitHub App onInstallationSuccess callback failed:", error);
104
+ const callbackError = (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, "Failed to complete installation. Please try again.", {
105
+ originalError: error.message,
106
+ });
107
+ // Call onInstallationError callback if provided
108
+ if (options.onInstallationError) {
109
+ const errorResult = await options.onInstallationError({
110
+ error: callbackError,
111
+ installationId: installation_id,
112
+ });
113
+ if (errorResult.redirectUrl) {
114
+ return {
115
+ status: 302,
116
+ headers: { Location: errorResult.redirectUrl },
117
+ data: {},
118
+ };
119
+ }
120
+ }
121
+ return (0, flink_1.internalServerError)("Installation failed. Please try again.");
122
+ }
123
+ // Extract userId and redirectUrl from callback result
124
+ const { userId, redirectUrl } = callbackResult;
125
+ if (!userId) {
126
+ return (0, flink_1.badRequest)("onInstallationSuccess callback must return userId");
127
+ }
128
+ // Store installation in database
129
+ await ctx.repos.githubInstallationRepo.create({
130
+ userId,
131
+ installationId: installationIdNum,
132
+ accountId: installationDetails.account.id,
133
+ accountLogin: installationDetails.account.login,
134
+ accountType: installationDetails.account.type,
135
+ avatarUrl: installationDetails.account.avatar_url,
136
+ repositories: repositories.map((repo) => ({
137
+ id: repo.id,
138
+ name: repo.name,
139
+ fullName: repo.full_name,
140
+ private: repo.private,
141
+ })),
142
+ permissions: installationDetails.permissions || {},
143
+ events: installationDetails.events || [],
144
+ createdAt: new Date(),
145
+ updatedAt: new Date(),
146
+ });
147
+ // Redirect to app using callback's redirectUrl or default to root
148
+ const finalRedirectUrl = redirectUrl || "/";
149
+ return {
150
+ status: 302,
151
+ headers: {
152
+ Location: finalRedirectUrl,
153
+ },
154
+ data: {},
155
+ };
156
+ }
157
+ catch (error) {
158
+ flink_1.log.error("GitHub App installation callback error:", error);
159
+ // Call onInstallationError callback if provided
160
+ const { options } = ctx.plugins.githubApp;
161
+ if (options.onInstallationError) {
162
+ try {
163
+ const errorResult = await options.onInstallationError({
164
+ error: error,
165
+ installationId: installation_id,
166
+ });
167
+ if (errorResult.redirectUrl) {
168
+ return {
169
+ status: 302,
170
+ headers: { Location: errorResult.redirectUrl },
171
+ data: {},
172
+ };
173
+ }
174
+ }
175
+ catch (callbackError) {
176
+ flink_1.log.error("onInstallationError callback failed:", callbackError);
177
+ }
178
+ }
179
+ return (0, flink_1.internalServerError)(error.message || "Installation callback failed. Please try again.");
180
+ }
181
+ };
182
+ /**
183
+ * Fetch installation details from GitHub API
184
+ *
185
+ * @param installationId - GitHub installation ID
186
+ * @param jwt - GitHub App JWT
187
+ * @param baseUrl - GitHub API base URL
188
+ * @returns Installation details
189
+ */
190
+ async function fetchInstallationDetails(installationId, jwt, baseUrl = "https://api.github.com") {
191
+ const url = `${baseUrl}/app/installations/${installationId}`;
192
+ const response = await fetch(url, {
193
+ method: "GET",
194
+ headers: {
195
+ Authorization: `Bearer ${jwt}`,
196
+ Accept: "application/vnd.github+json",
197
+ "User-Agent": "Flink-GitHub-App-Plugin",
198
+ },
199
+ });
200
+ if (!response.ok) {
201
+ const errorBody = await response.text();
202
+ throw new Error(`Failed to fetch installation details: ${response.statusText} - ${errorBody}`);
203
+ }
204
+ return await response.json();
205
+ }
206
+ /**
207
+ * Fetch repositories accessible by installation
208
+ *
209
+ * @param installationId - GitHub installation ID
210
+ * @param jwt - GitHub App JWT
211
+ * @param baseUrl - GitHub API base URL
212
+ * @returns Array of repositories
213
+ */
214
+ async function fetchInstallationRepositories(installationId, jwt, baseUrl = "https://api.github.com") {
215
+ const url = `${baseUrl}/installation/repositories`;
216
+ // First get an installation token
217
+ const tokenUrl = `${baseUrl}/app/installations/${installationId}/access_tokens`;
218
+ const tokenResponse = await fetch(tokenUrl, {
219
+ method: "POST",
220
+ headers: {
221
+ Authorization: `Bearer ${jwt}`,
222
+ Accept: "application/vnd.github+json",
223
+ "User-Agent": "Flink-GitHub-App-Plugin",
224
+ },
225
+ });
226
+ if (!tokenResponse.ok) {
227
+ const errorBody = await tokenResponse.text();
228
+ throw new Error(`Failed to get installation token: ${tokenResponse.statusText} - ${errorBody}`);
229
+ }
230
+ const tokenData = await tokenResponse.json();
231
+ const token = tokenData.token;
232
+ // Now fetch repositories with the installation token
233
+ const reposResponse = await fetch(url, {
234
+ method: "GET",
235
+ headers: {
236
+ Authorization: `Bearer ${token}`,
237
+ Accept: "application/vnd.github+json",
238
+ "User-Agent": "Flink-GitHub-App-Plugin",
239
+ },
240
+ });
241
+ if (!reposResponse.ok) {
242
+ const errorBody = await reposResponse.text();
243
+ throw new Error(`Failed to fetch repositories: ${reposResponse.statusText} - ${errorBody}`);
244
+ }
245
+ const data = await reposResponse.json();
246
+ return data.repositories || [];
247
+ }
248
+ exports.default = InstallationCallback;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * GitHub App Uninstall Handler
3
+ *
4
+ * Manually uninstalls a GitHub App installation by:
5
+ * 1. Extracting userId from request (app-defined authentication)
6
+ * 2. Verifying user owns the installation
7
+ * 3. Deleting installation from database
8
+ * 4. Clearing cached installation token
9
+ * 5. Returning 204 No Content
10
+ *
11
+ * Route: DELETE /github-app/installation/:installationId
12
+ *
13
+ * Note: This is an optional handler for manual uninstallation.
14
+ * Apps should also handle the 'installation.deleted' webhook event
15
+ * for automatic cleanup when users uninstall via GitHub UI.
16
+ */
17
+ import { Handler, RouteProps } from "@flink-app/flink";
18
+ /**
19
+ * Path parameters for the handler
20
+ */
21
+ interface PathParams {
22
+ installationId: string;
23
+ [key: string]: string;
24
+ }
25
+ /**
26
+ * Route configuration
27
+ * Registered programmatically by the plugin if registerRoutes is enabled
28
+ */
29
+ export declare const Route: RouteProps;
30
+ /**
31
+ * GitHub App Uninstall Handler
32
+ *
33
+ * Allows users to manually uninstall a GitHub App from their account.
34
+ * The application must provide userId extraction logic (via JWT, session, etc.)
35
+ */
36
+ declare const UninstallHandler: Handler<any, any, any, PathParams>;
37
+ export default UninstallHandler;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub App Uninstall Handler
4
+ *
5
+ * Manually uninstalls a GitHub App installation by:
6
+ * 1. Extracting userId from request (app-defined authentication)
7
+ * 2. Verifying user owns the installation
8
+ * 3. Deleting installation from database
9
+ * 4. Clearing cached installation token
10
+ * 5. Returning 204 No Content
11
+ *
12
+ * Route: DELETE /github-app/installation/:installationId
13
+ *
14
+ * Note: This is an optional handler for manual uninstallation.
15
+ * Apps should also handle the 'installation.deleted' webhook event
16
+ * for automatic cleanup when users uninstall via GitHub UI.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.Route = void 0;
20
+ const flink_1 = require("@flink-app/flink");
21
+ /**
22
+ * Route configuration
23
+ * Registered programmatically by the plugin if registerRoutes is enabled
24
+ */
25
+ exports.Route = {
26
+ path: "/github-app/installation/:installationId",
27
+ method: flink_1.HttpMethod.delete,
28
+ };
29
+ /**
30
+ * GitHub App Uninstall Handler
31
+ *
32
+ * Allows users to manually uninstall a GitHub App from their account.
33
+ * The application must provide userId extraction logic (via JWT, session, etc.)
34
+ */
35
+ const UninstallHandler = async ({ ctx, req }) => {
36
+ try {
37
+ const { installationId } = req.params;
38
+ const installationIdNum = parseInt(installationId, 10);
39
+ if (isNaN(installationIdNum)) {
40
+ return {
41
+ status: 400,
42
+ data: { error: "Invalid installation ID" },
43
+ };
44
+ }
45
+ // Extract userId from request
46
+ // This is app-defined and can work with any authentication system
47
+ // Examples:
48
+ // - JWT Auth Plugin: ctx.auth?.tokenData?.userId
49
+ // - Session-based: req.session?.userId
50
+ // - Custom header: req.headers['x-user-id']
51
+ //
52
+ // For now, we'll look for userId in multiple common places
53
+ const userId = extractUserId(req, ctx);
54
+ if (!userId) {
55
+ return {
56
+ status: 401,
57
+ data: { error: "Authentication required" },
58
+ };
59
+ }
60
+ // Find the installation
61
+ const installation = await ctx.repos.githubInstallationRepo.findByInstallationId(installationIdNum);
62
+ if (!installation) {
63
+ return {
64
+ status: 404,
65
+ data: { error: "Installation not found" },
66
+ };
67
+ }
68
+ // Verify user owns the installation
69
+ if (installation.userId !== userId) {
70
+ flink_1.log.warn("User attempted to delete installation they don't own", {
71
+ userId,
72
+ installationId: installationIdNum,
73
+ ownerId: installation.userId,
74
+ });
75
+ return {
76
+ status: 403,
77
+ data: { error: "You do not have permission to delete this installation" },
78
+ };
79
+ }
80
+ // Delete installation from database
81
+ const deletedCount = await ctx.repos.githubInstallationRepo.deleteByInstallationId(installationIdNum);
82
+ if (deletedCount === 0) {
83
+ flink_1.log.error("Failed to delete installation from database", {
84
+ installationId: installationIdNum,
85
+ });
86
+ return {
87
+ status: 500,
88
+ data: { error: "Failed to delete installation" },
89
+ };
90
+ }
91
+ // Clear cached installation token
92
+ ctx.plugins.githubApp.authService.deleteInstallationToken(installationIdNum);
93
+ flink_1.log.info("GitHub App installation deleted", {
94
+ userId,
95
+ installationId: installationIdNum,
96
+ });
97
+ // Return 204 No Content
98
+ return {
99
+ status: 204,
100
+ data: {},
101
+ };
102
+ }
103
+ catch (error) {
104
+ flink_1.log.error("Error deleting GitHub App installation", {
105
+ error: error.message,
106
+ });
107
+ return {
108
+ status: 500,
109
+ data: { error: "Failed to delete installation" },
110
+ };
111
+ }
112
+ };
113
+ /**
114
+ * Extract userId from request
115
+ *
116
+ * This helper function attempts to extract userId from various common
117
+ * authentication patterns. Applications can customize this logic based
118
+ * on their authentication system.
119
+ *
120
+ * @param req - Express request object
121
+ * @param ctx - Flink context
122
+ * @returns userId if found, undefined otherwise
123
+ */
124
+ function extractUserId(req, ctx) {
125
+ // Option 1: JWT Auth Plugin (if available)
126
+ if (ctx.auth?.tokenData?.userId) {
127
+ return ctx.auth.tokenData.userId;
128
+ }
129
+ // Option 2: Custom user property on request (set by custom middleware)
130
+ if (req.user?.id) {
131
+ return req.user.id;
132
+ }
133
+ if (req.user?.userId) {
134
+ return req.user.userId;
135
+ }
136
+ if (req.user?._id) {
137
+ return req.user._id;
138
+ }
139
+ // Option 3: Session-based authentication
140
+ if (req.session?.userId) {
141
+ return req.session.userId;
142
+ }
143
+ // Option 4: Custom header
144
+ if (req.headers["x-user-id"]) {
145
+ return req.headers["x-user-id"];
146
+ }
147
+ // Option 5: Query parameter (less secure, use with caution)
148
+ if (req.query?.user_id) {
149
+ return req.query.user_id;
150
+ }
151
+ return undefined;
152
+ }
153
+ exports.default = UninstallHandler;
@@ -0,0 +1,54 @@
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
+ import { FlinkContext, RouteProps } from "@flink-app/flink";
14
+ import { GitHubAppPluginContext } from "../GitHubAppPluginContext";
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>;
22
+ /**
23
+ * Route configuration
24
+ * Registered programmatically by the plugin if registerRoutes is enabled
25
+ */
26
+ export declare const Route: RouteProps;
27
+ /**
28
+ * GitHub App Webhook Handler
29
+ *
30
+ * Validates webhook signatures and processes GitHub webhook events.
31
+ */
32
+ declare const WebhookHandler: ({ ctx, req }: {
33
+ ctx: WebhookHandlerContext;
34
+ req: any;
35
+ }) => Promise<{
36
+ status: number;
37
+ data: {
38
+ error: string;
39
+ received?: undefined;
40
+ };
41
+ } | {
42
+ status: number;
43
+ data: {
44
+ received: boolean;
45
+ error?: undefined;
46
+ };
47
+ } | {
48
+ status: number;
49
+ data: {
50
+ received: boolean;
51
+ error: string;
52
+ };
53
+ }>;
54
+ export default WebhookHandler;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub App Webhook Handler
4
+ *
5
+ * Processes GitHub webhook events by:
6
+ * 1. Validating the webhook signature using HMAC-SHA256
7
+ * 2. Parsing the webhook payload
8
+ * 3. Optionally logging the event to the database
9
+ * 4. Calling the onWebhookEvent callback for processing
10
+ * 5. Returning 200 OK to GitHub
11
+ *
12
+ * Route: POST /github-app/webhook
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Route = void 0;
16
+ const flink_1 = require("@flink-app/flink");
17
+ /**
18
+ * Route configuration
19
+ * Registered programmatically by the plugin if registerRoutes is enabled
20
+ */
21
+ exports.Route = {
22
+ path: "/github-app/webhook",
23
+ method: flink_1.HttpMethod.post,
24
+ };
25
+ /**
26
+ * GitHub App Webhook Handler
27
+ *
28
+ * Validates webhook signatures and processes GitHub webhook events.
29
+ */
30
+ const WebhookHandler = async ({ ctx, req }) => {
31
+ try {
32
+ // Extract headers
33
+ const signature = req.headers["x-hub-signature-256"];
34
+ const event = req.headers["x-github-event"];
35
+ const deliveryId = req.headers["x-github-delivery"];
36
+ // Get raw request body
37
+ // In Express, if you use express.json(), the raw body is lost
38
+ // We need to access the raw body for signature validation
39
+ const rawBody = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
40
+ // Validate webhook signature using constant-time comparison
41
+ const { webhookValidator, options } = ctx.plugins.githubApp;
42
+ const isValid = webhookValidator.validateSignature(rawBody, signature);
43
+ if (!isValid) {
44
+ flink_1.log.warn("GitHub webhook signature validation failed", {
45
+ event,
46
+ deliveryId,
47
+ hasSignature: !!signature,
48
+ });
49
+ return {
50
+ status: 401,
51
+ data: { error: "Invalid webhook signature" },
52
+ };
53
+ }
54
+ // Parse webhook payload
55
+ let payload;
56
+ try {
57
+ payload = webhookValidator.parsePayload(rawBody);
58
+ }
59
+ catch (error) {
60
+ flink_1.log.error("Failed to parse webhook payload", {
61
+ event,
62
+ deliveryId,
63
+ error: error.message,
64
+ });
65
+ return {
66
+ status: 400,
67
+ data: { error: "Invalid webhook payload" },
68
+ };
69
+ }
70
+ // Extract event data
71
+ const action = payload.action;
72
+ const installationId = webhookValidator.extractInstallationId(payload);
73
+ flink_1.log.info("GitHub webhook received", {
74
+ event,
75
+ action,
76
+ installationId,
77
+ deliveryId,
78
+ });
79
+ // Optionally log webhook event to database
80
+ if (options.logWebhookEvents && ctx.repos.githubWebhookEventRepo) {
81
+ try {
82
+ await ctx.repos.githubWebhookEventRepo.create({
83
+ installationId: installationId || 0,
84
+ event,
85
+ action,
86
+ deliveryId,
87
+ payload,
88
+ processed: false,
89
+ createdAt: new Date(),
90
+ });
91
+ }
92
+ catch (error) {
93
+ // Don't fail the webhook if logging fails
94
+ flink_1.log.error("Failed to log webhook event to database", {
95
+ event,
96
+ deliveryId,
97
+ error: error.message,
98
+ });
99
+ }
100
+ }
101
+ // Call onWebhookEvent callback if provided
102
+ if (options.onWebhookEvent && installationId) {
103
+ try {
104
+ await options.onWebhookEvent({
105
+ event,
106
+ action,
107
+ payload,
108
+ installationId,
109
+ deliveryId,
110
+ }, ctx);
111
+ }
112
+ catch (error) {
113
+ // Log the error but still return 200 to GitHub to prevent retries
114
+ flink_1.log.error("Error in onWebhookEvent callback", {
115
+ event,
116
+ action,
117
+ installationId,
118
+ deliveryId,
119
+ error: error.message,
120
+ });
121
+ // Update webhook event log if enabled
122
+ if (options.logWebhookEvents && ctx.repos.githubWebhookEventRepo) {
123
+ try {
124
+ const webhookEvent = await ctx.repos.githubWebhookEventRepo.getOne({ deliveryId });
125
+ if (webhookEvent && webhookEvent._id) {
126
+ await ctx.repos.githubWebhookEventRepo.updateOne(webhookEvent._id, {
127
+ processed: true,
128
+ processedAt: new Date(),
129
+ error: error.message,
130
+ });
131
+ }
132
+ }
133
+ catch (updateError) {
134
+ // Ignore errors updating the log
135
+ }
136
+ }
137
+ }
138
+ }
139
+ // Return 200 OK to GitHub to acknowledge receipt
140
+ return {
141
+ status: 200,
142
+ data: { received: true },
143
+ };
144
+ }
145
+ catch (error) {
146
+ flink_1.log.error("Unexpected error in webhook handler", {
147
+ error: error.message,
148
+ });
149
+ // Still return 200 to GitHub to prevent infinite retries
150
+ // The error is logged for debugging
151
+ return {
152
+ status: 200,
153
+ data: { received: true, error: "Internal processing error" },
154
+ };
155
+ }
156
+ };
157
+ exports.default = WebhookHandler;
@@ -0,0 +1,19 @@
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
+ export { githubAppPlugin } from './GitHubAppPlugin';
11
+ export type { GitHubAppPluginOptions, InstallationSuccessParams, InstallationSuccessResponse, InstallationErrorParams, InstallationErrorResponse, WebhookEventParams } from './GitHubAppPluginOptions';
12
+ export type { GitHubAppPluginContext } from './GitHubAppPluginContext';
13
+ export type { default as GitHubInstallation } from './schemas/GitHubInstallation';
14
+ export type { default as GitHubAppSession } from './schemas/GitHubAppSession';
15
+ export type { default as WebhookEvent } from './schemas/WebhookEvent';
16
+ export type { default as WebhookPayload } from './schemas/WebhookPayload';
17
+ export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from './services/GitHubAPIClient';
18
+ export { GitHubAuthService } from './services/GitHubAuthService';
19
+ export { GitHubAppErrorCodes } from './utils/error-utils';