@oneuptime/common 9.2.16 → 9.2.17

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 (115) hide show
  1. package/Models/DatabaseModels/CodeRepository.ts +664 -0
  2. package/Models/DatabaseModels/Index.ts +8 -0
  3. package/Models/DatabaseModels/LlmLog.ts +818 -0
  4. package/Models/DatabaseModels/LlmProvider.ts +21 -0
  5. package/Models/DatabaseModels/Project.ts +206 -0
  6. package/Models/DatabaseModels/ServiceCatalogCodeRepository.ts +549 -0
  7. package/Server/API/AIBillingAPI.ts +126 -0
  8. package/Server/API/GitHubAPI.ts +360 -0
  9. package/Server/API/IncidentAPI.ts +126 -0
  10. package/Server/EnvironmentConfig.ts +44 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.ts +79 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.ts +75 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.ts +32 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.ts +69 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.ts +111 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.ts +39 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  18. package/Server/Services/AIBillingService.ts +247 -0
  19. package/Server/Services/AIService.ts +239 -0
  20. package/Server/Services/CodeRepositoryService.ts +10 -0
  21. package/Server/Services/IncidentService.ts +89 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/LlmLogService.ts +14 -0
  24. package/Server/Services/LlmProviderService.ts +58 -0
  25. package/Server/Services/ServiceCatalogCodeRepositoryService.ts +55 -0
  26. package/Server/Utils/AI/IncidentAIContextBuilder.ts +498 -0
  27. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +226 -0
  28. package/Server/Utils/LLM/LLMService.ts +276 -0
  29. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +166 -0
  30. package/Server/Utils/Workspace/Slack/Slack.ts +134 -0
  31. package/Server/Utils/Workspace/Workspace.ts +126 -0
  32. package/Types/CodeRepository/CodeRepositoryType.ts +1 -1
  33. package/Types/LlmLogStatus.ts +7 -0
  34. package/Types/Permission.ts +87 -0
  35. package/Types/ServiceCatalog/CodeRepositoryImprovementAction.ts +9 -0
  36. package/UI/Components/AI/AILoader.tsx +95 -0
  37. package/UI/Components/AI/GenerateFromAIModal.tsx +295 -0
  38. package/UI/Components/Modal/Modal.tsx +6 -1
  39. package/build/dist/Models/DatabaseModels/CodeRepository.js +689 -0
  40. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -0
  41. package/build/dist/Models/DatabaseModels/Index.js +7 -0
  42. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  43. package/build/dist/Models/DatabaseModels/LlmLog.js +856 -0
  44. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -0
  45. package/build/dist/Models/DatabaseModels/LlmProvider.js +22 -0
  46. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  47. package/build/dist/Models/DatabaseModels/Project.js +220 -0
  48. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js +565 -0
  50. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js.map +1 -0
  51. package/build/dist/Server/API/AIBillingAPI.js +58 -0
  52. package/build/dist/Server/API/AIBillingAPI.js.map +1 -0
  53. package/build/dist/Server/API/GitHubAPI.js +207 -0
  54. package/build/dist/Server/API/GitHubAPI.js.map +1 -0
  55. package/build/dist/Server/API/IncidentAPI.js +84 -1
  56. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  57. package/build/dist/Server/EnvironmentConfig.js +31 -0
  58. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js +34 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js.map +1 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js +32 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js.map +1 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js +38 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js.map +1 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js +30 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js.map +1 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js +44 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js.map +1 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js +22 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  73. package/build/dist/Server/Services/AIBillingService.js +187 -0
  74. package/build/dist/Server/Services/AIBillingService.js.map +1 -0
  75. package/build/dist/Server/Services/AIService.js +185 -0
  76. package/build/dist/Server/Services/AIService.js.map +1 -0
  77. package/build/dist/Server/Services/CodeRepositoryService.js +9 -0
  78. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -0
  79. package/build/dist/Server/Services/IncidentService.js +61 -0
  80. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  81. package/build/dist/Server/Services/Index.js +2 -0
  82. package/build/dist/Server/Services/Index.js.map +1 -1
  83. package/build/dist/Server/Services/LlmLogService.js +13 -0
  84. package/build/dist/Server/Services/LlmLogService.js.map +1 -0
  85. package/build/dist/Server/Services/LlmProviderService.js +65 -0
  86. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  87. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js +54 -0
  88. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js.map +1 -0
  89. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +408 -0
  90. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -0
  91. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +163 -0
  92. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  93. package/build/dist/Server/Utils/LLM/LLMService.js +225 -0
  94. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -0
  95. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +110 -0
  96. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  97. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +89 -0
  98. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  99. package/build/dist/Server/Utils/Workspace/Workspace.js +80 -0
  100. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  101. package/build/dist/Types/CodeRepository/CodeRepositoryType.js +1 -1
  102. package/build/dist/Types/CodeRepository/CodeRepositoryType.js.map +1 -1
  103. package/build/dist/Types/LlmLogStatus.js +8 -0
  104. package/build/dist/Types/LlmLogStatus.js.map +1 -0
  105. package/build/dist/Types/Permission.js +74 -0
  106. package/build/dist/Types/Permission.js.map +1 -1
  107. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js +10 -0
  108. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js.map +1 -0
  109. package/build/dist/UI/Components/AI/AILoader.js +64 -0
  110. package/build/dist/UI/Components/AI/AILoader.js.map +1 -0
  111. package/build/dist/UI/Components/AI/GenerateFromAIModal.js +207 -0
  112. package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -0
  113. package/build/dist/UI/Components/Modal/Modal.js +6 -1
  114. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,360 @@
