@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,246 @@
1
+ /**
2
+ * Basic GitHub App Installation Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Setting up GitHub App Plugin with standalone authentication
6
+ * - Initiating GitHub App installation
7
+ * - Handling installation success callback
8
+ * - Linking installation to user account
9
+ * - Accessing user's GitHub repositories
10
+ *
11
+ * Note: This example uses standalone authentication (no JWT Auth Plugin required)
12
+ */
13
+
14
+ import { FlinkApp, FlinkContext, FlinkRepo } from "@flink-app/flink";
15
+ import { githubAppPlugin } from "@flink-app/github-app-plugin";
16
+
17
+ // Define User schema
18
+ interface User {
19
+ _id?: string;
20
+ email: string;
21
+ name: string;
22
+ githubInstallations: number[]; // Array of installation IDs
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ }
26
+
27
+ // User repository
28
+ class UserRepo extends FlinkRepo<AppContext, User> {
29
+ async findByEmail(email: string) {
30
+ return this.getOne({ email });
31
+ }
32
+
33
+ async addGitHubInstallation(userId: string, installationId: number) {
34
+ const user = await this.getById(userId);
35
+ if (!user) {
36
+ throw new Error("User not found");
37
+ }
38
+
39
+ const installations = user.githubInstallations || [];
40
+ if (!installations.includes(installationId)) {
41
+ await this.updateOne(userId, {
42
+ githubInstallations: [...installations, installationId],
43
+ });
44
+ }
45
+ }
46
+ }
47
+
48
+ // Application context
49
+ interface AppContext extends FlinkContext {
50
+ repos: {
51
+ userRepo: UserRepo;
52
+ };
53
+ plugins: {
54
+ githubApp: any;
55
+ };
56
+ // Custom session property for this example
57
+ session?: {
58
+ userId?: string;
59
+ email?: string;
60
+ };
61
+ }
62
+
63
+ async function start() {
64
+ const app = new FlinkApp<AppContext>({
65
+ name: "GitHub App Basic Installation Example",
66
+ port: 3333,
67
+
68
+ db: {
69
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-example",
70
+ },
71
+
72
+ plugins: [
73
+ githubAppPlugin({
74
+ // GitHub App credentials (required)
75
+ appId: process.env.GITHUB_APP_ID!,
76
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
77
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
78
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
79
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
80
+
81
+ // Optional: App slug for installation URL
82
+ appSlug: "my-flink-app",
83
+
84
+ // Optional: Base URL (default: https://api.github.com)
85
+ baseUrl: process.env.GITHUB_API_URL || "https://api.github.com",
86
+
87
+ // Callback after successful installation
88
+ onInstallationSuccess: async ({ installationId, repositories, account, permissions, events }, ctx) => {
89
+ console.log(`\n=== Installation Success ===`);
90
+ console.log(`Installation ID: ${installationId}`);
91
+ console.log(`Account: ${account.login} (${account.type})`);
92
+ console.log(`Repositories: ${repositories.length}`);
93
+ console.log(`Permissions:`, permissions);
94
+ console.log(`Events subscribed:`, events);
95
+
96
+ // Get userId from your authentication system
97
+ // In this example, we use a mock session
98
+ const userId = ctx.session?.userId || "demo-user-123";
99
+
100
+ // Link installation to user in your database
101
+ try {
102
+ await ctx.repos.userRepo.addGitHubInstallation(userId, installationId);
103
+ console.log(`Installation ${installationId} linked to user ${userId}`);
104
+ } catch (error) {
105
+ console.error("Failed to link installation:", error);
106
+ }
107
+
108
+ // List repositories the user granted access to
109
+ console.log("\nRepositories granted access:");
110
+ repositories.forEach((repo) => {
111
+ console.log(` - ${repo.fullName} (${repo.private ? "private" : "public"})`);
112
+ });
113
+
114
+ // Redirect user back to dashboard
115
+ return {
116
+ userId,
117
+ redirectUrl: "/dashboard?installation=success",
118
+ };
119
+ },
120
+
121
+ // Optional: Handle installation errors
122
+ onInstallationError: async ({ error, installationId }) => {
123
+ console.error(`\n=== Installation Error ===`);
124
+ console.error(`Error Code: ${error.code}`);
125
+ console.error(`Message: ${error.message}`);
126
+ if (installationId) {
127
+ console.error(`Installation ID: ${installationId}`);
128
+ }
129
+
130
+ return {
131
+ redirectUrl: "/error?message=installation-failed",
132
+ };
133
+ },
134
+
135
+ // Optional: Log webhook events for debugging
136
+ logWebhookEvents: false,
137
+ }),
138
+ ],
139
+ });
140
+
141
+ // Create demo user if doesn't exist
142
+ await app.onReady(async () => {
143
+ const demoUser = await app.ctx.repos.userRepo.findByEmail("demo@example.com");
144
+
145
+ if (!demoUser) {
146
+ await app.ctx.repos.userRepo.create({
147
+ email: "demo@example.com",
148
+ name: "Demo User",
149
+ githubInstallations: [],
150
+ createdAt: new Date(),
151
+ updatedAt: new Date(),
152
+ });
153
+ console.log("Demo user created");
154
+ }
155
+ });
156
+
157
+ await app.start();
158
+
159
+ console.log(`
160
+ =================================
161
+ GitHub App Basic Example Started
162
+ =================================
163
+
164
+ To install the GitHub App:
165
+
166
+ 1. Navigate to: http://localhost:3333/github-app/install?user_id=demo-user-123
167
+ 2. Authorize the app on GitHub
168
+ 3. Select repositories to grant access
169
+ 4. Complete installation
170
+
171
+ To access user's repositories via API:
172
+
173
+ Use the context API in your handlers:
174
+
175
+ const installation = await ctx.plugins.githubApp.getInstallation('demo-user-123');
176
+ if (installation) {
177
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
178
+ const repos = await client.getRepositories();
179
+ console.log('User repositories:', repos);
180
+ }
181
+
182
+ =================================
183
+ `);
184
+ }
185
+
186
+ // Handle errors
187
+ start().catch((error) => {
188
+ console.error("Failed to start application:", error);
189
+ process.exit(1);
190
+ });
191
+
192
+ // Example handler to list user's GitHub repositories
193
+ // Save this in src/handlers/github/GetUserRepos.ts
194
+
195
+ /*
196
+ import { GetHandler } from "@flink-app/flink";
197
+
198
+ const GetUserRepos: GetHandler = async ({ ctx }) => {
199
+ // Get userId from your auth system
200
+ const userId = ctx.session?.userId || 'demo-user-123';
201
+
202
+ // Get user's installation
203
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
204
+
205
+ if (!installation) {
206
+ return {
207
+ status: 404,
208
+ data: {
209
+ error: 'GitHub App not installed',
210
+ message: 'Please install the GitHub App first'
211
+ }
212
+ };
213
+ }
214
+
215
+ // Get API client
216
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
217
+
218
+ // Fetch repositories
219
+ const repos = await client.getRepositories();
220
+
221
+ return {
222
+ status: 200,
223
+ data: {
224
+ installation: {
225
+ account: installation.accountLogin,
226
+ type: installation.accountType,
227
+ installedAt: installation.createdAt
228
+ },
229
+ repositories: repos.map(repo => ({
230
+ id: repo.id,
231
+ name: repo.name,
232
+ fullName: repo.full_name,
233
+ private: repo.private,
234
+ url: repo.html_url,
235
+ description: repo.description
236
+ }))
237
+ }
238
+ };
239
+ };
240
+
241
+ export default GetUserRepos;
242
+
243
+ export const Route = {
244
+ path: '/github/repos'
245
+ };
246
+ */
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Create GitHub Issue Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Creating GitHub issues with permission checks
6
+ * - Using the GitHub API client
7
+ * - Verifying repository access before operations
8
+ * - Adding labels and assignees to issues
9
+ * - Error handling for permission failures
10
+ */
11
+
12
+ import { FlinkApp, FlinkContext, PostHandler } 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
+ interface CreateIssueRequest {
25
+ owner: string;
26
+ repo: string;
27
+ title: string;
28
+ body?: string;
29
+ labels?: string[];
30
+ assignees?: string[];
31
+ }
32
+
33
+ async function start() {
34
+ const app = new FlinkApp<AppContext>({
35
+ name: "GitHub App Create Issue Example",
36
+ port: 3333,
37
+
38
+ db: {
39
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-issue-example",
40
+ },
41
+
42
+ plugins: [
43
+ githubAppPlugin({
44
+ appId: process.env.GITHUB_APP_ID!,
45
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
46
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
47
+ clientId: process.env.GITHUB_APP_CLIENT_ID!,
48
+ clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
49
+
50
+ onInstallationSuccess: async ({ installationId, repositories, account, permissions }) => {
51
+ const userId = "demo-user-123";
52
+
53
+ console.log(`\nInstallation successful!`);
54
+ console.log(`Account: ${account.login}`);
55
+ console.log(`Repositories: ${repositories.length}`);
56
+ console.log(`Permissions:`, permissions);
57
+
58
+ // Check if we have issues permission
59
+ if (permissions.issues !== "write") {
60
+ console.warn(`Warning: Issues permission is ${permissions.issues}, may not be able to create issues`);
61
+ }
62
+
63
+ return { userId, redirectUrl: "/dashboard" };
64
+ },
65
+ }),
66
+ ],
67
+ });
68
+
69
+ await app.start();
70
+
71
+ console.log(`
72
+ =================================
73
+ Create Issue Example Started
74
+ =================================
75
+
76
+ Available endpoints:
77
+
78
+ POST /issues/create
79
+ Body: {
80
+ "owner": "owner-name",
81
+ "repo": "repo-name",
82
+ "title": "Issue title",
83
+ "body": "Issue description",
84
+ "labels": ["bug", "help wanted"],
85
+ "assignees": ["username"]
86
+ }
87
+
88
+ Example curl:
89
+ curl -X POST http://localhost:3333/issues/create \\
90
+ -H "Content-Type: application/json" \\
91
+ -d '{
92
+ "owner": "facebook",
93
+ "repo": "react",
94
+ "title": "Bug Report",
95
+ "body": "Found a bug...",
96
+ "labels": ["bug"]
97
+ }'
98
+
99
+ Install the GitHub App first:
100
+ http://localhost:3333/github-app/install?user_id=demo-user-123
101
+
102
+ =================================
103
+ `);
104
+ }
105
+
106
+ // Handler: Create GitHub issue
107
+ const CreateIssue: PostHandler<CreateIssueRequest> = async ({ ctx, body }) => {
108
+ const userId = ctx.session?.userId || "demo-user-123";
109
+
110
+ // Validate request body
111
+ if (!body.owner || !body.repo || !body.title) {
112
+ return {
113
+ status: 400,
114
+ data: {
115
+ error: "invalid-request",
116
+ message: "owner, repo, and title are required",
117
+ },
118
+ };
119
+ }
120
+
121
+ const { owner, repo, title, body: issueBody, labels, assignees } = body;
122
+
123
+ try {
124
+ // Step 1: Check if user has access to repository
125
+ console.log(`Checking access to ${owner}/${repo}...`);
126
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
127
+
128
+ if (!hasAccess) {
129
+ return {
130
+ status: 403,
131
+ data: {
132
+ error: "access-denied",
133
+ message: `You do not have access to repository ${owner}/${repo}`,
134
+ hint: "Install the GitHub App and grant access to this repository",
135
+ },
136
+ };
137
+ }
138
+
139
+ // Step 2: Get user's installation
140
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
141
+
142
+ if (!installation) {
143
+ return {
144
+ status: 404,
145
+ data: {
146
+ error: "installation-not-found",
147
+ message: "GitHub App is not installed for your account",
148
+ hint: "Install the GitHub App first",
149
+ },
150
+ };
151
+ }
152
+
153
+ // Step 3: Get API client
154
+ console.log(`Getting API client for installation ${installation.installationId}...`);
155
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
156
+
157
+ // Step 4: Create the issue
158
+ console.log(`Creating issue in ${owner}/${repo}...`);
159
+ const issue = await client.createIssue(owner, repo, {
160
+ title,
161
+ body: issueBody || "",
162
+ });
163
+
164
+ console.log(`Issue created: #${issue.number}`);
165
+
166
+ // Step 5: Add labels if provided
167
+ if (labels && labels.length > 0) {
168
+ console.log(`Adding labels: ${labels.join(", ")}`);
169
+ try {
170
+ await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/labels`, {
171
+ labels,
172
+ });
173
+ } catch (error) {
174
+ console.warn(`Failed to add labels:`, error);
175
+ // Continue anyway
176
+ }
177
+ }
178
+
179
+ // Step 6: Add assignees if provided
180
+ if (assignees && assignees.length > 0) {
181
+ console.log(`Adding assignees: ${assignees.join(", ")}`);
182
+ try {
183
+ await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/assignees`, {
184
+ assignees,
185
+ });
186
+ } catch (error) {
187
+ console.warn(`Failed to add assignees:`, error);
188
+ // Continue anyway
189
+ }
190
+ }
191
+
192
+ // Return success response
193
+ return {
194
+ status: 201,
195
+ data: {
196
+ success: true,
197
+ issue: {
198
+ id: issue.id,
199
+ number: issue.number,
200
+ title: issue.title,
201
+ body: issue.body,
202
+ state: issue.state,
203
+ url: issue.html_url,
204
+ createdAt: issue.created_at,
205
+ labels: issue.labels,
206
+ assignees: issue.assignees,
207
+ },
208
+ message: `Issue #${issue.number} created successfully`,
209
+ },
210
+ };
211
+ } catch (error: any) {
212
+ console.error("Failed to create issue:", error);
213
+
214
+ // Handle specific GitHub API errors
215
+ if (error.response?.status === 403) {
216
+ return {
217
+ status: 403,
218
+ data: {
219
+ error: "permission-denied",
220
+ message: "GitHub App does not have permission to create issues in this repository",
221
+ hint: "Check that the GitHub App has 'issues: write' permission",
222
+ },
223
+ };
224
+ }
225
+
226
+ if (error.response?.status === 404) {
227
+ return {
228
+ status: 404,
229
+ data: {
230
+ error: "repository-not-found",
231
+ message: `Repository ${owner}/${repo} not found or not accessible`,
232
+ },
233
+ };
234
+ }
235
+
236
+ if (error.response?.status === 410) {
237
+ return {
238
+ status: 410,
239
+ data: {
240
+ error: "issues-disabled",
241
+ message: "Issues are disabled for this repository",
242
+ },
243
+ };
244
+ }
245
+
246
+ // Generic error
247
+ return {
248
+ status: 500,
249
+ data: {
250
+ error: "issue-creation-failed",
251
+ message: error.message || "Failed to create issue",
252
+ details: error.response?.data,
253
+ },
254
+ };
255
+ }
256
+ };
257
+
258
+ export { CreateIssue };
259
+
260
+ // Handler: Create issue from template
261
+ const CreateIssueFromTemplate: PostHandler<{
262
+ owner: string;
263
+ repo: string;
264
+ template: "bug" | "feature" | "question";
265
+ additionalInfo?: Record<string, any>;
266
+ }> = async ({ ctx, body }) => {
267
+ const userId = ctx.session?.userId || "demo-user-123";
268
+ const { owner, repo, template, additionalInfo = {} } = body;
269
+
270
+ // Define issue templates
271
+ const templates = {
272
+ bug: {
273
+ title: `[BUG] ${additionalInfo.summary || "Issue summary"}`,
274
+ body: `## Bug Description
275
+ ${additionalInfo.description || "Describe the bug..."}
276
+
277
+ ## Steps to Reproduce
278
+ ${additionalInfo.steps || "1. ...\\n2. ...\\n3. ..."}
279
+
280
+ ## Expected Behavior
281
+ ${additionalInfo.expected || "What should happen..."}
282
+
283
+ ## Actual Behavior
284
+ ${additionalInfo.actual || "What actually happens..."}
285
+
286
+ ## Environment
287
+ ${additionalInfo.environment || "- OS: ...\\n- Browser: ...\\n- Version: ..."}`,
288
+ labels: ["bug"],
289
+ },
290
+ feature: {
291
+ title: `[FEATURE] ${additionalInfo.summary || "Feature request"}`,
292
+ body: `## Feature Description
293
+ ${additionalInfo.description || "Describe the feature..."}
294
+
295
+ ## Use Case
296
+ ${additionalInfo.useCase || "Why is this feature needed?"}
297
+
298
+ ## Proposed Solution
299
+ ${additionalInfo.solution || "How should it work?"}
300
+
301
+ ## Alternatives Considered
302
+ ${additionalInfo.alternatives || "What alternatives have been considered?"}`,
303
+ labels: ["enhancement"],
304
+ },
305
+ question: {
306
+ title: `[QUESTION] ${additionalInfo.summary || "Question"}`,
307
+ body: `## Question
308
+ ${additionalInfo.question || "Your question..."}
309
+
310
+ ## Context
311
+ ${additionalInfo.context || "Additional context..."}`,
312
+ labels: ["question"],
313
+ },
314
+ };
315
+
316
+ const issueTemplate = templates[template];
317
+
318
+ if (!issueTemplate) {
319
+ return {
320
+ status: 400,
321
+ data: {
322
+ error: "invalid-template",
323
+ message: "Template must be one of: bug, feature, question",
324
+ },
325
+ };
326
+ }
327
+
328
+ try {
329
+ // Check access
330
+ const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
331
+ if (!hasAccess) {
332
+ return {
333
+ status: 403,
334
+ data: { error: "access-denied" },
335
+ };
336
+ }
337
+
338
+ // Get installation and client
339
+ const installation = await ctx.plugins.githubApp.getInstallation(userId);
340
+ if (!installation) {
341
+ return {
342
+ status: 404,
343
+ data: { error: "installation-not-found" },
344
+ };
345
+ }
346
+
347
+ const client = await ctx.plugins.githubApp.getClient(installation.installationId);
348
+
349
+ // Create issue
350
+ const issue = await client.createIssue(owner, repo, {
351
+ title: issueTemplate.title,
352
+ body: issueTemplate.body,
353
+ });
354
+
355
+ // Add labels
356
+ if (issueTemplate.labels.length > 0) {
357
+ await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/labels`, {
358
+ labels: issueTemplate.labels,
359
+ });
360
+ }
361
+
362
+ return {
363
+ status: 201,
364
+ data: {
365
+ success: true,
366
+ issue: {
367
+ number: issue.number,
368
+ title: issue.title,
369
+ url: issue.html_url,
370
+ template,
371
+ },
372
+ },
373
+ };
374
+ } catch (error: any) {
375
+ console.error("Failed to create issue from template:", error);
376
+ return {
377
+ status: 500,
378
+ data: {
379
+ error: "issue-creation-failed",
380
+ message: error.message,
381
+ },
382
+ };
383
+ }
384
+ };
385
+
386
+ export { CreateIssueFromTemplate };
387
+
388
+ // Start application
389
+ start().catch((error) => {
390
+ console.error("Failed to start application:", error);
391
+ process.exit(1);
392
+ });