@flink-app/github-app-plugin 0.12.1-alpha.38

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/LICENSE +21 -0
  3. package/README.md +667 -0
  4. package/SECURITY.md +498 -0
  5. package/dist/GitHubAppInternalContext.d.ts +44 -0
  6. package/dist/GitHubAppInternalContext.js +2 -0
  7. package/dist/GitHubAppPlugin.d.ts +45 -0
  8. package/dist/GitHubAppPlugin.js +367 -0
  9. package/dist/GitHubAppPluginContext.d.ts +242 -0
  10. package/dist/GitHubAppPluginContext.js +2 -0
  11. package/dist/GitHubAppPluginOptions.d.ts +369 -0
  12. package/dist/GitHubAppPluginOptions.js +2 -0
  13. package/dist/handlers/InitiateInstallation.d.ts +32 -0
  14. package/dist/handlers/InitiateInstallation.js +66 -0
  15. package/dist/handlers/InstallationCallback.d.ts +42 -0
  16. package/dist/handlers/InstallationCallback.js +248 -0
  17. package/dist/handlers/UninstallHandler.d.ts +37 -0
  18. package/dist/handlers/UninstallHandler.js +153 -0
  19. package/dist/handlers/WebhookHandler.d.ts +54 -0
  20. package/dist/handlers/WebhookHandler.js +157 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.js +23 -0
  23. package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
  24. package/dist/repos/GitHubAppSessionRepo.js +32 -0
  25. package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
  26. package/dist/repos/GitHubInstallationRepo.js +83 -0
  27. package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
  28. package/dist/repos/GitHubWebhookEventRepo.js +42 -0
  29. package/dist/schemas/GitHubAppSession.d.ts +13 -0
  30. package/dist/schemas/GitHubAppSession.js +2 -0
  31. package/dist/schemas/GitHubInstallation.d.ts +28 -0
  32. package/dist/schemas/GitHubInstallation.js +2 -0
  33. package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
  34. package/dist/schemas/InstallationCallbackRequest.js +2 -0
  35. package/dist/schemas/WebhookEvent.d.ts +16 -0
  36. package/dist/schemas/WebhookEvent.js +2 -0
  37. package/dist/schemas/WebhookPayload.d.ts +35 -0
  38. package/dist/schemas/WebhookPayload.js +2 -0
  39. package/dist/services/GitHubAPIClient.d.ts +143 -0
  40. package/dist/services/GitHubAPIClient.js +167 -0
  41. package/dist/services/GitHubAuthService.d.ts +85 -0
  42. package/dist/services/GitHubAuthService.js +160 -0
  43. package/dist/services/WebhookValidator.d.ts +93 -0
  44. package/dist/services/WebhookValidator.js +123 -0
  45. package/dist/utils/error-utils.d.ts +67 -0
  46. package/dist/utils/error-utils.js +121 -0
  47. package/dist/utils/jwt-utils.d.ts +35 -0
  48. package/dist/utils/jwt-utils.js +67 -0
  49. package/dist/utils/state-utils.d.ts +38 -0
  50. package/dist/utils/state-utils.js +74 -0
  51. package/dist/utils/token-cache-utils.d.ts +47 -0
  52. package/dist/utils/token-cache-utils.js +74 -0
  53. package/dist/utils/webhook-signature-utils.d.ts +22 -0
  54. package/dist/utils/webhook-signature-utils.js +57 -0
  55. package/examples/basic-installation.ts +246 -0
  56. package/examples/create-issue.ts +392 -0
  57. package/examples/error-handling.ts +396 -0
  58. package/examples/multi-event-webhook.ts +367 -0
  59. package/examples/organization-installation.ts +316 -0
  60. package/examples/repository-access.ts +480 -0
  61. package/examples/webhook-handling.ts +343 -0
  62. package/examples/with-jwt-auth.ts +319 -0
  63. package/package.json +41 -0
  64. package/spec/core-utilities.spec.ts +243 -0
  65. package/spec/handlers.spec.ts +216 -0
  66. package/spec/helpers/reporter.ts +41 -0
  67. package/spec/integration-and-security.spec.ts +483 -0
  68. package/spec/plugin-core.spec.ts +258 -0
  69. package/spec/project-setup.spec.ts +56 -0
  70. package/spec/repos-and-schemas.spec.ts +288 -0
  71. package/spec/services.spec.ts +108 -0
  72. package/spec/support/jasmine.json +7 -0
  73. package/src/GitHubAppPlugin.ts +411 -0
  74. package/src/GitHubAppPluginContext.ts +254 -0
  75. package/src/GitHubAppPluginOptions.ts +412 -0
  76. package/src/handlers/InstallationCallback.ts +292 -0
  77. package/src/handlers/WebhookHandler.ts +179 -0
  78. package/src/index.ts +29 -0
  79. package/src/repos/GitHubAppSessionRepo.ts +36 -0
  80. package/src/repos/GitHubInstallationRepo.ts +95 -0
  81. package/src/repos/GitHubWebhookEventRepo.ts +48 -0
  82. package/src/schemas/GitHubAppSession.ts +13 -0
  83. package/src/schemas/GitHubInstallation.ts +28 -0
  84. package/src/schemas/InstallationCallbackRequest.ts +10 -0
  85. package/src/schemas/WebhookEvent.ts +16 -0
  86. package/src/schemas/WebhookPayload.ts +35 -0
  87. package/src/services/GitHubAPIClient.ts +244 -0
  88. package/src/services/GitHubAuthService.ts +188 -0
  89. package/src/services/WebhookValidator.ts +159 -0
  90. package/src/utils/error-utils.ts +148 -0
  91. package/src/utils/jwt-utils.ts +64 -0
  92. package/src/utils/state-utils.ts +72 -0
  93. package/src/utils/token-cache-utils.ts +89 -0
  94. package/src/utils/webhook-signature-utils.ts +57 -0
  95. package/tsconfig.dist.json +4 -0
  96. package/tsconfig.json +24 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Comprehensive Error Handling Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Handling installation errors