1
+ import Express, {
2
+ ExpressRequest,
3
+ ExpressResponse,
4
+ ExpressRouter,
5
+ } from "../Utils/Express";
6
+ import Response from "../Utils/Response";
7
+ import BadDataException from "../../Types/Exception/BadDataException";
8
+ import logger from "../Utils/Logger";
9
+ import { JSONObject } from "../../Types/JSON";
10
+ import { DashboardClientUrl, GitHubAppClientId } from "../EnvironmentConfig";
11
+ import ObjectID from "../../Types/ObjectID";
12
+ import GitHubUtil, {
13
+ GitHubRepository,
14
+ } from "../Utils/CodeRepository/GitHub/GitHub";
15
+ import CodeRepositoryService from "../Services/CodeRepositoryService";
16
+ import CodeRepository from "../../Models/DatabaseModels/CodeRepository";
17
+ import CodeRepositoryType from "../../Types/CodeRepository/CodeRepositoryType";
18
+ import URL from "../../Types/API/URL";
19
+ import UserMiddleware from "../Middleware/UserAuthorization";
20
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
21
+
22
+ export default class GitHubAPI {
23
+ public getRouter(): ExpressRouter {
24
+ const router: ExpressRouter = Express.getRouter();
25
+
26
+ /*
27
+ * GitHub App installation callback
28
+ * This is called after a user installs the GitHub App
29
+ * The state parameter contains base64 encoded JSON with projectId and userId
30
+ */
31
+ router.get(
32
+ "/github/auth/callback",
33
+ async (req: ExpressRequest, res: ExpressResponse) => {
34
+ try {
35
+ // GitHub sends state parameter back which contains projectId and userId
36
+ const state: string | undefined = req.query["state"]?.toString();
37
+
38
+ if (!state) {
39
+ return Response.sendErrorResponse(
40
+ req,
41
+ res,
42
+ new BadDataException("State parameter is required"),
43
+ );
44
+ }
45
+
46
+ // Decode the state parameter to get projectId and userId
47
+ let projectId: string | undefined;
48
+ let userId: string | undefined;
49
+
50
+ try {
51
+ const decodedState: { projectId?: string; userId?: string } =
52
+ JSON.parse(Buffer.from(state, "base64").toString("utf-8"));
53
+ projectId = decodedState.projectId;
54
+ userId = decodedState.userId;
55
+ } catch {
56
+ return Response.sendErrorResponse(
57
+ req,
58
+ res,
59
+ new BadDataException("Invalid state parameter"),
60
+ );
61
+ }
62
+
63
+ if (!projectId) {
64
+ return Response.sendErrorResponse(
65
+ req,
66
+ res,
67
+ new BadDataException("Project ID is required in state"),
68
+ );
69
+ }
70
+
71
+ if (!userId) {
72
+ return Response.sendErrorResponse(
73
+ req,
74
+ res,
75
+ new BadDataException("User ID is required in state"),
76
+ );
77
+ }
78
+
79
+ // GitHub sends installation_id in query params after app installation
80
+ const installationId: string | undefined =
81
+ req.query["installation_id"]?.toString();
82
+
83
+ if (!installationId) {
84
+ return Response.sendErrorResponse(
85
+ req,
86
+ res,
87
+ new BadDataException(
88
+ "Installation ID is required. Please install the GitHub App first.",
89
+ ),
90
+ );
91
+ }
92
+
93
+ /*
94
+ * Store the installation ID - we'll create repositories when user selects them
95
+ * For now, redirect back to dashboard with installation ID
96
+ */
97
+ const redirectUrl: string = `${DashboardClientUrl.toString()}/dashboard/${projectId}/code-repository?installation_id=${installationId}`;
98
+
99
+ return Response.redirect(req, res, URL.fromString(redirectUrl));
100
+ } catch (error) {
101
+ logger.error("GitHub Auth Callback Error:");
102
+ logger.error(error);
103
+ return Response.sendErrorResponse(
104
+ req,
105
+ res,
106
+ error instanceof Error
107
+ ? new BadDataException(error.message)
108
+ : new BadDataException("An error occurred"),
109
+ );
110
+ }
111
+ },
112
+ );
113
+
114
+ // Initiate GitHub App installation
115
+ router.get(
116
+ "/github/auth/install",
117
+ async (req: ExpressRequest, res: ExpressResponse) => {
118
+ try {
119
+ if (!GitHubAppClientId) {
120
+ return Response.sendErrorResponse(
121
+ req,
122
+ res,
123
+ new BadDataException(
124
+ "GitHub App is not configured. Please set GITHUB_APP_CLIENT_ID.",
125
+ ),
126
+ );
127
+ }
128
+
129
+ const projectId: string | undefined =
130
+ req.query["projectId"]?.toString();
131
+ const userId: string | undefined = req.query["userId"]?.toString();
132
+
133
+ if (!projectId || !userId) {
134
+ return Response.sendErrorResponse(
135
+ req,
136
+ res,
137
+ new BadDataException("Project ID and User ID are required"),
138
+ );
139
+ }
140
+
141
+ /*
142
+ * Redirect to GitHub App installation page
143
+ * The state parameter helps us track the installation
144
+ */
145
+ const state: string = Buffer.from(
146
+ JSON.stringify({ projectId, userId }),
147
+ ).toString("base64");
148
+
149
+ const installUrl: string = `https://github.com/apps/${GitHubAppClientId}/installations/new?state=${state}`;
150
+
151
+ return Response.redirect(req, res, URL.fromString(installUrl));
152
+ } catch (error) {
153
+ logger.error("GitHub Install Redirect Error:");
154
+ logger.error(error);
155
+ return Response.sendErrorResponse(
156
+ req,
157
+ res,
158
+ error instanceof Error
159
+ ? new BadDataException(error.message)
160
+ : new BadDataException("An error occurred"),
161
+ );
162
+ }
163
+ },
164
+ );
165
+
166
+ // List repositories for an installation
167
+ router.get(
168
+ "/github/repositories/:projectId/:installationId",
169
+ UserMiddleware.getUserMiddleware,
170
+ async (req: ExpressRequest, res: ExpressResponse) => {
171
+ try {
172
+ const installationId: string | undefined =
173
+ req.params["installationId"]?.toString();
174
+
175
+ if (!installationId) {
176
+ return Response.sendErrorResponse(
177
+ req,
178
+ res,
179
+ new BadDataException("Installation ID is required"),
180
+ );
181
+ }
182
+
183
+ const repositories: Array<GitHubRepository> =
184
+ await GitHubUtil.listRepositoriesForInstallation(installationId);
185
+
186
+ return Response.sendJsonObjectResponse(req, res, {
187
+ repositories: repositories as unknown,
188
+ } as JSONObject);
189
+ } catch (error) {
190
+ logger.error("GitHub List Repositories Error:");
191
+ logger.error(error);
192
+ return Response.sendErrorResponse(
193
+ req,
194
+ res,
195
+ error instanceof Error
196
+ ? new BadDataException(error.message)
197
+ : new BadDataException("An error occurred"),
198
+ );
199
+ }
200
+ },
201
+ );
202
+
203
+ // Connect a repository to a project
204
+ router.post(
205
+ "/github/repository/connect",
206
+ UserMiddleware.getUserMiddleware,
207
+ async (req: ExpressRequest, res: ExpressResponse) => {
208
+ try {
209
+ const body: JSONObject = req.body;
210
+
211
+ const projectId: string | undefined = body["projectId"]?.toString();
212
+ const installationId: string | undefined =
213
+ body["installationId"]?.toString();
214
+ const repositoryName: string | undefined =
215
+ body["repositoryName"]?.toString();
216
+ const organizationName: string | undefined =
217
+ body["organizationName"]?.toString();
218
+ const name: string | undefined = body["name"]?.toString();
219
+ const defaultBranch: string | undefined =
220
+ body["defaultBranch"]?.toString();
221
+ const repositoryUrl: string | undefined =
222
+ body["repositoryUrl"]?.toString();
223
+
224
+ if (!projectId) {
225
+ return Response.sendErrorResponse(
226
+ req,
227
+ res,
228
+ new BadDataException("Project ID is required"),
229
+ );
230
+ }
231
+
232
+ if (!installationId) {
233
+ return Response.sendErrorResponse(
234
+ req,
235
+ res,
236
+ new BadDataException("Installation ID is required"),
237
+ );
238
+ }
239
+
240
+ if (!repositoryName) {
241
+ return Response.sendErrorResponse(
242
+ req,
243
+ res,
244
+ new BadDataException("Repository name is required"),
245
+ );
246
+ }
247
+
248
+ if (!organizationName) {
249
+ return Response.sendErrorResponse(
250
+ req,
251
+ res,
252
+ new BadDataException("Organization name is required"),
253
+ );
254
+ }
255
+
256
+ // Create the code repository record
257
+ const codeRepository: CodeRepository = new CodeRepository();
258
+ codeRepository.projectId = new ObjectID(projectId);
259
+ codeRepository.name = name || `${organizationName}/${repositoryName}`;
260
+ codeRepository.repositoryHostedAt = CodeRepositoryType.GitHub;
261
+ codeRepository.organizationName = organizationName;
262
+ codeRepository.repositoryName = repositoryName;
263
+ codeRepository.mainBranchName = defaultBranch || "main";
264
+ codeRepository.gitHubAppInstallationId = installationId;
265
+
266
+ if (repositoryUrl) {
267
+ codeRepository.repositoryUrl = URL.fromString(repositoryUrl);
268
+ }
269
+
270
+ const createdRepository: CodeRepository =
271
+ await CodeRepositoryService.create({
272
+ data: codeRepository,
273
+ props: {
274
+ isRoot: true,
275
+ },
276
+ });
277
+
278
+ return Response.sendJsonObjectResponse(req, res, {
279
+ repository: BaseModel.toJSON(createdRepository, CodeRepository),
280
+ } as JSONObject);
281
+ } catch (error) {
282
+ logger.error("GitHub Connect Repository Error:");
283
+ logger.error(error);
284
+ return Response.sendErrorResponse(
285
+ req,
286
+ res,
287
+ error instanceof Error
288
+ ? new BadDataException(error.message)
289
+ : new BadDataException("An error occurred"),
290
+ );
291
+ }
292
+ },
293
+ );
294
+
295
+ // GitHub webhook handler
296
+ router.post(
297
+ "/github/webhook",
298
+ async (req: ExpressRequest, res: ExpressResponse) => {
299
+ try {
300
+ const signature: string | undefined = req.headers[
301
+ "x-hub-signature-256"
302
+ ] as string | undefined;
303
+ const event: string | undefined = req.headers["x-github-event"] as
304
+ | string
305
+ | undefined;
306
+
307
+ if (!signature) {
308
+ return Response.sendErrorResponse(
309
+ req,
310
+ res,
311
+ new BadDataException("Missing webhook signature"),
312
+ );
313
+ }
314
+
315
+ // Get raw body for signature verification
316
+ const rawBody: string = JSON.stringify(req.body);
317
+
318
+ // Verify webhook signature
319
+ const isValid: boolean = GitHubUtil.verifyWebhookSignature(
320
+ rawBody,
321
+ signature,
322
+ );
323
+
324
+ if (!isValid) {
325
+ return Response.sendErrorResponse(
326
+ req,
327
+ res,
328
+ new BadDataException("Invalid webhook signature"),
329
+ );
330
+ }
331
+
332
+ logger.debug(`Received GitHub webhook event: ${event}`);
333
+
334
+ /*
335
+ * Handle different webhook events here
336
+ * For now, just acknowledge receipt
337
+ * Future: Handle push, pull_request, check_run events
338
+ */
339
+
340
+ return Response.sendJsonObjectResponse(req, res, {
341
+ success: true,
342
+ message: "Webhook received",
343
+ } as JSONObject);
344
+ } catch (error) {
345
+ logger.error("GitHub Webhook Error:");
346
+ logger.error(error);
347
+ return Response.sendErrorResponse(
348
+ req,
349
+ res,
350
+ error instanceof Error
351
+ ? new BadDataException(error.message)
352
+ : new BadDataException("An error occurred"),
353
+ );
354
+ }
355
+ },
356
+ );
357
+
358
+ return router;
359
+ }
360
+ }
@@ -1,6 +1,7 @@
1
1
  import Incident from "../../Models/DatabaseModels/Incident";
