@flink-app/github-app-plugin 0.12.1-alpha.45 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/github-app-plugin",
3
- "version": "0.12.1-alpha.45",
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.45",
27
- "@flink-app/test-utils": "^0.12.1-alpha.45",
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": "af426a157217c110ac9c7beb48e2e746968bec33"
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
- }
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });