@flink-app/github-app-plugin 0.12.1-alpha.44 → 0.12.1-alpha.47
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/package.json +4 -4
- package/dist/handlers/InitiateInstallation.d.ts +0 -32
- package/dist/handlers/InitiateInstallation.js +0 -66
- package/dist/handlers/InstallationCallback.d.ts +0 -36
- package/dist/handlers/InstallationCallback.js +0 -248
- package/dist/handlers/UninstallHandler.d.ts +0 -37
- package/dist/handlers/UninstallHandler.js +0 -153
- package/dist/schemas/InstallationCallbackRequest.d.ts +0 -10
- package/dist/schemas/InstallationCallbackRequest.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/github-app-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.47",
|
|
4
4
|
"description": "Flink plugin for GitHub App integration with installation management and webhook handling",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"jsonwebtoken": "^9.0.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@flink-app/flink": "^0.12.1-alpha.
|
|
27
|
-
"@flink-app/test-utils": "^0.12.1-alpha.
|
|
26
|
+
"@flink-app/flink": "^0.12.1-alpha.47",
|
|
27
|
+
"@flink-app/test-utils": "^0.12.1-alpha.47",
|
|
28
28
|
"@types/jasmine": "^3.7.1",
|
|
29
29
|
"@types/jsonwebtoken": "^9.0.5",
|
|
30
30
|
"@types/node": "22.13.10",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"tsc-watch": "^4.2.9",
|
|
38
38
|
"typescript": "5.4.5"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "a98a0af7f11e4a97f68da4d0d67677df7d2a2749"
|
|
41
41
|
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub App Installation Initiation Handler
|
|
3
|
-
*
|
|
4
|
-
* Initiates the GitHub App installation flow by:
|
|
5
|
-
* 1. Generating a cryptographically secure state parameter for CSRF protection
|
|
6
|
-
* 2. Creating an installation session to track the flow
|
|
7
|
-
* 3. Building GitHub's installation URL with state parameter
|
|
8
|
-
* 4. Redirecting the user to GitHub for app installation
|
|
9
|
-
*
|
|
10
|
-
* Route: GET /github-app/install?user_id={optional_user_id}
|
|
11
|
-
*/
|
|
12
|
-
import { GetHandler, RouteProps } from "@flink-app/flink";
|
|
13
|
-
/**
|
|
14
|
-
* Query parameters for the handler
|
|
15
|
-
*/
|
|
16
|
-
interface QueryParams {
|
|
17
|
-
user_id?: string;
|
|
18
|
-
[key: string]: any;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Route configuration
|
|
22
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
23
|
-
*/
|
|
24
|
-
export declare const Route: RouteProps;
|
|
25
|
-
/**
|
|
26
|
-
* GitHub App Installation Initiation Handler
|
|
27
|
-
*
|
|
28
|
-
* Starts the installation flow by generating CSRF state, creating a session,
|
|
29
|
-
* and redirecting to GitHub's app installation page.
|
|
30
|
-
*/
|
|
31
|
-
declare const InitiateInstallation: GetHandler<any, any, any, QueryParams>;
|
|
32
|
-
export default InitiateInstallation;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* GitHub App Installation Initiation Handler
|
|
4
|
-
*
|
|
5
|
-
* Initiates the GitHub App installation flow by:
|
|
6
|
-
* 1. Generating a cryptographically secure state parameter for CSRF protection
|
|
7
|
-
* 2. Creating an installation session to track the flow
|
|
8
|
-
* 3. Building GitHub's installation URL with state parameter
|
|
9
|
-
* 4. Redirecting the user to GitHub for app installation
|
|
10
|
-
*
|
|
11
|
-
* Route: GET /github-app/install?user_id={optional_user_id}
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.Route = void 0;
|
|
15
|
-
const flink_1 = require("@flink-app/flink");
|
|
16
|
-
const state_utils_1 = require("../utils/state-utils");
|
|
17
|
-
/**
|
|
18
|
-
* Route configuration
|
|
19
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
20
|
-
*/
|
|
21
|
-
exports.Route = {
|
|
22
|
-
path: "/github-app/install",
|
|
23
|
-
method: flink_1.HttpMethod.get,
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* GitHub App Installation Initiation Handler
|
|
27
|
-
*
|
|
28
|
-
* Starts the installation flow by generating CSRF state, creating a session,
|
|
29
|
-
* and redirecting to GitHub's app installation page.
|
|
30
|
-
*/
|
|
31
|
-
const InitiateInstallation = async ({ ctx, req }) => {
|
|
32
|
-
const { user_id } = req.query;
|
|
33
|
-
try {
|
|
34
|
-
// Get plugin options
|
|
35
|
-
const { options } = ctx.plugins.githubApp;
|
|
36
|
-
// Validate that appSlug is configured
|
|
37
|
-
if (!options.appSlug) {
|
|
38
|
-
return (0, flink_1.badRequest)("GitHub App slug is not configured. Please set appSlug in plugin options.");
|
|
39
|
-
}
|
|
40
|
-
// Generate cryptographically secure state and session ID
|
|
41
|
-
const state = (0, state_utils_1.generateState)();
|
|
42
|
-
const sessionId = (0, state_utils_1.generateSessionId)();
|
|
43
|
-
// Store session for state validation in callback
|
|
44
|
-
await ctx.repos.githubAppSessionRepo.create({
|
|
45
|
-
sessionId,
|
|
46
|
-
state,
|
|
47
|
-
userId: user_id,
|
|
48
|
-
createdAt: new Date(),
|
|
49
|
-
});
|
|
50
|
-
// Build GitHub installation URL
|
|
51
|
-
const installationUrl = `https://github.com/apps/${options.appSlug}/installations/new?state=${state}`;
|
|
52
|
-
// Redirect user to GitHub's app installation page
|
|
53
|
-
return {
|
|
54
|
-
status: 302,
|
|
55
|
-
headers: {
|
|
56
|
-
Location: installationUrl,
|
|
57
|
-
},
|
|
58
|
-
data: {},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
// Handle unexpected errors
|
|
63
|
-
return (0, flink_1.internalServerError)(error.message || "Failed to initiate GitHub App installation");
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
exports.default = InitiateInstallation;
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
import { RouteProps } from "@flink-app/flink";
|
|
14
|
-
import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
|
|
15
|
-
/**
|
|
16
|
-
* Route configuration
|
|
17
|
-
* Registered programmatically by the plugin if registerRoutes is enabled
|
|
18
|
-
*/
|
|
19
|
-
export declare const Route: RouteProps;
|
|
20
|
-
/**
|
|
21
|
-
* GitHub App Installation Callback Handler
|
|
22
|
-
*
|
|
23
|
-
* Completes the installation flow by validating state, fetching installation
|
|
24
|
-
* details, calling the app's callback, and storing the installation.
|
|
25
|
-
*/
|
|
26
|
-
declare const InstallationCallback: ({ ctx, req }: {
|
|
27
|
-
ctx: GitHubAppInternalContext;
|
|
28
|
-
req: any;
|
|
29
|
-
}) => Promise<import("@flink-app/flink").FlinkResponse<undefined> | {
|
|
30
|
-
status: number;
|
|
31
|
-
headers: {
|
|
32
|
-
Location: string;
|
|
33
|
-
};
|
|
34
|
-
data: {};
|
|
35
|
-
}>;
|
|
36
|
-
export default InstallationCallback;
|
|
@@ -1,248 +0,0 @@
|
|
|
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;
|
|
@@ -1,37 +0,0 @@
|
|
|
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;
|
|
@@ -1,153 +0,0 @@
|
|
|
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;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query parameters received from GitHub after app installation.
|
|
3
|
-
*/
|
|
4
|
-
export default interface InstallationCallbackRequest {
|
|
5
|
-
installation_id: string;
|
|
6
|
-
setup_action: 'install' | 'update' | 'request';
|
|
7
|
-
state: string;
|
|
8
|
-
code?: string;
|
|
9
|
-
[key: string]: string | undefined;
|
|
10
|
-
}
|