2
2
  import File from "../../Models/DatabaseModels/File";
3
3
  import NotFoundException from "../../Types/Exception/NotFoundException";
4
+ import BadDataException from "../../Types/Exception/BadDataException";
4
5
  import ObjectID from "../../Types/ObjectID";
5
6
  import IncidentService, {
6
7
  Service as IncidentServiceType,
@@ -15,6 +16,13 @@ import {
15
16
  } from "../Utils/Express";
16
17
  import CommonAPI from "./CommonAPI";
17
18
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
19
+ import AIService, { AILogRequest, AILogResponse } from "../Services/AIService";
20
+ import IncidentAIContextBuilder, {
21
+ AIGenerationContext,
22
+ IncidentContextData,
23
+ } from "../Utils/AI/IncidentAIContextBuilder";
24
+ import JSONFunctions from "../../Types/JSONFunctions";
25
+ import Permission from "../../Types/Permission";
18
26
 
19
27
  export default class IncidentAPI extends BaseAPI<
20
28
  Incident,
@@ -36,6 +44,21 @@ export default class IncidentAPI extends BaseAPI<
36
44
  }
37
45
  },
38
46
  );
47
+
48
+ // Generate postmortem from AI
49
+ this.router.post(
50
+ `${new this.entityType()
51
+ .getCrudApiPath()
52
+ ?.toString()}/generate-postmortem-from-ai/:incidentId`,
53
+ UserMiddleware.getUserMiddleware,
54
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
55
+ try {
56
+ await this.generatePostmortemFromAI(req, res);
57
+ } catch (err) {
58
+ next(err);
59
+ }
60
+ },
61
+ );
39
62
  }
