@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,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;
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|