@flink-app/github-app-plugin 0.12.1-alpha.38 → 0.12.1-alpha.40
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/README.md +106 -163
- package/dist/GitHubAppInternalContext.d.ts +4 -33
- package/dist/GitHubAppPlugin.js +16 -7
- package/dist/GitHubAppPluginContext.d.ts +57 -0
- package/dist/GitHubAppPluginOptions.d.ts +10 -53
- package/dist/handlers/InstallationCallback.d.ts +3 -9
- package/dist/handlers/WebhookHandler.d.ts +3 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +3 -1
- package/dist/services/InstallationService.d.ts +109 -0
- package/dist/services/InstallationService.js +224 -0
- package/package.json +4 -4
- package/spec/handlers.spec.ts +1 -130
- package/spec/integration-and-security.spec.ts +45 -47
- package/spec/plugin-core.spec.ts +6 -10
- package/spec/project-setup.spec.ts +0 -2
- package/src/GitHubAppInternalContext.ts +16 -0
- package/src/GitHubAppPlugin.ts +23 -7
- package/src/GitHubAppPluginContext.ts +58 -0
- package/src/GitHubAppPluginOptions.ts +11 -55
- package/src/handlers/WebhookHandler.ts +3 -11
- package/src/index.ts +14 -10
- package/src/services/InstallationService.ts +302 -0
- package/src/handlers/InstallationCallback.ts +0 -292
- package/src/schemas/InstallationCallbackRequest.ts +0 -10
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installation Service
|
|
3
|
+
*
|
|
4
|
+
* Handles GitHub App installation completion flow including:
|
|
5
|
+
* - State validation (CSRF protection)
|
|
6
|
+
* - Fetching installation details from GitHub API
|
|
7
|
+
* - Storing installation in database
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { log } from "@flink-app/flink";
|
|
11
|
+
import { GitHubAuthService } from "./GitHubAuthService";
|
|
12
|
+
import GitHubAppSessionRepo from "../repos/GitHubAppSessionRepo";
|
|
13
|
+
import GitHubInstallationRepo from "../repos/GitHubInstallationRepo";
|
|
14
|
+
import GitHubInstallation from "../schemas/GitHubInstallation";
|
|
15
|
+
import { createGitHubAppError, GitHubAppErrorCodes, handleGitHubAPIError } from "../utils/error-utils";
|
|
16
|
+
import { validateState } from "../utils/state-utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Installation details from GitHub API
|
|
20
|
+
*/
|
|
21
|
+
interface GitHubInstallationDetails {
|
|
22
|
+
id: number;
|
|
23
|
+
account: {
|
|
24
|
+
id: number;
|
|
25
|
+
login: string;
|
|
26
|
+
type: "User" | "Organization";
|
|
27
|
+
avatar_url?: string;
|
|
28
|
+
};
|
|
29
|
+
permissions?: Record<string, string>;
|
|
30
|
+
events?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Repository details from GitHub API
|
|
35
|
+
*/
|
|
36
|
+
interface GitHubRepository {
|
|
37
|
+
id: number;
|
|
38
|
+
name: string;
|
|
39
|
+
full_name: string;
|
|
40
|
+
private: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Result of completing installation
|
|
45
|
+
*/
|
|
46
|
+
export interface CompleteInstallationResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
installation?: GitHubInstallation;
|
|
49
|
+
error?: {
|
|
50
|
+
code: string;
|
|
51
|
+
message: string;
|
|
52
|
+
details?: any;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Installation Service
|
|
58
|
+
*
|
|
59
|
+
* Provides methods for completing GitHub App installation flow.
|
|
60
|
+
* Host applications call these methods from their own handlers with
|
|
61
|
+
* their own authentication and authorization logic.
|
|
62
|
+
*/
|
|
63
|
+
export class InstallationService {
|
|
64
|
+
constructor(
|
|
65
|
+
private authService: GitHubAuthService,
|
|
66
|
+
private sessionRepo: GitHubAppSessionRepo,
|
|
67
|
+
private installationRepo: GitHubInstallationRepo,
|
|
68
|
+
private baseUrl: string = "https://api.github.com"
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Complete GitHub App installation
|
|
73
|
+
*
|
|
74
|
+
* This method handles all the boilerplate of:
|
|
75
|
+
* 1. Validating the state parameter (CSRF protection)
|
|
76
|
+
* 2. Fetching installation details from GitHub API
|
|
77
|
+
* 3. Storing the installation in database
|
|
78
|
+
*
|
|
79
|
+
* The host application should:
|
|
80
|
+
* - Check if user is authenticated
|
|
81
|
+
* - Parse query parameters from the callback URL
|
|
82
|
+
* - Call this method with userId
|
|
83
|
+
* - Handle the response and redirect
|
|
84
|
+
*
|
|
85
|
+
* @param params - Installation completion parameters
|
|
86
|
+
* @param params.installationId - GitHub installation ID from query params
|
|
87
|
+
* @param params.state - State parameter from query params (CSRF protection)
|
|
88
|
+
* @param params.userId - Application user ID (from auth system)
|
|
89
|
+
* @returns Result with installation details or error
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // In your custom handler
|
|
94
|
+
* export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
|
|
95
|
+
* // 1. Check authentication (your way)
|
|
96
|
+
* if (!ctx.auth?.tokenData?.userId) {
|
|
97
|
+
* return unauthorized('Please log in first');
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* // 2. Parse query params
|
|
101
|
+
* const { installation_id, state } = req.query;
|
|
102
|
+
*
|
|
103
|
+
* // 3. Complete installation
|
|
104
|
+
* const result = await ctx.plugins.githubApp.completeInstallation({
|
|
105
|
+
* installationId: parseInt(installation_id),
|
|
106
|
+
* state,
|
|
107
|
+
* userId: ctx.auth.tokenData.userId
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // 4. Handle response (your way)
|
|
111
|
+
* if (!result.success) {
|
|
112
|
+
* return redirect(`/error?code=${result.error.code}`);
|
|
113
|
+
* }
|
|
114
|
+
*
|
|
115
|
+
* return redirect('/dashboard/github');
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
async completeInstallation(params: {
|
|
120
|
+
installationId: number;
|
|
121
|
+
state: string;
|
|
122
|
+
userId: string;
|
|
123
|
+
}): Promise<CompleteInstallationResult> {
|
|
124
|
+
const { installationId, state, userId } = params;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// 1. Find session by state
|
|
128
|
+
const session = await this.sessionRepo.getOne({ state });
|
|
129
|
+
|
|
130
|
+
if (!session) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: {
|
|
134
|
+
code: GitHubAppErrorCodes.SESSION_EXPIRED,
|
|
135
|
+
message: "Installation session not found or expired. Please try again.",
|
|
136
|
+
details: { state: state.substring(0, 10) + "..." },
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Validate state parameter (CSRF protection)
|
|
142
|
+
if (!validateState(state, session.state)) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: {
|
|
146
|
+
code: GitHubAppErrorCodes.INVALID_STATE,
|
|
147
|
+
message: "Invalid state parameter. Possible CSRF attack detected.",
|
|
148
|
+
details: { providedState: state.substring(0, 10) + "..." },
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 3. Delete session immediately after validation (one-time use)
|
|
154
|
+
await this.sessionRepo.deleteBySessionId(session.sessionId);
|
|
155
|
+
|
|
156
|
+
// 4. Generate GitHub App JWT
|
|
157
|
+
const jwt = this.authService.generateAppJWT();
|
|
158
|
+
|
|
159
|
+
// 5. Fetch installation details from GitHub API
|
|
160
|
+
const installationDetails = await this.fetchInstallationDetails(installationId, jwt);
|
|
161
|
+
|
|
162
|
+
// 6. Fetch repositories accessible by this installation
|
|
163
|
+
const repositories = await this.fetchInstallationRepositories(installationId, jwt);
|
|
164
|
+
|
|
165
|
+
// 7. Store installation in database
|
|
166
|
+
const installation = await this.installationRepo.create({
|
|
167
|
+
userId,
|
|
168
|
+
installationId,
|
|
169
|
+
accountId: installationDetails.account.id,
|
|
170
|
+
accountLogin: installationDetails.account.login,
|
|
171
|
+
accountType: installationDetails.account.type,
|
|
172
|
+
avatarUrl: installationDetails.account.avatar_url,
|
|
173
|
+
repositories: repositories.map((repo) => ({
|
|
174
|
+
id: repo.id,
|
|
175
|
+
name: repo.name,
|
|
176
|
+
fullName: repo.full_name,
|
|
177
|
+
private: repo.private,
|
|
178
|
+
})),
|
|
179
|
+
permissions: installationDetails.permissions || {},
|
|
180
|
+
events: installationDetails.events || [],
|
|
181
|
+
createdAt: new Date(),
|
|
182
|
+
updatedAt: new Date(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
log.info("GitHub App installation completed", {
|
|
186
|
+
userId,
|
|
187
|
+
installationId,
|
|
188
|
+
accountLogin: installationDetails.account.login,
|
|
189
|
+
repositoryCount: repositories.length,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
installation,
|
|
195
|
+
};
|
|
196
|
+
} catch (error: any) {
|
|
197
|
+
log.error("GitHub App installation completion failed", {
|
|
198
|
+
userId,
|
|
199
|
+
installationId,
|
|
200
|
+
error: error.message,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: {
|
|
206
|
+
code: error.code || GitHubAppErrorCodes.SERVER_ERROR,
|
|
207
|
+
message: error.message || "Installation completion failed. Please try again.",
|
|
208
|
+
details: error.details,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fetch installation details from GitHub API
|
|
216
|
+
*
|
|
217
|
+
* @param installationId - GitHub installation ID
|
|
218
|
+
* @param jwt - GitHub App JWT
|
|
219
|
+
* @returns Installation details
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
private async fetchInstallationDetails(installationId: number, jwt: string): Promise<GitHubInstallationDetails> {
|
|
223
|
+
const url = `${this.baseUrl}/app/installations/${installationId}`;
|
|
224
|
+
|
|
225
|
+
const response = await fetch(url, {
|
|
226
|
+
method: "GET",
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${jwt}`,
|
|
229
|
+
Accept: "application/vnd.github+json",
|
|
230
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const errorBody = await response.text();
|
|
236
|
+
throw createGitHubAppError(
|
|
237
|
+
GitHubAppErrorCodes.SERVER_ERROR,
|
|
238
|
+
`Failed to fetch installation details: ${response.statusText}`,
|
|
239
|
+
{ status: response.status, body: errorBody }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (await response.json()) as GitHubInstallationDetails;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fetch repositories accessible by installation
|
|
248
|
+
*
|
|
249
|
+
* @param installationId - GitHub installation ID
|
|
250
|
+
* @param jwt - GitHub App JWT
|
|
251
|
+
* @returns Array of repositories
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
private async fetchInstallationRepositories(installationId: number, jwt: string): Promise<GitHubRepository[]> {
|
|
255
|
+
const url = `${this.baseUrl}/installation/repositories`;
|
|
256
|
+
|
|
257
|
+
// First get an installation token
|
|
258
|
+
const tokenUrl = `${this.baseUrl}/app/installations/${installationId}/access_tokens`;
|
|
259
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: {
|
|
262
|
+
Authorization: `Bearer ${jwt}`,
|
|
263
|
+
Accept: "application/vnd.github+json",
|
|
264
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!tokenResponse.ok) {
|
|
269
|
+
const errorBody = await tokenResponse.text();
|
|
270
|
+
throw createGitHubAppError(
|
|
271
|
+
GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED,
|
|
272
|
+
`Failed to get installation token: ${tokenResponse.statusText}`,
|
|
273
|
+
{ status: tokenResponse.status, body: errorBody }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tokenData: any = await tokenResponse.json();
|
|
278
|
+
const token = tokenData.token;
|
|
279
|
+
|
|
280
|
+
// Now fetch repositories with the installation token
|
|
281
|
+
const reposResponse = await fetch(url, {
|
|
282
|
+
method: "GET",
|
|
283
|
+
headers: {
|
|
284
|
+
Authorization: `Bearer ${token}`,
|
|
285
|
+
Accept: "application/vnd.github+json",
|
|
286
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!reposResponse.ok) {
|
|
291
|
+
const errorBody = await reposResponse.text();
|
|
292
|
+
throw createGitHubAppError(
|
|
293
|
+
GitHubAppErrorCodes.SERVER_ERROR,
|
|
294
|
+
`Failed to fetch repositories: ${reposResponse.statusText}`,
|
|
295
|
+
{ status: reposResponse.status, body: errorBody }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const data: any = await reposResponse.json();
|
|
300
|
+
return data.repositories || [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -1,292 +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
|
-
|
|
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;
|
|
@@ -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
|
-
}
|