40
63
 
41
64
  private async getPostmortemAttachment(
@@ -103,4 +126,107 @@ export default class IncidentAPI extends BaseAPI<
103
126
  Response.setNoCacheHeaders(res);
104
127
  return Response.sendFileResponse(req, res, attachment);
105
128
  }
129
+
130
+ private async generatePostmortemFromAI(
131
+ req: ExpressRequest,
132
+ res: ExpressResponse,
133
+ ): Promise<void> {
134
+ const incidentIdParam: string | undefined = req.params["incidentId"];
135
+
136
+ if (!incidentIdParam) {
137
+ throw new BadDataException("Incident ID is required");
138
+ }
139
+
140
+ let incidentId: ObjectID;
141
+
142
+ try {
143
+ incidentId = new ObjectID(incidentIdParam);
144
+ } catch {
145
+ throw new BadDataException("Invalid Incident ID");
146
+ }
147
+
148
+ const props: DatabaseCommonInteractionProps =
149
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
150
+
151
+ // Verify user has permission to edit the incident
152
+ const permissions: Array<Permission> | undefined = props
153
+ .userTenantAccessPermission?.["permissions"] as
154
+ | Array<Permission>
155
+ | undefined;
156
+
157
+ const hasPermission: boolean = permissions
158
+ ? permissions.some((p: Permission) => {
159
+ return (
160
+ p === Permission.ProjectOwner ||
161
+ p === Permission.ProjectAdmin ||
162
+ p === Permission.EditProjectIncident
163
+ );
164
+ })
165
+ : false;
166
+
167
+ if (!hasPermission && !props.isMasterAdmin) {
168
+ throw new BadDataException(
169
+ "You do not have permission to generate postmortem for this incident. You need to have one of these permissions: Project Owner, Project Admin, Edit Project Incident.",
170
+ );
171
+ }
172
+
173
+ // Get the template from request body if provided
174
+ const template: string | undefined = JSONFunctions.getJSONValueInPath(
175
+ req.body,
176
+ "template",
177
+ ) as string | undefined;
178
+
179
+ // Always include workspace messages for comprehensive context
180
+ const includeWorkspaceMessages: boolean = true;
181
+
182
+ // Get the incident to verify it exists and get the project ID
183
+ const incident: Incident | null = await this.service.findOneById({
184
+ id: incidentId,
185
+ select: {
186
+ _id: true,
187
+ projectId: true,
188
+ },
189
+ props,
190
+ });
191
+
192
+ if (!incident || !incident.projectId) {
193
+ throw new NotFoundException("Incident not found");
194
+ }
195
+
196
+ // Build incident context
197
+ const contextData: IncidentContextData =
198
+ await IncidentAIContextBuilder.buildIncidentContext({
199
+ incidentId,
200
+ includeWorkspaceMessages,
201
+ workspaceMessageLimit: 500,
202
+ });
203
+
204
+ // Format context for postmortem generation
205
+ const aiContext: AIGenerationContext =
206
+ IncidentAIContextBuilder.formatIncidentContextForPostmortem(
207
+ contextData,
208
+ template,
209
+ );
210
+
211
+ // Generate postmortem using AIService (handles billing and logging)
212
+ const aiLogRequest: AILogRequest = {
213
+ projectId: incident.projectId,
214
+ feature: "Incident Postmortem",
215
+ incidentId: incidentId,
216
+ messages: aiContext.messages,
217
+ maxTokens: 8192,
218
+ temperature: 0.7,
219
+ };
220
+
221
+ if (props.userId) {
222
+ aiLogRequest.userId = props.userId;
223
+ }
224
+
225
+ const response: AILogResponse =
226
+ await AIService.executeWithLogging(aiLogRequest);
227
+
228
+ return Response.sendJsonObjectResponse(req, res, {
229
+ postmortemNote: response.content,
230
+ });
231
+ }
106
232
  }
