@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,480 @@
1
+ /**
2
+ * Repository Access Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Accessing user's GitHub repositories via API client
6
+ * - Fetching repository contents
7
+ * - Checking repository permissions
8
+ * - Listing branches and commits
9
+ * - Verifying user has access to specific repositories
10
+ */
11
+
12
+ import { FlinkApp, FlinkContext, GetHandler } from "@flink-app/flink";
13
+ import { githubAppPlugin } from "@flink-app/github-app-plugin";
14
+
15
+ interface AppContext extends FlinkContext {
16
+ plugins: {
17
+ githubApp: any;
18
+ };
19
+ session?: {
20
+ userId?: string;
21
+ };
22
+ }
23
+
24
+ async function start() {
25
+ const app = new FlinkApp<AppContext>({
26
+ name: "GitHub App Repository Access Example",
27
+ port: 3333,
28
+
29
+ db: {
30
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-repo-example",
31
+ },
32
+
33
+ plugins: [
34
+ githubAppPlugin({
35
+ appId: process.env.GITHUB_APP_ID!,
36
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
37
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
38
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
39
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
40
+
41
+ onInstallationSuccess: async ({ installationId, repositories, account }) => {
42
+ const userId = "demo-user-123";
43
+
44
+ console.log(`\nInstallation successful!`);
45
+ console.log(`Account: ${account.login}`);
46
+ console.log(`Repositories: ${repositories.length}`);
47
+
48
+ return { userId, redirectUrl: "/repos" };
49
+ },
50
+ }),
51
+ ],
52
+ });
53
+
54
+ await app.start();
55
+
56
+ console.log(`
57
+ =================================
58
+ Repository Access Example Started
59
+ =================================
60
+
61
+ Available endpoints:
62
+
63
+ 1. GET /repos/list
64
+ List all accessible repositories
65
+
66
+ 2. GET /repos/:owner/:repo
67
+ Get repository details
68
+
69
+ 3. GET /repos/:owner/:repo/contents?path=README.md
70
+ Get file contents
71
+
72
+ 4. GET /repos/:owner/:repo/check-access
73
+ Check if user has access to repository
74
+
75
+ 5. GET /repos/:owner/:repo/branches
76
+ List repository branches
77
+
78
+ 6. GET /repos/:owner/:repo/commits?branch=main
79
+ List recent commits
80
+
81
+ Install the GitHub App first:
82
+ http://localhost:3333/github-app/install?user_id=demo-user-123
83
+
84
+ =================================
85
+ `);
86
+ }
87
+
88
+ // Handler: List all accessible repositories
89
+ const ListRepositories: GetHandler = async ({ ctx }) => {
90
+ const userId = ctx.session?.userId || "demo-user-123";
91
+
92
+ try {
93
+ // Get user's installation
94
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
95
+
96
+ if (!installation) {
97
+ return {
98
+ status: 404,
99
+ data: {
100
+ error: "github-app-not-installed",
101
+ message: "Please install the GitHub App first",
102
+ },
103
+ };
104
+ }
105
+
106
+ // Get API client
107
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
108
+
109
+ // Fetch repositories
110
+ const repos = await client.getRepositories();
111
+
112
+ return {
113
+ status: 200,
114
+ data: {
115
+ installation: {
116
+ account: installation.accountLogin,
117
+ type: installation.accountType,
118
+ },
119
+ repositories: repos.map((repo: any) => ({
120
+ id: repo.id,
121
+ name: repo.name,
122
+ fullName: repo.full_name,
123
+ private: repo.private,
124
+ url: repo.html_url,
125
+ description: repo.description,
126
+ language: repo.language,
127
+ stargazersCount: repo.stargazers_count,
128
+ forksCount: repo.forks_count,
129
+ defaultBranch: repo.default_branch,
130
+ updatedAt: repo.updated_at,
131
+ })),
132
+ total: repos.length,
133
+ },
134
+ };
135
+ } catch (error: any) {
136
+ console.error("Failed to list repositories:", error);
137
+ return {
138
+ status: 500,
139
+ data: {
140
+ error: "repository-access-failed",
141
+ message: error.message,
142
+ },
143
+ };
144
+ }
145
+ };
146
+
147
+ export { ListRepositories };
148
+
149
+ // Handler: Get repository details
150
+ const GetRepository: GetHandler<any, any, { owner: string; repo: string }> = async ({ ctx, params }) => {
151
+ const userId = ctx.session?.userId || "demo-user-123";
152
+ const { owner, repo } = params;
153
+
154
+ try {
155
+ // Check if user has access
156
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
157
+
158
+ if (!hasAccess) {
159
+ return {
160
+ status: 403,
161
+ data: {
162
+ error: "access-denied",
163
+ message: "You do not have access to this repository",
164
+ },
165
+ };
166
+ }
167
+
168
+ // Get installation
169
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
170
+ if (!installation) {
171
+ return {
172
+ status: 404,
173
+ data: { error: "installation-not-found" },
174
+ };
175
+ }
176
+
177
+ // Get API client
178
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
179
+
180
+ // Fetch repository details
181
+ const repository = await client.getRepository(owner, repo);
182
+
183
+ return {
184
+ status: 200,
185
+ data: {
186
+ id: repository.id,
187
+ name: repository.name,
188
+ fullName: repository.full_name,
189
+ private: repository.private,
190
+ description: repository.description,
191
+ url: repository.html_url,
192
+ cloneUrl: repository.clone_url,
193
+ language: repository.language,
194
+ defaultBranch: repository.default_branch,
195
+ stargazersCount: repository.stargazers_count,
196
+ watchersCount: repository.watchers_count,
197
+ forksCount: repository.forks_count,
198
+ openIssuesCount: repository.open_issues_count,
199
+ license: repository.license?.name,
200
+ topics: repository.topics,
201
+ createdAt: repository.created_at,
202
+ updatedAt: repository.updated_at,
203
+ pushedAt: repository.pushed_at,
204
+ },
205
+ };
206
+ } catch (error: any) {
207
+ console.error("Failed to get repository:", error);
208
+ return {
209
+ status: 500,
210
+ data: {
211
+ error: "repository-access-failed",
212
+ message: error.message,
213
+ },
214
+ };
215
+ }
216
+ };
217
+
218
+ export { GetRepository };
219
+
220
+ // Handler: Get file contents
221
+ const GetContents: GetHandler<any, any, { owner: string; repo: string }, { path?: string }> = async ({
222
+ ctx,
223
+ params,
224
+ query,
225
+ }) => {
226
+ const userId = ctx.session?.userId || "demo-user-123";
227
+ const { owner, repo } = params;
228
+ const { path = "" } = query;
229
+
230
+ try {
231
+ // Check access
232
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
233
+
234
+ if (!hasAccess) {
235
+ return {
236
+ status: 403,
237
+ data: {
238
+ error: "access-denied",
239
+ message: "You do not have access to this repository",
240
+ },
241
+ };
242
+ }
243
+
244
+ // Get installation
245
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
246
+ if (!installation) {
247
+ return {
248
+ status: 404,
249
+ data: { error: "installation-not-found" },
250
+ };
251
+ }
252
+
253
+ // Get API client
254
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
255
+
256
+ // Fetch contents
257
+ const contents = await client.getContents(owner, repo, path);
258
+
259
+ // Handle directory vs file
260
+ if (Array.isArray(contents)) {
261
+ // Directory listing
262
+ return {
263
+ status: 200,
264
+ data: {
265
+ type: "directory",
266
+ path,
267
+ items: contents.map((item: any) => ({
268
+ name: item.name,
269
+ path: item.path,
270
+ type: item.type,
271
+ size: item.size,
272
+ sha: item.sha,
273
+ url: item.html_url,
274
+ })),
275
+ },
276
+ };
277
+ } else {
278
+ // Single file
279
+ const file = contents as any;
280
+ let content = file.content;
281
+
282
+ // Decode base64 content if present
283
+ if (file.encoding === "base64" && content) {
284
+ content = Buffer.from(content, "base64").toString("utf-8");
285
+ }
286
+
287
+ return {
288
+ status: 200,
289
+ data: {
290
+ type: "file",
291
+ name: file.name,
292
+ path: file.path,
293
+ sha: file.sha,
294
+ size: file.size,
295
+ encoding: file.encoding,
296
+ content,
297
+ url: file.html_url,
298
+ },
299
+ };
300
+ }
301
+ } catch (error: any) {
302
+ console.error("Failed to get contents:", error);
303
+ return {
304
+ status: 500,
305
+ data: {
306
+ error: "contents-access-failed",
307
+ message: error.message,
308
+ },
309
+ };
310
+ }
311
+ };
312
+
313
+ export { GetContents };
314
+
315
+ // Handler: Check repository access
316
+ const CheckAccess: GetHandler<any, any, { owner: string; repo: string }> = async ({ ctx, params }) => {
317
+ const userId = ctx.session?.userId || "demo-user-123";
318
+ const { owner, repo } = params;
319
+
320
+ try {
321
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
322
+
323
+ return {
324
+ status: 200,
325
+ data: {
326
+ owner,
327
+ repo,
328
+ hasAccess,
329
+ },
330
+ };
331
+ } catch (error: any) {
332
+ console.error("Failed to check access:", error);
333
+ return {
334
+ status: 500,
335
+ data: {
336
+ error: "access-check-failed",
337
+ message: error.message,
338
+ },
339
+ };
340
+ }
341
+ };
342
+
343
+ export { CheckAccess };
344
+
345
+ // Handler: List branches
346
+ const ListBranches: GetHandler<any, any, { owner: string; repo: string }> = async ({ ctx, params }) => {
347
+ const userId = ctx.session?.userId || "demo-user-123";
348
+ const { owner, repo } = params;
349
+
350
+ try {
351
+ // Check access
352
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
353
+
354
+ if (!hasAccess) {
355
+ return {
356
+ status: 403,
357
+ data: { error: "access-denied" },
358
+ };
359
+ }
360
+
361
+ // Get installation and client
362
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
363
+ if (!installation) {
364
+ return {
365
+ status: 404,
366
+ data: { error: "installation-not-found" },
367
+ };
368
+ }
369
+
370
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
371
+
372
+ // Fetch branches
373
+ const branches = await client.request("GET", `/repos/${owner}/${repo}/branches`);
374
+
375
+ return {
376
+ status: 200,
377
+ data: {
378
+ branches: branches.map((branch: any) => ({
379
+ name: branch.name,
380
+ protected: branch.protected,
381
+ commit: {
382
+ sha: branch.commit.sha,
383
+ url: branch.commit.url,
384
+ },
385
+ })),
386
+ total: branches.length,
387
+ },
388
+ };
389
+ } catch (error: any) {
390
+ console.error("Failed to list branches:", error);
391
+ return {
392
+ status: 500,
393
+ data: {
394
+ error: "branches-access-failed",
395
+ message: error.message,
396
+ },
397
+ };
398
+ }
399
+ };
400
+
401
+ export { ListBranches };
402
+
403
+ // Handler: List commits
404
+ const ListCommits: GetHandler<any, any, { owner: string; repo: string }, { branch?: string; limit?: string }> = async ({
405
+ ctx,
406
+ params,
407
+ query,
408
+ }) => {
409
+ const userId = ctx.session?.userId || "demo-user-123";
410
+ const { owner, repo } = params;
411
+ const { branch = "main", limit = "10" } = query;
412
+
413
+ try {
414
+ // Check access
415
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
416
+
417
+ if (!hasAccess) {
418
+ return {
419
+ status: 403,
420
+ data: { error: "access-denied" },
421
+ };
422
+ }
423
+
424
+ // Get installation and client
425
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
426
+ if (!installation) {
427
+ return {
428
+ status: 404,
429
+ data: { error: "installation-not-found" },
430
+ };
431
+ }
432
+
433
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
434
+
435
+ // Fetch commits
436
+ const commits = await client.request("GET", `/repos/${owner}/${repo}/commits`, {
437
+ sha: branch,
438
+ per_page: parseInt(limit, 10),
439
+ });
440
+
441
+ return {
442
+ status: 200,
443
+ data: {
444
+ branch,
445
+ commits: commits.map((commit: any) => ({
446
+ sha: commit.sha,
447
+ message: commit.commit.message,
448
+ author: {
449
+ name: commit.commit.author.name,
450
+ email: commit.commit.author.email,
451
+ date: commit.commit.author.date,
452
+ },
453
+ committer: {
454
+ name: commit.commit.committer.name,
455
+ date: commit.commit.committer.date,
456
+ },
457
+ url: commit.html_url,
458
+ })),
459
+ total: commits.length,
460
+ },
461
+ };
462
+ } catch (error: any) {
463
+ console.error("Failed to list commits:", error);
464
+ return {
465
+ status: 500,
466
+ data: {
467
+ error: "commits-access-failed",
468
+ message: error.message,
469
+ },
470
+ };
471
+ }
472
+ };
473
+
474
+ export { ListCommits };
475
+
476
+ // Start application
477
+ start().catch((error) => {
478
+ console.error("Failed to start application:", error);
479
+ process.exit(1);
480
+ });