6
+ * - API client error handling
7
+ * - GitHub API rate limiting
8
+ * - Permission errors
9
+ * - Network errors
10
+ * - Retry logic
11
+ */
12
+
13
+ import { FlinkApp, FlinkContext, GetHandler } from "@flink-app/flink";
14
+ import { githubAppPlugin, GitHubAppErrorCodes } from "@flink-app/github-app-plugin";
15
+
16
+ interface AppContext extends FlinkContext {
17
+ plugins: {
18
+ githubApp: any;
19
+ };
20
+ }
21
+
22
+ async function start() {
23
+ const app = new FlinkApp<AppContext>({
24
+ name: "GitHub App Error Handling Example",
25
+ port: 3333,
26
+
27
+ db: {
28
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-error-example",
29
+ },
30
+
31
+ plugins: [
32
+ githubAppPlugin({
33
+ appId: process.env.GITHUB_APP_ID!,
34
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
35
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
36
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
37
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
38
+
39
+ onInstallationSuccess: async ({ installationId, account }) => {
40
+ console.log(`Installation successful: ${installationId}`);
41
+ return {
42
+ userId: "demo-user",
43
+ redirectUrl: "/dashboard",
44
+ };
45
+ },
46
+
47
+ // Handle installation errors
48
+ onInstallationError: async ({ error, installationId }) => {
49
+ console.error(`\n=== Installation Error ===`);
50
+ console.error(`Code: ${error.code}`);
51
+ console.error(`Message: ${error.message}`);
52
+
53
+ // Handle specific error codes
54
+ switch (error.code) {
55
+ case GitHubAppErrorCodes.INVALID_STATE:
56
+ console.error("CSRF state validation failed");
57
+ console.error("Possible causes:");
58
+ console.error("- Session expired");
59
+ console.error("- State parameter tampered with");
60
+ console.error("- Clock skew between servers");
61
+ return {
62
+ redirectUrl: "/error?message=installation-expired",
63
+ };
64
+
65
+ case GitHubAppErrorCodes.SESSION_EXPIRED:
66
+ console.error("Installation session expired");
67
+ console.error("User took too long to complete installation");
68
+ return {
69
+ redirectUrl: "/error?message=session-expired&retry=true",
70
+ };
71
+
72
+ case GitHubAppErrorCodes.JWT_SIGNING_FAILED:
73
+ console.error("Failed to sign JWT");
74
+ console.error("Check private key configuration");
75
+ return {
76
+ redirectUrl: "/error?message=configuration-error",
77
+ };
78
+
79
+ case GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED:
80
+ console.error("Failed to exchange JWT for installation token");
81
+ console.error("Possible causes:");
82
+ console.error("- Invalid GitHub App credentials");
83
+ console.error("- GitHub API is down");
84
+ console.error("- Network connectivity issues");
85
+ return {
86
+ redirectUrl: "/error?message=github-unavailable",
87
+ };
88
+
89
+ case GitHubAppErrorCodes.INSTALLATION_SUSPENDED:
90
+ console.error("Installation is suspended");
91
+ return {
92
+ redirectUrl: "/error?message=installation-suspended",
93
+ };
94
+
95
+ default:
96
+ console.error("Unhandled error:", error);
97
+ return {
98
+ redirectUrl: "/error?message=unknown-error",
99
+ };
100
+ }
101
+ },
102
+
103
+ // Handle webhook errors
104
+ onWebhookEvent: async ({ event, payload }, ctx) => {
105
+ try {
106
+ console.log(`Processing webhook: ${event}`);
107
+ // Process webhook...
108
+ } catch (error: any) {
109
+ console.error(`Webhook processing error:`, error);
110
+ // Log error but don't throw (to return 200 to GitHub)
111
+ }
112
+ },
113
+ }),
114
+ ],
115
+ });
116
+
117
+ await app.start();
118
+
119
+ console.log(`
120
+ =================================
121
+ Error Handling Example Started
122
+ =================================
123
+
124
+ This example demonstrates comprehensive error handling.
125
+
126
+ Try these scenarios:
127
+ 1. Invalid private key - Plugin will fail to start
128
+ 2. Expired session - Wait 10+ minutes before completing installation
129
+ 3. Invalid repository - Try to access non-existent repo
130
+ 4. Permission denied - Try to access repo without permission
131
+ 5. Rate limit - Make many API calls rapidly
132
+
133
+ =================================
134
+ `);
135
+ }
136
+
137
+ // Handler with comprehensive error handling
138
+ const GetRepositoryWithErrorHandling: GetHandler<any, any, { owner: string; repo: string }> = async ({
139
+ ctx,
140
+ params,
141
+ }) => {
142
+ const userId = "demo-user";
143
+ const { owner, repo } = params;
144
+
145
+ try {
146
+ // Step 1: Check if installation exists
147
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
148
+
149
+ if (!installation) {
150
+ return {
151
+ status: 404,
152
+ data: {
153
+ error: GitHubAppErrorCodes.INSTALLATION_NOT_FOUND,
154
+ message: "GitHub App is not installed",
155
+ action: "install",
156
+ installUrl: `/github-app/install?user_id=${userId}`,
157
+ },
158
+ };
159
+ }
160
+
161
+ // Step 2: Check if installation is suspended
162
+ if (installation.suspendedAt) {
163
+ return {
164
+ status: 403,
165
+ data: {
166
+ error: GitHubAppErrorCodes.INSTALLATION_SUSPENDED,
167
+ message: "Your GitHub App installation is suspended",
168
+ suspendedAt: installation.suspendedAt,
169
+ suspendedBy: installation.suspendedBy,
170
+ action: "contact-admin",
171
+ },
172
+ };
173
+ }
174
+
175
+ // Step 3: Check repository access
176
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
177
+
178
+ if (!hasAccess) {
179
+ return {
180
+ status: 403,
181
+ data: {
182
+ error: GitHubAppErrorCodes.REPOSITORY_NOT_ACCESSIBLE,
183
+ message: `You do not have access to ${owner}/${repo}`,
184
+ availableRepos: installation.repositories.map((r: any) => r.fullName),
185
+ action: "grant-access",
186
+ },
187
+ };
188
+ }
189
+
190
+ // Step 4: Get API client and fetch repository
191
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
192
+
193
+ try {
194
+ const repository = await client.getRepository(owner, repo);
195
+
196
+ return {
197
+ status: 200,
198
+ data: {
199
+ repository: {
200
+ name: repository.name,
201
+ fullName: repository.full_name,
202
+ description: repository.description,
203
+ url: repository.html_url,
204
+ },
205
+ },
206
+ };
207
+ } catch (apiError: any) {
208
+ // Handle GitHub API specific errors
209
+ return handleGitHubAPIError(apiError, owner, repo);
210
+ }
211
+ } catch (error: any) {
212
+ // Handle unexpected errors
213
+ console.error("Unexpected error:", error);
214
+ return {
215
+ status: 500,
216
+ data: {
217
+ error: GitHubAppErrorCodes.SERVER_ERROR,
218
+ message: "An unexpected error occurred",
219
+ details: process.env.NODE_ENV === "development" ? error.message : undefined,
220
+ },
221
+ };
222
+ }
223
+ };
224
+
225
+ export { GetRepositoryWithErrorHandling };
226
+
227
+ // Helper function to handle GitHub API errors
228
+ function handleGitHubAPIError(error: any, owner: string, repo: string) {
229
+ const status = error.response?.status;
230
+ const githubError = error.response?.data;
231
+
232
+ switch (status) {
233
+ case 401:
234
+ return {
235
+ status: 401,
236
+ data: {
237
+ error: "authentication-failed",
238
+ message: "GitHub authentication failed",
239
+ hint: "Token may be expired or invalid",
240
+ action: "retry",
241
+ },
242
+ };
243
+
244
+ case 403:
245
+ // Check if it's a rate limit error
246
+ if (error.response?.headers?.["x-ratelimit-remaining"] === "0") {
247
+ const resetTime = parseInt(error.response.headers["x-ratelimit-reset"], 10) * 1000;
248
+ const waitTime = Math.ceil((resetTime - Date.now()) / 1000 / 60);
249
+
250
+ return {
251
+ status: 429,
252
+ data: {
253
+ error: GitHubAppErrorCodes.API_RATE_LIMIT,
254
+ message: "GitHub API rate limit exceeded",
255
+ retryAfter: resetTime,
256
+ waitMinutes: waitTime,
257
+ action: "wait-and-retry",
258
+ },
259
+ };
260
+ }
261
+
262
+ // Permission denied
263
+ return {
264
+ status: 403,
265
+ data: {
266
+ error: "permission-denied",
267
+ message: "GitHub App does not have permission to access this resource",
268
+ hint: "Check GitHub App permissions in settings",
269
+ action: "update-permissions",
270
+ },
271
+ };
272
+
273
+ case 404:
274
+ return {
275
+ status: 404,
276
+ data: {
277
+ error: "repository-not-found",
278
+ message: `Repository ${owner}/${repo} not found`,
279
+ possibleCauses: ["Repository doesn't exist", "Repository is private", "Repository was deleted"],
280
+ action: "verify-repository",
281
+ },
282
+ };
283
+
284
+ case 422:
285
+ return {
286
+ status: 422,
287
+ data: {
288
+ error: "validation-failed",
289
+ message: "GitHub API validation failed",
290
+ details: githubError?.errors,
291
+ action: "check-request",
292
+ },
293
+ };
294
+
295
+ case 502:
296
+ case 503:
297
+ case 504:
298
+ return {
299
+ status: 503,
300
+ data: {
301
+ error: "github-unavailable",
302
+ message: "GitHub API is temporarily unavailable",
303
+ statusCode: status,
304
+ action: "retry-later",
305
+ },
306
+ };
307
+
308
+ default:
309
+ return {
310
+ status: 500,
311
+ data: {
312
+ error: GitHubAppErrorCodes.NETWORK_ERROR,
313
+ message: "Failed to communicate with GitHub API",
314
+ statusCode: status,
315
+ githubError: githubError?.message,
316
+ action: "retry",
317
+ },
318
+ };
319
+ }
320
+ }
321
+
322
+ // Handler demonstrating retry logic
323
+ const GetRepositoryWithRetry: GetHandler<any, any, { owner: string; repo: string }> = async ({ ctx, params }) => {
324
+ const userId = "demo-user";
325
+ const { owner, repo } = params;
326
+
327
+ const maxRetries = 3;
328
+ const retryDelay = 1000; // 1 second
329
+
330
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
331
+ try {
332
+ console.log(`Attempt ${attempt}/${maxRetries}`);
333
+
334
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
335
+ if (!installation) {
336
+ return {
337
+ status: 404,
338
+ data: { error: "installation-not-found" },
339
+ };
340
+ }
341
+
342
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
343
+ const repository = await client.getRepository(owner, repo);
344
+
345
+ return {
346
+ status: 200,
347
+ data: {
348
+ repository: {
349
+ name: repository.name,
350
+ fullName: repository.full_name,
351
+ },
352
+ attempts: attempt,
353
+ },
354
+ };
355
+ } catch (error: any) {
356
+ const isLastAttempt = attempt === maxRetries;
357
+
358
+ // Don't retry on client errors (4xx)
359
+ const status = error.response?.status;
360
+ if (status && status >= 400 && status < 500) {
361
+ return handleGitHubAPIError(error, owner, repo);
362
+ }
363
+
364
+ // Retry on server errors (5xx) or network errors
365
+ if (!isLastAttempt) {
366
+ console.log(`Attempt ${attempt} failed, retrying in ${retryDelay}ms...`);
367
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
368
+ continue;
369
+ }
370
+
371
+ // Max retries exceeded
372
+ return {
373
+ status: 500,
374
+ data: {
375
+ error: "max-retries-exceeded",
376
+ message: "Failed after multiple retry attempts",
377
+ attempts: maxRetries,
378
+ },
379
+ };
380
+ }
381
+ }
382
+
383
+ // Should never reach here
384
+ return {
385
+ status: 500,
386
+ data: { error: "unexpected-error" },
387
+ };
388
+ };
389
+
390
+ export { GetRepositoryWithRetry };
391
+
392
+ // Start application
393
+ start().catch((error) => {
394
+ console.error("Failed to start application:", error);
395
+ process.exit(1);
396
+ });