@@ -44,6 +44,7 @@ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
44
44
  "DISABLE_TELEMETRY",
45
45
  "SLACK_APP_CLIENT_ID",
46
46
  "MICROSOFT_TEAMS_APP_CLIENT_ID",
47
+ "GITHUB_APP_CLIENT_ID",
47
48
  "CAPTCHA_ENABLED",
48
49
  "CAPTCHA_SITE_KEY",
49
50
  ];
@@ -458,6 +459,49 @@ export const MicrosoftTeamsAppClientSecret: string | null =
458
459
  export const MicrosoftTeamsAppTenantId: string | null =
459
460
  process.env["MICROSOFT_TEAMS_APP_TENANT_ID"] || null;
460
461
 
462
+ // GitHub App Configuration
463
+ export const GitHubAppId: string | null = process.env["GITHUB_APP_ID"] || null;
464
+ export const GitHubAppClientId: string | null =
465
+ process.env["GITHUB_APP_CLIENT_ID"] || null;
466
+ export const GitHubAppClientSecret: string | null =
467
+ process.env["GITHUB_APP_CLIENT_SECRET"] || null;
468
+
469
+ type DecodePrivateKeyFunction = (key: string | undefined) => string | null;
470
+
471
+ // Helper function to decode base64 private key if needed
472
+ const decodePrivateKey: DecodePrivateKeyFunction = (
473
+ key: string | undefined,
474
+ ): string | null => {
475
+ if (!key) {
476
+ return null;
477
+ }
478
+
479
+ // If it starts with "-----BEGIN", it's already in PEM format
480
+ if (key.trim().startsWith("-----BEGIN")) {
481
+ return key;
482
+ }
483
+
484
+ // Otherwise, assume it's base64 encoded and decode it
485
+ try {
486
+ const decoded: string = Buffer.from(key, "base64").toString("utf-8");
487
+ // Verify it's a valid PEM key after decoding
488
+ if (decoded.trim().startsWith("-----BEGIN")) {
489
+ return decoded;
490
+ }
491
+ // If decoding doesn't produce a valid PEM, return original value
492
+ return key;
493
+ } catch {
494
+ // If decoding fails, return the original value
495
+ return key;
496
+ }
497
+ };
498
+
499
+ export const GitHubAppPrivateKey: string | null = decodePrivateKey(
500
+ process.env["GITHUB_APP_PRIVATE_KEY"],
501
+ );
502
+ export const GitHubAppWebhookSecret: string | null =
503
+ process.env["GITHUB_APP_WEBHOOK_SECRET"] || null;
504
+
461
505
  // VAPID Configuration for Web Push Notifications
