@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
|
@@ -53,12 +53,6 @@ describe("Integration and Security Tests", () => {
|
|
|
53
53
|
webhookSecret: "test-webhook-secret",
|
|
54
54
|
clientId: "test-client-id",
|
|
55
55
|
clientSecret: "test-client-secret",
|
|
56
|
-
onInstallationSuccess: async ({ installationId, repositories, account }) => {
|
|
57
|
-
return {
|
|
58
|
-
userId: "test-user-id",
|
|
59
|
-
redirectUrl: "/dashboard",
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
56
|
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
|
|
63
57
|
// Webhook event handler
|
|
64
58
|
},
|
|
@@ -88,8 +82,8 @@ describe("Integration and Security Tests", () => {
|
|
|
88
82
|
});
|
|
89
83
|
|
|
90
84
|
describe("Integration Test: Full Installation Flow", () => {
|
|
91
|
-
it("should
|
|
92
|
-
//
|
|
85
|
+
it("should initiate installation and generate state", async () => {
|
|
86
|
+
// Test initiation only (callback must be implemented by host app)
|
|
93
87
|
const { redirectUrl, state } = await app.ctx.plugins.githubApp.initiateInstallation({
|
|
94
88
|
userId: "test-user",
|
|
95
89
|
metadata: { source: "test" },
|
|
@@ -98,6 +92,14 @@ describe("Integration and Security Tests", () => {
|
|
|
98
92
|
expect(redirectUrl).toContain("github.com/apps/test-app/installations/new");
|
|
99
93
|
expect(redirectUrl).toContain("state=");
|
|
100
94
|
expect(state).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should complete installation using completeInstallation method", async () => {
|
|
98
|
+
// Step 1: Initiate installation
|
|
99
|
+
const { state } = await app.ctx.plugins.githubApp.initiateInstallation({
|
|
100
|
+
userId: "test-user",
|
|
101
|
+
metadata: { source: "test" },
|
|
102
|
+
});
|
|
101
103
|
|
|
102
104
|
// Step 2: Mock GitHub API responses
|
|
103
105
|
global.fetch = jasmine.createSpy("fetch").and.returnValues(
|
|
@@ -140,17 +142,17 @@ describe("Integration and Security Tests", () => {
|
|
|
140
142
|
} as any)
|
|
141
143
|
);
|
|
142
144
|
|
|
143
|
-
// Step 3: Complete
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
state: state!,
|
|
149
|
-
},
|
|
145
|
+
// Step 3: Complete installation using plugin method
|
|
146
|
+
const result = await app.ctx.plugins.githubApp.completeInstallation({
|
|
147
|
+
installationId: 12345,
|
|
148
|
+
state: state!,
|
|
149
|
+
userId: "test-user-id",
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
152
|
+
expect(result.success).toBe(true);
|
|
153
|
+
expect(result.installation).toBeDefined();
|
|
154
|
+
expect(result.installation?.installationId).toBe(12345);
|
|
155
|
+
expect(result.installation?.accountLogin).toBe("testuser");
|
|
154
156
|
|
|
155
157
|
// Step 4: Verify installation was stored
|
|
156
158
|
const installation = await app.ctx.plugins.githubApp.getInstallation("test-user-id");
|
|
@@ -161,20 +163,19 @@ describe("Integration and Security Tests", () => {
|
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
describe("Security Test: CSRF State Validation", () => {
|
|
164
|
-
it("should prevent replay attacks with invalid state", async () => {
|
|
166
|
+
it("should prevent replay attacks with invalid state via completeInstallation", async () => {
|
|
165
167
|
const invalidState = generateState();
|
|
166
168
|
|
|
167
169
|
global.fetch = jasmine.createSpy("fetch");
|
|
168
170
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
state: invalidState,
|
|
174
|
-
},
|
|
171
|
+
const result = await app.ctx.plugins.githubApp.completeInstallation({
|
|
172
|
+
installationId: 12345,
|
|
173
|
+
state: invalidState,
|
|
174
|
+
userId: "test-user",
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
expect(
|
|
177
|
+
expect(result.success).toBe(false);
|
|
178
|
+
expect(result.error?.code).toBe("session-expired");
|
|
178
179
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
179
180
|
});
|
|
180
181
|
|
|
@@ -211,27 +212,24 @@ describe("Integration and Security Tests", () => {
|
|
|
211
212
|
} as any)
|
|
212
213
|
);
|
|
213
214
|
|
|
214
|
-
// Step 2: Complete
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
state: state!,
|
|
220
|
-
},
|
|
215
|
+
// Step 2: Complete installation first time (should succeed)
|
|
216
|
+
const firstResult = await app.ctx.plugins.githubApp.completeInstallation({
|
|
217
|
+
installationId: 12345,
|
|
218
|
+
state: state!,
|
|
219
|
+
userId: "test-user-id",
|
|
221
220
|
});
|
|
222
221
|
|
|
223
|
-
expect(
|
|
222
|
+
expect(firstResult.success).toBe(true);
|
|
224
223
|
|
|
225
224
|
// Step 3: Try to reuse same state (should fail)
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
state: state!,
|
|
231
|
-
},
|
|
225
|
+
const secondResult = await app.ctx.plugins.githubApp.completeInstallation({
|
|
226
|
+
installationId: 12345,
|
|
227
|
+
state: state!,
|
|
228
|
+
userId: "test-user-id",
|
|
232
229
|
});
|
|
233
230
|
|
|
234
|
-
expect(
|
|
231
|
+
expect(secondResult.success).toBe(false);
|
|
232
|
+
expect(secondResult.error?.code).toBe("session-expired");
|
|
235
233
|
});
|
|
236
234
|
});
|
|
237
235
|
|
|
@@ -399,16 +397,16 @@ describe("Integration and Security Tests", () => {
|
|
|
399
397
|
} as any)
|
|
400
398
|
);
|
|
401
399
|
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
state: state!,
|
|
407
|
-
},
|
|
400
|
+
const result = await app.ctx.plugins.githubApp.completeInstallation({
|
|
401
|
+
installationId: 12345,
|
|
402
|
+
state: state!,
|
|
403
|
+
userId: "test-user-id",
|
|
408
404
|
});
|
|
409
405
|
|
|
410
406
|
// Should handle error gracefully
|
|
411
|
-
expect(
|
|
407
|
+
expect(result.success).toBe(false);
|
|
408
|
+
expect(result.error).toBeDefined();
|
|
409
|
+
expect(result.error?.code).toBe("server-error");
|
|
412
410
|
});
|
|
413
411
|
|
|
414
412
|
it("should handle network errors when getting installation token", async () => {
|
package/spec/plugin-core.spec.ts
CHANGED
|
@@ -60,7 +60,6 @@ describe("Plugin Core", () => {
|
|
|
60
60
|
webhookSecret: "test-secret",
|
|
61
61
|
clientId: "test-client-id",
|
|
62
62
|
clientSecret: "test-client-secret",
|
|
63
|
-
onInstallationSuccess: async () => ({ userId: "test", redirectUrl: "/" }),
|
|
64
63
|
} as any;
|
|
65
64
|
|
|
66
65
|
expect(() => githubAppPlugin(invalidOptions)).toThrowError(/appId is required/);
|
|
@@ -72,14 +71,13 @@ describe("Plugin Core", () => {
|
|
|
72
71
|
webhookSecret: "test-secret",
|
|
73
72
|
clientId: "test-client-id",
|
|
74
73
|
clientSecret: "test-client-secret",
|
|
75
|
-
onInstallationSuccess: async () => ({ userId: "test", redirectUrl: "/" }),
|
|
76
74
|
} as any;
|
|
77
75
|
|
|
78
76
|
expect(() => githubAppPlugin(invalidOptions)).toThrowError(/privateKey is required/);
|
|
79
77
|
});
|
|
80
78
|
|
|
81
|
-
it("should throw error
|
|
82
|
-
const
|
|
79
|
+
it("should not throw error when onInstallationSuccess is omitted", () => {
|
|
80
|
+
const validOptions = {
|
|
83
81
|
appId: "12345",
|
|
84
82
|
privateKey: testPrivateKeyBase64,
|
|
85
83
|
webhookSecret: "test-secret",
|
|
@@ -87,7 +85,7 @@ describe("Plugin Core", () => {
|
|
|
87
85
|
clientSecret: "test-client-secret",
|
|
88
86
|
} as any;
|
|
89
87
|
|
|
90
|
-
expect(() => githubAppPlugin(
|
|
88
|
+
expect(() => githubAppPlugin(validOptions)).not.toThrow();
|
|
91
89
|
});
|
|
92
90
|
});
|
|
93
91
|
|
|
@@ -102,7 +100,6 @@ describe("Plugin Core", () => {
|
|
|
102
100
|
webhookSecret: "test-webhook-secret",
|
|
103
101
|
clientId: "test-client-id",
|
|
104
102
|
clientSecret: "test-client-secret",
|
|
105
|
-
onInstallationSuccess: async () => ({ userId: "test-user", redirectUrl: "/dashboard" }),
|
|
106
103
|
};
|
|
107
104
|
});
|
|
108
105
|
|
|
@@ -125,12 +122,12 @@ describe("Plugin Core", () => {
|
|
|
125
122
|
expect(mockApp.addRepo).toHaveBeenCalledWith("githubInstallationRepo", jasmine.any(Object));
|
|
126
123
|
});
|
|
127
124
|
|
|
128
|
-
it("should register only
|
|
125
|
+
it("should register only webhook handler when registerRoutes is true (default)", async () => {
|
|
129
126
|
const plugin = githubAppPlugin(validOptions);
|
|
130
127
|
await plugin.init!(mockApp, mockDb);
|
|
131
128
|
|
|
132
|
-
// Verify only
|
|
133
|
-
expect(mockApp.addHandler).toHaveBeenCalledTimes(
|
|
129
|
+
// Verify only 1 handler was registered (webhook only, NOT callback)
|
|
130
|
+
expect(mockApp.addHandler).toHaveBeenCalledTimes(1);
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
it("should NOT register handlers when registerRoutes is false", async () => {
|
|
@@ -187,7 +184,6 @@ describe("Plugin Core", () => {
|
|
|
187
184
|
webhookSecret: "test-webhook-secret",
|
|
188
185
|
clientId: "test-client-id",
|
|
189
186
|
clientSecret: "test-client-secret",
|
|
190
|
-
onInstallationSuccess: async () => ({ userId: "test-user", redirectUrl: "/dashboard" }),
|
|
191
187
|
};
|
|
192
188
|
|
|
193
189
|
plugin = githubAppPlugin(validOptions);
|
|
@@ -20,7 +20,6 @@ describe('GitHub App Plugin - Project Setup', () => {
|
|
|
20
20
|
webhookSecret: 'test',
|
|
21
21
|
clientId: 'test',
|
|
22
22
|
clientSecret: 'test',
|
|
23
|
-
onInstallationSuccess: async () => ({ userId: 'test', redirectUrl: '/' }),
|
|
24
23
|
};
|
|
25
24
|
expect(options).toBeDefined();
|
|
26
25
|
});
|
|
@@ -43,7 +42,6 @@ describe('GitHub App Plugin - Project Setup', () => {
|
|
|
43
42
|
webhookSecret: 'test-webhook-secret',
|
|
44
43
|
clientId: 'test-client-id',
|
|
45
44
|
clientSecret: 'test-client-secret',
|
|
46
|
-
onInstallationSuccess: async () => ({ userId: 'test', redirectUrl: '/' }),
|
|
47
45
|
});
|
|
48
46
|
|
|
49
47
|
expect(plugin).toBeDefined();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FlinkContext } from "@flink-app/flink";
|
|
2
|
+
import { GitHubAppPluginContext } from "./GitHubAppPluginContext";
|
|
3
|
+
import GitHubAppSessionRepo from "./repos/GitHubAppSessionRepo";
|
|
4
|
+
import GitHubInstallationRepo from "./repos/GitHubInstallationRepo";
|
|
5
|
+
import GitHubWebhookEventRepo from "./repos/GitHubWebhookEventRepo";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal context used for plugin handlers.
|
|
9
|
+
*/
|
|
10
|
+
export interface GitHubAppInternalContext extends FlinkContext<GitHubAppPluginContext> {
|
|
11
|
+
repos: {
|
|
12
|
+
githubAppSessionRepo: GitHubAppSessionRepo;
|
|
13
|
+
githubInstallationRepo: GitHubInstallationRepo;
|
|
14
|
+
githubWebhookEventRepo?: GitHubWebhookEventRepo;
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/GitHubAppPlugin.ts
CHANGED
|
@@ -8,10 +8,10 @@ import GitHubWebhookEventRepo from "./repos/GitHubWebhookEventRepo";
|
|
|
8
8
|
import { GitHubAuthService } from "./services/GitHubAuthService";
|
|
9
9
|
import { GitHubAPIClient } from "./services/GitHubAPIClient";
|
|
10
10
|
import { WebhookValidator } from "./services/WebhookValidator";
|
|
11
|
+
import { InstallationService } from "./services/InstallationService";
|
|
11
12
|
import GitHubInstallation from "./schemas/GitHubInstallation";
|
|
12
13
|
import { createGitHubAppError, GitHubAppErrorCodes } from "./utils/error-utils";
|
|
13
14
|
import { generateState, generateSessionId } from "./utils/state-utils";
|
|
14
|
-
import * as InstallationCallback from "./handlers/InstallationCallback";
|
|
15
15
|
import * as WebhookHandler from "./handlers/WebhookHandler";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -73,9 +73,6 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
73
73
|
if (!options.clientSecret) {
|
|
74
74
|
throw new Error("GitHub App Plugin: clientSecret is required");
|
|
75
75
|
}
|
|
76
|
-
if (!options.onInstallationSuccess) {
|
|
77
|
-
throw new Error("GitHub App Plugin: onInstallationSuccess callback is required");
|
|
78
|
-
}
|
|
79
76
|
|
|
80
77
|
// Determine configuration defaults
|
|
81
78
|
const baseUrl = options.baseUrl || "https://api.github.com";
|
|
@@ -87,6 +84,7 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
87
84
|
let flinkApp: FlinkApp<any>;
|
|
88
85
|
let authService: GitHubAuthService;
|
|
89
86
|
let webhookValidator: WebhookValidator;
|
|
87
|
+
let installationService: InstallationService;
|
|
90
88
|
let sessionRepo: GitHubAppSessionRepo;
|
|
91
89
|
let installationRepo: GitHubInstallationRepo;
|
|
92
90
|
let webhookEventRepo: GitHubWebhookEventRepo | undefined;
|
|
@@ -129,6 +127,9 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
129
127
|
flinkApp.addRepo("githubAppSessionRepo", sessionRepo);
|
|
130
128
|
flinkApp.addRepo("githubInstallationRepo", installationRepo);
|
|
131
129
|
|
|
130
|
+
// Initialize InstallationService
|
|
131
|
+
installationService = new InstallationService(authService, sessionRepo, installationRepo, baseUrl);
|
|
132
|
+
|
|
132
133
|
// Conditionally initialize webhook event repo if logging is enabled
|
|
133
134
|
if (logWebhookEvents) {
|
|
134
135
|
webhookEventRepo = new GitHubWebhookEventRepo(flinkApp.ctx, db, webhookEventsCollectionName);
|
|
@@ -149,11 +150,10 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
149
150
|
log.info(`GitHub App Plugin: Created TTL index on ${webhookEventsCollectionName} with ${webhookEventTTL}s expiration`);
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
// Conditionally register handlers (only
|
|
153
|
+
// Conditionally register handlers (only webhook handler)
|
|
153
154
|
if (registerRoutes) {
|
|
154
|
-
flinkApp.addHandler(InstallationCallback);
|
|
155
155
|
flinkApp.addHandler(WebhookHandler);
|
|
156
|
-
log.info("GitHub App Plugin: Registered
|
|
156
|
+
log.info("GitHub App Plugin: Registered webhook handler");
|
|
157
157
|
} else {
|
|
158
158
|
log.info("GitHub App Plugin: Skipped handler registration (routes disabled)");
|
|
159
159
|
}
|
|
@@ -217,6 +217,21 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Complete GitHub App installation
|
|
222
|
+
*/
|
|
223
|
+
async function completeInstallation(params: {
|
|
224
|
+
installationId: number;
|
|
225
|
+
state: string;
|
|
226
|
+
userId: string;
|
|
227
|
+
}) {
|
|
228
|
+
if (!installationService) {
|
|
229
|
+
throw new Error("GitHub App Plugin: Plugin not initialized");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return installationService.completeInstallation(params);
|
|
233
|
+
}
|
|
234
|
+
|
|
220
235
|
/**
|
|
221
236
|
* Uninstalls GitHub App for a user
|
|
222
237
|
*/
|
|
@@ -387,6 +402,7 @@ export function githubAppPlugin(options: GitHubAppPluginOptions): FlinkPlugin {
|
|
|
387
402
|
*/
|
|
388
403
|
const pluginCtx: GitHubAppPluginContext["githubApp"] = {
|
|
389
404
|
initiateInstallation,
|
|
405
|
+
completeInstallation,
|
|
390
406
|
uninstall,
|
|
391
407
|
getClient,
|
|
392
408
|
getInstallation,
|
|
@@ -3,6 +3,7 @@ import GitHubInstallation from "./schemas/GitHubInstallation";
|
|
|
3
3
|
import { GitHubAppPluginOptions } from "./GitHubAppPluginOptions";
|
|
4
4
|
import { GitHubAuthService } from "./services/GitHubAuthService";
|
|
5
5
|
import { WebhookValidator } from "./services/WebhookValidator";
|
|
6
|
+
import { CompleteInstallationResult } from "./services/InstallationService";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Public context interface exposed via ctx.plugins.githubApp
|
|
@@ -52,6 +53,63 @@ export interface GitHubAppPluginContext {
|
|
|
52
53
|
sessionId: string;
|
|
53
54
|
}>;
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Complete GitHub App installation
|
|
58
|
+
*
|
|
59
|
+
* Handles all the boilerplate of completing a GitHub App installation:
|
|
60
|
+
* 1. Validates the state parameter (CSRF protection)
|
|
61
|
+
* 2. Fetches installation details from GitHub API
|
|
62
|
+
* 3. Stores the installation in database
|
|
63
|
+
*
|
|
64
|
+
* The host application should:
|
|
65
|
+
* - Check if user is authenticated
|
|
66
|
+
* - Parse query parameters from the callback URL
|
|
67
|
+
* - Call this method with userId
|
|
68
|
+
* - Handle the response and redirect
|
|
69
|
+
*
|
|
70
|
+
* @param params - Installation completion parameters
|
|
71
|
+
* @param params.installationId - GitHub installation ID from query params
|
|
72
|
+
* @param params.state - State parameter from query params (CSRF protection)
|
|
73
|
+
* @param params.userId - Application user ID (from auth system)
|
|
74
|
+
* @returns Result with installation details or error
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // In your custom callback handler
|
|
79
|
+
* export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
|
|
80
|
+
* // 1. Check authentication (your way)
|
|
81
|
+
* if (!ctx.auth?.tokenData?.userId) {
|
|
82
|
+
* return unauthorized('Please log in first');
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* // 2. Parse query params
|
|
86
|
+
* const { installation_id, state } = req.query;
|
|
87
|
+
* if (!installation_id || !state) {
|
|
88
|
+
* return badRequest('Missing required parameters');
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* // 3. Complete installation
|
|
92
|
+
* const result = await ctx.plugins.githubApp.completeInstallation({
|
|
93
|
+
* installationId: parseInt(installation_id),
|
|
94
|
+
* state,
|
|
95
|
+
* userId: ctx.auth.tokenData.userId
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* // 4. Handle response (your way)
|
|
99
|
+
* if (!result.success) {
|
|
100
|
+
* return redirect(`/error?code=${result.error.code}`);
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* return redirect('/dashboard/github');
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
completeInstallation(params: {
|
|
108
|
+
installationId: number;
|
|
109
|
+
state: string;
|
|
110
|
+
userId: string;
|
|
111
|
+
}): Promise<CompleteInstallationResult>;
|
|
112
|
+
|
|
55
113
|
/**
|
|
56
114
|
* Uninstalls GitHub App for a user
|
|
57
115
|
*
|
|
@@ -8,6 +8,9 @@ export interface InstallationSuccessParams {
|
|
|
8
8
|
/** GitHub installation ID */
|
|
9
9
|
installationId: number;
|
|
10
10
|
|
|
11
|
+
/** Setup action type: 'install' for new installations, 'update' for repository selection updates */
|
|
12
|
+
setupAction: string;
|
|
13
|
+
|
|
11
14
|
/** GitHub account (user or organization) where app was installed */
|
|
12
15
|
account: {
|
|
13
16
|
/** Account ID on GitHub */
|
|
@@ -149,8 +152,11 @@ export interface WebhookEventParams {
|
|
|
149
152
|
/**
|
|
150
153
|
* Configuration options for GitHub App Plugin
|
|
151
154
|
*
|
|
152
|
-
* Configure the plugin with your GitHub App credentials
|
|
153
|
-
*
|
|
155
|
+
* Configure the plugin with your GitHub App credentials and optional settings
|
|
156
|
+
* for database, caching, and features.
|
|
157
|
+
*
|
|
158
|
+
* Note: Installation callback handler must be implemented by the host app.
|
|
159
|
+
* Use ctx.plugins.githubApp.completeInstallation() in your own handler.
|
|
154
160
|
*
|
|
155
161
|
* @example
|
|
156
162
|
* ```typescript
|
|
@@ -161,12 +167,7 @@ export interface WebhookEventParams {
|
|
|
161
167
|
* webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
162
168
|
* clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
163
169
|
* clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
164
|
-
*
|
|
165
|
-
* // Required: Installation success callback
|
|
166
|
-
* onInstallationSuccess: async ({ installationId, repositories }, ctx) => {
|
|
167
|
-
* const userId = getUserId(ctx); // Your auth system
|
|
168
|
-
* return { userId, redirectUrl: '/dashboard' };
|
|
169
|
-
* },
|
|
170
|
+
* appSlug: 'my-app',
|
|
170
171
|
*
|
|
171
172
|
* // Optional: Handle webhook events
|
|
172
173
|
* onWebhookEvent: async ({ event, payload }, ctx) => {
|
|
@@ -263,52 +264,6 @@ export interface GitHubAppPluginOptions {
|
|
|
263
264
|
|
|
264
265
|
// Callbacks
|
|
265
266
|
|
|
266
|
-
/**
|
|
267
|
-
* Callback invoked after successful installation
|
|
268
|
-
*
|
|
269
|
-
* REQUIRED. Called when user completes GitHub App installation.
|
|
270
|
-
* Return userId to link installation and optional redirectUrl.
|
|
271
|
-
*
|
|
272
|
-
* @param params - Installation data from GitHub
|
|
273
|
-
* @param ctx - Flink context with repos and plugins
|
|
274
|
-
* @returns Object with userId and optional redirectUrl
|
|
275
|
-
*
|
|
276
|
-
* @throws Should not throw - errors are caught and passed to onInstallationError
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```typescript
|
|
280
|
-
* async ({ installationId, repositories, account }, ctx) => {
|
|
281
|
-
* const userId = ctx.auth?.tokenData?.userId || 'anonymous';
|
|
282
|
-
* return {
|
|
283
|
-
* userId,
|
|
284
|
-
* redirectUrl: '/dashboard/github'
|
|
285
|
-
* };
|
|
286
|
-
* }
|
|
287
|
-
* ```
|
|
288
|
-
*/
|
|
289
|
-
onInstallationSuccess: (params: InstallationSuccessParams, ctx: any) => Promise<InstallationSuccessResponse>;
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Callback invoked when installation fails (optional)
|
|
293
|
-
*
|
|
294
|
-
* Called when installation encounters an error (invalid state,
|
|
295
|
-
* expired session, GitHub API failure, etc.)
|
|
296
|
-
*
|
|
297
|
-
* @param params - Error information
|
|
298
|
-
* @returns Object with optional redirectUrl
|
|
299
|
-
*
|
|
300
|
-
* @example
|
|
301
|
-
* ```typescript
|
|
302
|
-
* async ({ error, installationId }) => {
|
|
303
|
-
* console.error('Installation error:', error);
|
|
304
|
-
* return {
|
|
305
|
-
* redirectUrl: `/error?code=${error.code}`
|
|
306
|
-
* };
|
|
307
|
-
* }
|
|
308
|
-
* ```
|
|
309
|
-
*/
|
|
310
|
-
onInstallationError?: (params: InstallationErrorParams) => Promise<InstallationErrorResponse>;
|
|
311
|
-
|
|
312
267
|
/**
|
|
313
268
|
* Callback invoked when webhook event is received (optional)
|
|
314
269
|
*
|
|
@@ -393,8 +348,9 @@ export interface GitHubAppPluginOptions {
|
|
|
393
348
|
/**
|
|
394
349
|
* Register HTTP handlers automatically (optional)
|
|
395
350
|
*
|
|
396
|
-
* If true, registers
|
|
351
|
+
* If true, registers the webhook handler.
|
|
397
352
|
* Set to false if you want to implement custom handlers.
|
|
353
|
+
* Note: Installation callback handler must be implemented by the host app.
|
|
398
354
|
*
|
|
399
355
|
* @default true
|
|
400
356
|
*/
|
|
@@ -11,16 +11,8 @@
|
|
|
11
11
|
* Route: POST /github-app/webhook
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Context with GitHub App Plugin
|
|
19
|
-
*
|
|
20
|
-
* Note: Using 'any' for simplicity. In a real-world scenario, you'd want to properly
|
|
21
|
-
* type the context with both FlinkContext and GitHubAppPluginContext including the repos.
|
|
22
|
-
*/
|
|
23
|
-
type WebhookHandlerContext = FlinkContext<GitHubAppPluginContext>;
|
|
14
|
+
import { HttpMethod, RouteProps, log } from "@flink-app/flink";
|
|
15
|
+
import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
|
|
24
16
|
|
|
25
17
|
/**
|
|
26
18
|
* Route configuration
|
|
@@ -36,7 +28,7 @@ export const Route: RouteProps = {
|
|
|
36
28
|
*
|
|
37
29
|
* Validates webhook signatures and processes GitHub webhook events.
|
|
38
30
|
*/
|
|
39
|
-
const WebhookHandler = async ({ ctx, req }: { ctx:
|
|
31
|
+
const WebhookHandler = async ({ ctx, req }: { ctx: GitHubAppInternalContext; req: any }) => {
|
|
40
32
|
try {
|
|
41
33
|
// Extract headers
|
|
42
34
|
const signature = req.headers["x-hub-signature-256"] as string;
|
package/src/index.ts
CHANGED
|
@@ -9,21 +9,25 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// Plugin factory
|
|
12
|
-
export { githubAppPlugin } from
|
|
12
|
+
export { githubAppPlugin } from "./GitHubAppPlugin";
|
|
13
13
|
|
|
14
14
|
// Type exports
|
|
15
|
-
export type {
|
|
16
|
-
|
|
15
|
+
export type {
|
|
16
|
+
GitHubAppPluginOptions,
|
|
17
|
+
WebhookEventParams,
|
|
18
|
+
} from "./GitHubAppPluginOptions";
|
|
19
|
+
export type { GitHubAppPluginContext } from "./GitHubAppPluginContext";
|
|
17
20
|
|
|
18
21
|
// Schema exports
|
|
19
|
-
export type { default as GitHubInstallation } from
|
|
20
|
-
export type { default as GitHubAppSession } from
|
|
21
|
-
export type { default as WebhookEvent } from
|
|
22
|
-
export type { default as WebhookPayload } from
|
|
22
|
+
export type { default as GitHubInstallation } from "./schemas/GitHubInstallation";
|
|
23
|
+
export type { default as GitHubAppSession } from "./schemas/GitHubAppSession";
|
|
24
|
+
export type { default as WebhookEvent } from "./schemas/WebhookEvent";
|
|
25
|
+
export type { default as WebhookPayload } from "./schemas/WebhookPayload";
|
|
23
26
|
|
|
24
27
|
// Service exports (for advanced usage)
|
|
25
|
-
export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from
|
|
26
|
-
export { GitHubAuthService } from
|
|
28
|
+
export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from "./services/GitHubAPIClient";
|
|
29
|
+
export { GitHubAuthService } from "./services/GitHubAuthService";
|
|
30
|
+
export { InstallationService, type CompleteInstallationResult } from "./services/InstallationService";
|
|
27
31
|
|
|
28
32
|
// Error utilities
|
|
29
|
-
export { GitHubAppErrorCodes } from
|
|
33
|
+
export { GitHubAppErrorCodes } from "./utils/error-utils";
|