@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/dist/index.d.ts CHANGED
@@ -7,13 +7,14 @@
7
7
  * - Webhook handling with signature validation
8
8
  * - API client wrapper
9
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';
10
+ export { githubAppPlugin } from "./GitHubAppPlugin";
11
+ export type { GitHubAppPluginOptions, 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 { InstallationService, type CompleteInstallationResult } from "./services/InstallationService";
20
+ export { GitHubAppErrorCodes } from "./utils/error-utils";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - API client wrapper
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.GitHubAppErrorCodes = exports.GitHubAuthService = exports.GitHubAPIClient = exports.githubAppPlugin = void 0;
12
+ exports.GitHubAppErrorCodes = exports.InstallationService = exports.GitHubAuthService = exports.GitHubAPIClient = exports.githubAppPlugin = void 0;
13
13
  // Plugin factory
14
14
  var GitHubAppPlugin_1 = require("./GitHubAppPlugin");
15
15
  Object.defineProperty(exports, "githubAppPlugin", { enumerable: true, get: function () { return GitHubAppPlugin_1.githubAppPlugin; } });
@@ -18,6 +18,8 @@ var GitHubAPIClient_1 = require("./services/GitHubAPIClient");
18
18
  Object.defineProperty(exports, "GitHubAPIClient", { enumerable: true, get: function () { return GitHubAPIClient_1.GitHubAPIClient; } });
19
19
  var GitHubAuthService_1 = require("./services/GitHubAuthService");
20
20
  Object.defineProperty(exports, "GitHubAuthService", { enumerable: true, get: function () { return GitHubAuthService_1.GitHubAuthService; } });
21
+ var InstallationService_1 = require("./services/InstallationService");
22
+ Object.defineProperty(exports, "InstallationService", { enumerable: true, get: function () { return InstallationService_1.InstallationService; } });
21
23
  // Error utilities
22
24
  var error_utils_1 = require("./utils/error-utils");
23
25
  Object.defineProperty(exports, "GitHubAppErrorCodes", { enumerable: true, get: function () { return error_utils_1.GitHubAppErrorCodes; } });
@@ -0,0 +1,109 @@
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
+ import { GitHubAuthService } from "./GitHubAuthService";
10
+ import GitHubAppSessionRepo from "../repos/GitHubAppSessionRepo";
11
+ import GitHubInstallationRepo from "../repos/GitHubInstallationRepo";
12
+ import GitHubInstallation from "../schemas/GitHubInstallation";
13
+ /**
14
+ * Result of completing installation
15
+ */
16
+ export interface CompleteInstallationResult {
17
+ success: boolean;
18
+ installation?: GitHubInstallation;
19
+ error?: {
20
+ code: string;
21
+ message: string;
22
+ details?: any;
23
+ };
24
+ }
25
+ /**
26
+ * Installation Service
27
+ *
28
+ * Provides methods for completing GitHub App installation flow.
29
+ * Host applications call these methods from their own handlers with
30
+ * their own authentication and authorization logic.
31
+ */
32
+ export declare class InstallationService {
33
+ private authService;
34
+ private sessionRepo;
35
+ private installationRepo;
36
+ private baseUrl;
37
+ constructor(authService: GitHubAuthService, sessionRepo: GitHubAppSessionRepo, installationRepo: GitHubInstallationRepo, baseUrl?: string);
38
+ /**
39
+ * Complete GitHub App installation
40
+ *
41
+ * This method handles all the boilerplate of:
42
+ * 1. Validating the state parameter (CSRF protection)
43
+ * 2. Fetching installation details from GitHub API
44
+ * 3. Storing the installation in database
45
+ *
46
+ * The host application should:
47
+ * - Check if user is authenticated
48
+ * - Parse query parameters from the callback URL
49
+ * - Call this method with userId
50
+ * - Handle the response and redirect
51
+ *
52
+ * @param params - Installation completion parameters
53
+ * @param params.installationId - GitHub installation ID from query params
54
+ * @param params.state - State parameter from query params (CSRF protection)
55
+ * @param params.userId - Application user ID (from auth system)
56
+ * @returns Result with installation details or error
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // In your custom handler
61
+ * export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
62
+ * // 1. Check authentication (your way)
63
+ * if (!ctx.auth?.tokenData?.userId) {
64
+ * return unauthorized('Please log in first');
65
+ * }
66
+ *
67
+ * // 2. Parse query params
68
+ * const { installation_id, state } = req.query;
69
+ *
70
+ * // 3. Complete installation
71
+ * const result = await ctx.plugins.githubApp.completeInstallation({
72
+ * installationId: parseInt(installation_id),
73
+ * state,
74
+ * userId: ctx.auth.tokenData.userId
75
+ * });
76
+ *
77
+ * // 4. Handle response (your way)
78
+ * if (!result.success) {
79
+ * return redirect(`/error?code=${result.error.code}`);
80
+ * }
81
+ *
82
+ * return redirect('/dashboard/github');
83
+ * }
84
+ * ```
85
+ */
86
+ completeInstallation(params: {
87
+ installationId: number;
88
+ state: string;
89
+ userId: string;
90
+ }): Promise<CompleteInstallationResult>;
91
+ /**
92
+ * Fetch installation details from GitHub API
93
+ *
94
+ * @param installationId - GitHub installation ID
95
+ * @param jwt - GitHub App JWT
96
+ * @returns Installation details
97
+ * @private
98
+ */
99
+ private fetchInstallationDetails;
100
+ /**
101
+ * Fetch repositories accessible by installation
102
+ *
103
+ * @param installationId - GitHub installation ID
104
+ * @param jwt - GitHub App JWT
105
+ * @returns Array of repositories
106
+ * @private
107
+ */
108
+ private fetchInstallationRepositories;
109
+ }
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ /**
3
+ * Installation Service
4
+ *
5
+ * Handles GitHub App installation completion flow including:
6
+ * - State validation (CSRF protection)
7
+ * - Fetching installation details from GitHub API
8
+ * - Storing installation in database
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.InstallationService = void 0;
12
+ const flink_1 = require("@flink-app/flink");
13
+ const error_utils_1 = require("../utils/error-utils");
14
+ const state_utils_1 = require("../utils/state-utils");
15
+ /**
16
+ * Installation Service
17
+ *
18
+ * Provides methods for completing GitHub App installation flow.
19
+ * Host applications call these methods from their own handlers with
20
+ * their own authentication and authorization logic.
21
+ */
22
+ class InstallationService {
23
+ constructor(authService, sessionRepo, installationRepo, baseUrl = "https://api.github.com") {
24
+ this.authService = authService;
25
+ this.sessionRepo = sessionRepo;
26
+ this.installationRepo = installationRepo;
27
+ this.baseUrl = baseUrl;
28
+ }
29
+ /**
30
+ * Complete GitHub App installation
31
+ *
32
+ * This method handles all the boilerplate of:
33
+ * 1. Validating the state parameter (CSRF protection)
34
+ * 2. Fetching installation details from GitHub API
35
+ * 3. Storing the installation in database
36
+ *
37
+ * The host application should:
38
+ * - Check if user is authenticated
39
+ * - Parse query parameters from the callback URL
40
+ * - Call this method with userId
41
+ * - Handle the response and redirect
42
+ *
43
+ * @param params - Installation completion parameters
44
+ * @param params.installationId - GitHub installation ID from query params
45
+ * @param params.state - State parameter from query params (CSRF protection)
46
+ * @param params.userId - Application user ID (from auth system)
47
+ * @returns Result with installation details or error
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // In your custom handler
52
+ * export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
53
+ * // 1. Check authentication (your way)
54
+ * if (!ctx.auth?.tokenData?.userId) {
55
+ * return unauthorized('Please log in first');
56
+ * }
57
+ *
58
+ * // 2. Parse query params
59
+ * const { installation_id, state } = req.query;
60
+ *
61
+ * // 3. Complete installation
62
+ * const result = await ctx.plugins.githubApp.completeInstallation({
63
+ * installationId: parseInt(installation_id),
64
+ * state,
65
+ * userId: ctx.auth.tokenData.userId
66
+ * });
67
+ *
68
+ * // 4. Handle response (your way)
69
+ * if (!result.success) {
70
+ * return redirect(`/error?code=${result.error.code}`);
71
+ * }
72
+ *
73
+ * return redirect('/dashboard/github');
74
+ * }
75
+ * ```
76
+ */
77
+ async completeInstallation(params) {
78
+ const { installationId, state, userId } = params;
79
+ try {
80
+ // 1. Find session by state
81
+ const session = await this.sessionRepo.getOne({ state });
82
+ if (!session) {
83
+ return {
84
+ success: false,
85
+ error: {
86
+ code: error_utils_1.GitHubAppErrorCodes.SESSION_EXPIRED,
87
+ message: "Installation session not found or expired. Please try again.",
88
+ details: { state: state.substring(0, 10) + "..." },
89
+ },
90
+ };
91
+ }
92
+ // 2. Validate state parameter (CSRF protection)
93
+ if (!(0, state_utils_1.validateState)(state, session.state)) {
94
+ return {
95
+ success: false,
96
+ error: {
97
+ code: error_utils_1.GitHubAppErrorCodes.INVALID_STATE,
98
+ message: "Invalid state parameter. Possible CSRF attack detected.",
99
+ details: { providedState: state.substring(0, 10) + "..." },
100
+ },
101
+ };
102
+ }
103
+ // 3. Delete session immediately after validation (one-time use)
104
+ await this.sessionRepo.deleteBySessionId(session.sessionId);
105
+ // 4. Generate GitHub App JWT
106
+ const jwt = this.authService.generateAppJWT();
107
+ // 5. Fetch installation details from GitHub API
108
+ const installationDetails = await this.fetchInstallationDetails(installationId, jwt);
109
+ // 6. Fetch repositories accessible by this installation
110
+ const repositories = await this.fetchInstallationRepositories(installationId, jwt);
111
+ // 7. Store installation in database
112
+ const installation = await this.installationRepo.create({
113
+ userId,
114
+ installationId,
115
+ accountId: installationDetails.account.id,
116
+ accountLogin: installationDetails.account.login,
117
+ accountType: installationDetails.account.type,
118
+ avatarUrl: installationDetails.account.avatar_url,
119
+ repositories: repositories.map((repo) => ({
120
+ id: repo.id,
121
+ name: repo.name,
122
+ fullName: repo.full_name,
123
+ private: repo.private,
124
+ })),
125
+ permissions: installationDetails.permissions || {},
126
+ events: installationDetails.events || [],
127
+ createdAt: new Date(),
128
+ updatedAt: new Date(),
129
+ });
130
+ flink_1.log.info("GitHub App installation completed", {
131
+ userId,
132
+ installationId,
133
+ accountLogin: installationDetails.account.login,
134
+ repositoryCount: repositories.length,
135
+ });
136
+ return {
137
+ success: true,
138
+ installation,
139
+ };
140
+ }
141
+ catch (error) {
142
+ flink_1.log.error("GitHub App installation completion failed", {
143
+ userId,
144
+ installationId,
145
+ error: error.message,
146
+ });
147
+ return {
148
+ success: false,
149
+ error: {
150
+ code: error.code || error_utils_1.GitHubAppErrorCodes.SERVER_ERROR,
151
+ message: error.message || "Installation completion failed. Please try again.",
152
+ details: error.details,
153
+ },
154
+ };
155
+ }
156
+ }
157
+ /**
158
+ * Fetch installation details from GitHub API
159
+ *
160
+ * @param installationId - GitHub installation ID
161
+ * @param jwt - GitHub App JWT
162
+ * @returns Installation details
163
+ * @private
164
+ */
165
+ async fetchInstallationDetails(installationId, jwt) {
166
+ const url = `${this.baseUrl}/app/installations/${installationId}`;
167
+ const response = await fetch(url, {
168
+ method: "GET",
169
+ headers: {
170
+ Authorization: `Bearer ${jwt}`,
171
+ Accept: "application/vnd.github+json",
172
+ "User-Agent": "Flink-GitHub-App-Plugin",
173
+ },
174
+ });
175
+ if (!response.ok) {
176
+ const errorBody = await response.text();
177
+ throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, `Failed to fetch installation details: ${response.statusText}`, { status: response.status, body: errorBody });
178
+ }
179
+ return (await response.json());
180
+ }
181
+ /**
182
+ * Fetch repositories accessible by installation
183
+ *
184
+ * @param installationId - GitHub installation ID
185
+ * @param jwt - GitHub App JWT
186
+ * @returns Array of repositories
187
+ * @private
188
+ */
189
+ async fetchInstallationRepositories(installationId, jwt) {
190
+ const url = `${this.baseUrl}/installation/repositories`;
191
+ // First get an installation token
192
+ const tokenUrl = `${this.baseUrl}/app/installations/${installationId}/access_tokens`;
193
+ const tokenResponse = await fetch(tokenUrl, {
194
+ method: "POST",
195
+ headers: {
196
+ Authorization: `Bearer ${jwt}`,
197
+ Accept: "application/vnd.github+json",
198
+ "User-Agent": "Flink-GitHub-App-Plugin",
199
+ },
200
+ });
201
+ if (!tokenResponse.ok) {
202
+ const errorBody = await tokenResponse.text();
203
+ throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED, `Failed to get installation token: ${tokenResponse.statusText}`, { status: tokenResponse.status, body: errorBody });
204
+ }
205
+ const tokenData = await tokenResponse.json();
206
+ const token = tokenData.token;
207
+ // Now fetch repositories with the installation token
208
+ const reposResponse = await fetch(url, {
209
+ method: "GET",
210
+ headers: {
211
+ Authorization: `Bearer ${token}`,
212
+ Accept: "application/vnd.github+json",
213
+ "User-Agent": "Flink-GitHub-App-Plugin",
214
+ },
215
+ });
216
+ if (!reposResponse.ok) {
217
+ const errorBody = await reposResponse.text();
218
+ throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, `Failed to fetch repositories: ${reposResponse.statusText}`, { status: reposResponse.status, body: errorBody });
219
+ }
220
+ const data = await reposResponse.json();
221
+ return data.repositories || [];
222
+ }
223
+ }
224
+ exports.InstallationService = InstallationService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/github-app-plugin",
3
- "version": "0.12.1-alpha.38",
3
+ "version": "0.12.1-alpha.40",
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.35",
27
- "@flink-app/test-utils": "^0.12.1-alpha.38",
26
+ "@flink-app/flink": "^0.12.1-alpha.40",
27
+ "@flink-app/test-utils": "^0.12.1-alpha.40",
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": "bba579788683fe0729399a56152eba4974f29bb6"
40
+ "gitHead": "456502f273fe9473df05b71a803f3eda1a2f8931"
41
41
  }