462
506
  export const VapidPublicKey: string | undefined =
463
507
  process.env["VAPID_PUBLIC_KEY"] || undefined;
@@ -0,0 +1,79 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1765580181582 implements MigrationInterface {
4
+ public name = "MigrationName1765580181582";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `CREATE TABLE "CodeRepository" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "slug" character varying(100) NOT NULL, "description" character varying(500), "repositoryHostedAt" character varying(100) NOT NULL, "organizationName" character varying(100) NOT NULL, "repositoryName" character varying(100) NOT NULL, "mainBranchName" character varying(100) DEFAULT 'main', "repositoryUrl" text, "gitHubAppInstallationId" character varying(500), "gitLabProjectId" character varying(500), "secretToken" text, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_7b5219d06a82fbc0bc4540b74f0" PRIMARY KEY ("_id"))`,
9
+ );
10
+ await queryRunner.query(
11
+ `CREATE INDEX "IDX_a653bdc2fac520c9c8b9a7c7a6" ON "CodeRepository" ("projectId") `,
12
+ );
13
+ await queryRunner.query(
14
+ `CREATE TABLE "CodeRepositoryLabel" ("codeRepositoryId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_5adb09e0b5957488be8931f46bc" PRIMARY KEY ("codeRepositoryId", "labelId"))`,
15
+ );
16
+ await queryRunner.query(
17
+ `CREATE INDEX "IDX_7710ab8ee47601f78f3a4b76b6" ON "CodeRepositoryLabel" ("codeRepositoryId") `,
18
+ );
19
+ await queryRunner.query(
20
+ `CREATE INDEX "IDX_8f7d12100e441fc72e02151fc5" ON "CodeRepositoryLabel" ("labelId") `,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "CodeRepository" ADD CONSTRAINT "FK_a653bdc2fac520c9c8b9a7c7a6a" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "CodeRepository" ADD CONSTRAINT "FK_a870b71b99c87ea658c11421490" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "CodeRepository" ADD CONSTRAINT "FK_79d9249eb5f8174a6f6228311f4" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "CodeRepositoryLabel" ADD CONSTRAINT "FK_7710ab8ee47601f78f3a4b76b64" FOREIGN KEY ("codeRepositoryId") REFERENCES "CodeRepository"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
39
+ );
40
+ await queryRunner.query(
41
+ `ALTER TABLE "CodeRepositoryLabel" ADD CONSTRAINT "FK_8f7d12100e441fc72e02151fc56" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
42
+ );
43
+ }
44
+
45
+ public async down(queryRunner: QueryRunner): Promise<void> {
46
+ await queryRunner.query(
47
+ `ALTER TABLE "CodeRepositoryLabel" DROP CONSTRAINT "FK_8f7d12100e441fc72e02151fc56"`,
48
+ );
49
+ await queryRunner.query(
50
+ `ALTER TABLE "CodeRepositoryLabel" DROP CONSTRAINT "FK_7710ab8ee47601f78f3a4b76b64"`,
51
+ );
52
+ await queryRunner.query(
53
+ `ALTER TABLE "CodeRepository" DROP CONSTRAINT "FK_79d9249eb5f8174a6f6228311f4"`,
54
+ );
55
+ await queryRunner.query(
56
+ `ALTER TABLE "CodeRepository" DROP CONSTRAINT "FK_a870b71b99c87ea658c11421490"`,
57
+ );
58
+ await queryRunner.query(
59
+ `ALTER TABLE "CodeRepository" DROP CONSTRAINT "FK_a653bdc2fac520c9c8b9a7c7a6a"`,
60
+ );
61
+ await queryRunner.query(
62
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
63
+ );
64
+ await queryRunner.query(
65
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
66
+ );
67
+ await queryRunner.query(
68
+ `DROP INDEX "public"."IDX_8f7d12100e441fc72e02151fc5"`,
69
+ );
70
+ await queryRunner.query(
71
+ `DROP INDEX "public"."IDX_7710ab8ee47601f78f3a4b76b6"`,
72
+ );
73
+ await queryRunner.query(`DROP TABLE "CodeRepositoryLabel"`);
74
+ await queryRunner.query(
75
+ `DROP INDEX "public"."IDX_a653bdc2fac520c9c8b9a7c7a6"`,
76
+ );
77
+ await queryRunner.query(`DROP TABLE "CodeRepository"`);
78
+ }
79
+ }