@@ -1,142 +1,13 @@
1
1
  /**
2
2
  * Handler Tests for GitHub App Plugin
3
3
  *
4
- * Tests for installation callback and webhook handlers.
4
+ * Tests for webhook handler.
5
5
  * Focused tests covering critical paths only.
6
6
  */
7
7
 
8
- import InstallationCallback from "../src/handlers/InstallationCallback";
9
8
  import WebhookHandler from "../src/handlers/WebhookHandler";
10
- import { generateSessionId, generateState } from "../src/utils/state-utils";
11
9
 
12
10
  describe("GitHub App Handlers", () => {
13
- describe("InstallationCallback", () => {
14
- it("should validate state and store installation", async () => {
15
- const state = generateState();
16
- const mockSession = {
17
- sessionId: generateSessionId(),
18
- state,
19
- userId: "user123",
20
- createdAt: new Date(),
21
- };
22
-
23
- const mockInstallation = {
24
- id: 12345,
25
- account: {
26
- id: 67890,
27
- login: "testuser",
28
- type: "User" as const,
29
- avatar_url: "https://example.com/avatar.png",
30
- },
31
- repository_selection: "selected",
32
- permissions: {
33
- contents: "read",
34
- issues: "write",
35
- },
36
- events: ["push", "pull_request"],
37
- };
38
-
39
- const mockRepositories = [
40
- {
41
- id: 1,
42
- name: "repo1",
43
- full_name: "testuser/repo1",
44
- private: false,
45
- },
46
- ];
47
-
48
- const mockCtx: any = {
49
- repos: {
50
- githubAppSessionRepo: {
51
- getOne: jasmine.createSpy("getOne").and.returnValue(Promise.resolve(mockSession)),
52
- deleteBySessionId: jasmine.createSpy("deleteBySessionId").and.returnValue(Promise.resolve(1)),
53
- },
54
- githubInstallationRepo: {
55
- create: jasmine.createSpy("create").and.returnValue(Promise.resolve({})),
56
- },
57
- },
58
- plugins: {
59
- githubApp: {
60
- authService: {
61
- generateAppJWT: jasmine.createSpy("generateAppJWT").and.returnValue("mock-jwt"),
62
- },
63
- options: {
64
- baseUrl: "https://api.github.com",
65
- onInstallationSuccess: jasmine.createSpy("onInstallationSuccess").and.returnValue(
66
- Promise.resolve({
67
- userId: "user123",
68
- redirectUrl: "/dashboard",
69
- })
70
- ),
71
- },
72
- },
73
- },
74
- };
75
-
76
- // Mock fetch for installation details and repositories
77
- global.fetch = jasmine.createSpy("fetch").and.returnValues(
78
- // First call: get installation details
79
- Promise.resolve({
80
- ok: true,
81
- json: () => Promise.resolve(mockInstallation),
82
- } as any),
83
- // Second call: get installation token
84
- Promise.resolve({
85
- ok: true,
86
- json: () => Promise.resolve({ token: "mock-token" }),
87
- } as any),
88
- // Third call: get repositories
89
- Promise.resolve({
90
- ok: true,
91
- json: () => Promise.resolve({ repositories: mockRepositories }),
92
- } as any)
93
- );
94
-
95
- const mockReq: any = {
96
- query: {
97
- installation_id: "12345",
98
- setup_action: "install",
99
- state,
100
- },
101
- };
102
-
103
- const result = await InstallationCallback({ ctx: mockCtx, req: mockReq } as any);
104
-
105
- expect(result.status).toBe(302);
106
- expect(mockCtx.repos.githubAppSessionRepo.getOne).toHaveBeenCalledWith({ state });
107
- expect(mockCtx.repos.githubAppSessionRepo.deleteBySessionId).toHaveBeenCalledWith(mockSession.sessionId);
108
- expect(mockCtx.plugins.githubApp.options.onInstallationSuccess).toHaveBeenCalled();
109
- expect(mockCtx.repos.githubInstallationRepo.create).toHaveBeenCalled();
110
- });
111
-
112
- it("should reject invalid state", async () => {
113
- const mockCtx: any = {
114
- repos: {
115
- githubAppSessionRepo: {
116
- getOne: jasmine.createSpy("getOne").and.returnValue(Promise.resolve(null)),
117
- },
118
- },
119
- plugins: {
120
- githubApp: {
121
- options: {},
122
- },
123
- },
124
- };
125
-
126
- const mockReq: any = {
127
- query: {
128
- installation_id: "12345",
129
- setup_action: "install",
130
- state: "invalid-state",
131
- },
132
- };
133
-
134
- const result = await InstallationCallback({ ctx: mockCtx, req: mockReq } as any);
135
-
136
- expect(result.status).toBe(400);
137
- });
138
- });
139
-
140
11
  describe("WebhookHandler", () => {
141
12
  it("should validate signature and call webhook callback", async () => {
142
13
  const secret = "test-secret";