@oneuptime/common 9.2.16 → 9.2.18

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 (133) 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/AlertAPI.ts +139 -0
  9. package/Server/API/GitHubAPI.ts +360 -0
  10. package/Server/API/IncidentAPI.ts +258 -0
  11. package/Server/API/ScheduledMaintenanceAPI.ts +164 -0
  12. package/Server/EnvironmentConfig.ts +44 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.ts +79 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.ts +75 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.ts +69 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.ts +111 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.ts +39 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  20. package/Server/Services/AIBillingService.ts +247 -0
  21. package/Server/Services/AIService.ts +238 -0
  22. package/Server/Services/CodeRepositoryService.ts +10 -0
  23. package/Server/Services/IncidentService.ts +88 -0
  24. package/Server/Services/Index.ts +2 -0
  25. package/Server/Services/LlmLogService.ts +14 -0
  26. package/Server/Services/LlmProviderService.ts +58 -0
  27. package/Server/Services/ServiceCatalogCodeRepositoryService.ts +55 -0
  28. package/Server/Utils/AI/AlertAIContextBuilder.ts +264 -0
  29. package/Server/Utils/AI/IncidentAIContextBuilder.ts +710 -0
  30. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +345 -0
  31. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +226 -0
  32. package/Server/Utils/LLM/LLMService.ts +276 -0
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +166 -0
  34. package/Server/Utils/Workspace/Slack/Slack.ts +134 -0
  35. package/Server/Utils/Workspace/Workspace.ts +126 -0
  36. package/Tests/Types/Domain.test.ts +24 -3
  37. package/Types/CodeRepository/CodeRepositoryType.ts +1 -1
  38. package/Types/Domain.ts +21 -24
  39. package/Types/LlmLogStatus.ts +7 -0
  40. package/Types/Permission.ts +87 -0
  41. package/Types/ServiceCatalog/CodeRepositoryImprovementAction.ts +9 -0
  42. package/UI/Components/AI/AILoader.tsx +95 -0
  43. package/UI/Components/AI/GenerateFromAIModal.tsx +432 -0
  44. package/UI/Components/Modal/Modal.tsx +6 -1
  45. package/build/dist/Models/DatabaseModels/CodeRepository.js +689 -0
  46. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -0
  47. package/build/dist/Models/DatabaseModels/Index.js +7 -0
  48. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/LlmLog.js +856 -0
  50. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -0
  51. package/build/dist/Models/DatabaseModels/LlmProvider.js +22 -0
  52. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  53. package/build/dist/Models/DatabaseModels/Project.js +220 -0
  54. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js +565 -0
  56. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js.map +1 -0
  57. package/build/dist/Server/API/AIBillingAPI.js +58 -0
  58. package/build/dist/Server/API/AIBillingAPI.js.map +1 -0
  59. package/build/dist/Server/API/AlertAPI.js +94 -0
  60. package/build/dist/Server/API/AlertAPI.js.map +1 -0
  61. package/build/dist/Server/API/GitHubAPI.js +207 -0
  62. package/build/dist/Server/API/GitHubAPI.js.map +1 -0
  63. package/build/dist/Server/API/IncidentAPI.js +171 -1
  64. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  65. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +103 -0
  66. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -0
  67. package/build/dist/Server/EnvironmentConfig.js +31 -0
  68. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js +34 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js +32 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js +38 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js +30 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js.map +1 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js +44 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js.map +1 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js +22 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  83. package/build/dist/Server/Services/AIBillingService.js +187 -0
  84. package/build/dist/Server/Services/AIBillingService.js.map +1 -0
  85. package/build/dist/Server/Services/AIService.js +184 -0
  86. package/build/dist/Server/Services/AIService.js.map +1 -0
  87. package/build/dist/Server/Services/CodeRepositoryService.js +9 -0
  88. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -0
  89. package/build/dist/Server/Services/IncidentService.js +60 -0
  90. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  91. package/build/dist/Server/Services/Index.js +2 -0
  92. package/build/dist/Server/Services/Index.js.map +1 -1
  93. package/build/dist/Server/Services/LlmLogService.js +13 -0
  94. package/build/dist/Server/Services/LlmLogService.js.map +1 -0
  95. package/build/dist/Server/Services/LlmProviderService.js +65 -0
  96. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  97. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js +54 -0
  98. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js.map +1 -0
  99. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +238 -0
  100. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -0
  101. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +597 -0
  102. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -0
  103. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +311 -0
  104. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -0
  105. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +163 -0
  106. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  107. package/build/dist/Server/Utils/LLM/LLMService.js +225 -0
  108. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -0
  109. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +110 -0
  110. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  111. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +89 -0
  112. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  113. package/build/dist/Server/Utils/Workspace/Workspace.js +80 -0
  114. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  115. package/build/dist/Tests/Types/Domain.test.js +19 -3
  116. package/build/dist/Tests/Types/Domain.test.js.map +1 -1
  117. package/build/dist/Types/CodeRepository/CodeRepositoryType.js +1 -1
  118. package/build/dist/Types/CodeRepository/CodeRepositoryType.js.map +1 -1
  119. package/build/dist/Types/Domain.js +18 -16
  120. package/build/dist/Types/Domain.js.map +1 -1
  121. package/build/dist/Types/LlmLogStatus.js +8 -0
  122. package/build/dist/Types/LlmLogStatus.js.map +1 -0
  123. package/build/dist/Types/Permission.js +74 -0
  124. package/build/dist/Types/Permission.js.map +1 -1
  125. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js +10 -0
  126. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js.map +1 -0
  127. package/build/dist/UI/Components/AI/AILoader.js +64 -0
  128. package/build/dist/UI/Components/AI/AILoader.js.map +1 -0
  129. package/build/dist/UI/Components/AI/GenerateFromAIModal.js +320 -0
  130. package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -0
  131. package/build/dist/UI/Components/Modal/Modal.js +6 -1
  132. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  133. package/package.json +1 -1
@@ -0,0 +1,345 @@
1
+ import ObjectID from "../../../Types/ObjectID";
2
+ import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
3
+ import ScheduledMaintenanceStateTimeline from "../../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
4
+ import ScheduledMaintenanceInternalNote from "../../../Models/DatabaseModels/ScheduledMaintenanceInternalNote";
5
+ import ScheduledMaintenancePublicNote from "../../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
6
+ import ScheduledMaintenanceService from "../../Services/ScheduledMaintenanceService";
7
+ import ScheduledMaintenanceStateTimelineService from "../../Services/ScheduledMaintenanceStateTimelineService";
8
+ import ScheduledMaintenanceInternalNoteService from "../../Services/ScheduledMaintenanceInternalNoteService";
9
+ import ScheduledMaintenancePublicNoteService from "../../Services/ScheduledMaintenancePublicNoteService";
10
+ import CaptureSpan from "../Telemetry/CaptureSpan";
11
+ import OneUptimeDate from "../../../Types/Date";
12
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
13
+ import { LLMMessage } from "../LLM/LLMService";
14
+
15
+ export interface ScheduledMaintenanceContextData {
16
+ scheduledMaintenance: ScheduledMaintenance;
17
+ stateTimeline: Array<ScheduledMaintenanceStateTimeline>;
18
+ internalNotes: Array<ScheduledMaintenanceInternalNote>;
19
+ publicNotes: Array<ScheduledMaintenancePublicNote>;
20
+ }
21
+
22
+ export interface AIGenerationContext {
23
+ contextText: string;
24
+ systemPrompt: string;
25
+ messages: Array<LLMMessage>;
26
+ }
27
+
28
+ export default class ScheduledMaintenanceAIContextBuilder {
29
+ @CaptureSpan()
30
+ public static async buildScheduledMaintenanceContext(data: {
31
+ scheduledMaintenanceId: ObjectID;
32
+ }): Promise<ScheduledMaintenanceContextData> {
33
+ const scheduledMaintenance: ScheduledMaintenance | null =
34
+ await ScheduledMaintenanceService.findOneById({
35
+ id: data.scheduledMaintenanceId,
36
+ select: {
37
+ _id: true,
38
+ title: true,
39
+ description: true,
40
+ createdAt: true,
41
+ startsAt: true,
42
+ endsAt: true,
43
+ customFields: true,
44
+ projectId: true,
45
+ currentScheduledMaintenanceState: {
46
+ name: true,
47
+ color: true,
48
+ },
49
+ monitors: {
50
+ name: true,
51
+ },
52
+ labels: {
53
+ name: true,
54
+ color: true,
55
+ },
56
+ },
57
+ props: {
58
+ isRoot: true,
59
+ },
60
+ });
61
+
62
+ if (!scheduledMaintenance) {
63
+ throw new Error("Scheduled Maintenance not found");
64
+ }
65
+
66
+ // Fetch state timeline
67
+ const stateTimeline: Array<ScheduledMaintenanceStateTimeline> =
68
+ await ScheduledMaintenanceStateTimelineService.findBy({
69
+ query: {
70
+ scheduledMaintenanceId: data.scheduledMaintenanceId,
71
+ },
72
+ select: {
73
+ _id: true,
74
+ createdAt: true,
75
+ startsAt: true,
76
+ endsAt: true,
77
+ scheduledMaintenanceState: {
78
+ name: true,
79
+ color: true,
80
+ },
81
+ createdByUser: {
82
+ name: true,
83
+ email: true,
84
+ },
85
+ },
86
+ sort: {
87
+ startsAt: SortOrder.Ascending,
88
+ },
89
+ limit: 100,
90
+ skip: 0,
91
+ props: {
92
+ isRoot: true,
93
+ },
94
+ });
95
+
96
+ // Fetch internal notes
97
+ const internalNotes: Array<ScheduledMaintenanceInternalNote> =
98
+ await ScheduledMaintenanceInternalNoteService.findBy({
99
+ query: {
100
+ scheduledMaintenanceId: data.scheduledMaintenanceId,
101
+ },
102
+ select: {
103
+ _id: true,
104
+ note: true,
105
+ createdAt: true,
106
+ createdByUser: {
107
+ name: true,
108
+ email: true,
109
+ },
110
+ },
111
+ sort: {
112
+ createdAt: SortOrder.Ascending,
113
+ },
114
+ limit: 100,
115
+ skip: 0,
116
+ props: {
117
+ isRoot: true,
118
+ },
119
+ });
120
+
121
+ // Fetch public notes
122
+ const publicNotes: Array<ScheduledMaintenancePublicNote> =
123
+ await ScheduledMaintenancePublicNoteService.findBy({
124
+ query: {
125
+ scheduledMaintenanceId: data.scheduledMaintenanceId,
126
+ },
127
+ select: {
128
+ _id: true,
129
+ note: true,
130
+ createdAt: true,
131
+ postedAt: true,
132
+ createdByUser: {
133
+ name: true,
134
+ email: true,
135
+ },
136
+ },
137
+ sort: {
138
+ createdAt: SortOrder.Ascending,
139
+ },
140
+ limit: 100,
141
+ skip: 0,
142
+ props: {
143
+ isRoot: true,
144
+ },
145
+ });
146
+
147
+ return {
148
+ scheduledMaintenance,
149
+ stateTimeline,
150
+ internalNotes,
151
+ publicNotes,
152
+ };
153
+ }
154
+
155
+ @CaptureSpan()
156
+ public static formatScheduledMaintenanceContextForNote(
157
+ contextData: ScheduledMaintenanceContextData,
158
+ noteType: "public" | "internal",
159
+ template?: string,
160
+ ): AIGenerationContext {
161
+ const { scheduledMaintenance, stateTimeline, internalNotes, publicNotes } =
162
+ contextData;
163
+
164
+ let contextText: string = "";
165
+
166
+ // Basic scheduled maintenance information
167
+ contextText += "# Scheduled Maintenance Information\n\n";
168
+ contextText += `**Title:** ${scheduledMaintenance.title || "N/A"}\n\n`;
169
+ contextText += `**Description:** ${scheduledMaintenance.description || "N/A"}\n\n`;
170
+ contextText += `**Current State:** ${scheduledMaintenance.currentScheduledMaintenanceState?.name || "N/A"}\n\n`;
171
+ contextText += `**Scheduled Start:** ${scheduledMaintenance.startsAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.startsAt) : "N/A"}\n\n`;
172
+ contextText += `**Scheduled End:** ${scheduledMaintenance.endsAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.endsAt) : "N/A"}\n\n`;
173
+ contextText += `**Created At:** ${scheduledMaintenance.createdAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.createdAt) : "N/A"}\n\n`;
174
+
175
+ // Affected monitors
176
+ if (
177
+ scheduledMaintenance.monitors &&
178
+ scheduledMaintenance.monitors.length > 0
179
+ ) {
180
+ contextText += "**Affected Monitors:** ";
181
+ contextText += scheduledMaintenance.monitors
182
+ .map((m: { name?: string }) => {
183
+ return m.name;
184
+ })
185
+ .join(", ");
186
+ contextText += "\n\n";
187
+ }
188
+
189
+ // Labels
190
+ if (scheduledMaintenance.labels && scheduledMaintenance.labels.length > 0) {
191
+ contextText += "**Labels:** ";
192
+ contextText += scheduledMaintenance.labels
193
+ .map((l: { name?: string }) => {
194
+ return l.name;
195
+ })
196
+ .join(", ");
197
+ contextText += "\n\n";
198
+ }
199
+
200
+ // State timeline
201
+ if (stateTimeline.length > 0) {
202
+ contextText += "# State Timeline\n\n";
203
+ for (const timeline of stateTimeline) {
204
+ const startTime: string = timeline.startsAt
205
+ ? OneUptimeDate.getDateAsFormattedString(timeline.startsAt)
206
+ : "N/A";
207
+ const stateName: string =
208
+ timeline.scheduledMaintenanceState?.name?.toString() || "Unknown";
209
+ const createdBy: string =
210
+ timeline.createdByUser?.name?.toString() ||
211
+ timeline.createdByUser?.email?.toString() ||
212
+ "System";
213
+
214
+ contextText += `- **${startTime}**: State changed to **${stateName}** by ${createdBy}\n`;
215
+ }
216
+ contextText += "\n";
217
+ }
218
+
219
+ // Include internal notes for context (for both note types)
220
+ if (internalNotes.length > 0) {
221
+ contextText += "# Internal Notes (Private)\n\n";
222
+ for (const note of internalNotes) {
223
+ const noteTime: string = note.createdAt
224
+ ? OneUptimeDate.getDateAsFormattedString(note.createdAt)
225
+ : "N/A";
226
+ const createdBy: string =
227
+ note.createdByUser?.name?.toString() ||
228
+ note.createdByUser?.email?.toString() ||
229
+ "Unknown";
230
+
231
+ contextText += `**[${noteTime}] ${createdBy}:**\n`;
232
+ contextText += `${note.note || "N/A"}\n\n`;
233
+ }
234
+ }
235
+
236
+ // Public notes
237
+ if (publicNotes.length > 0) {
238
+ contextText += "# Public Notes\n\n";
239
+ for (const note of publicNotes) {
240
+ const noteTime: string = note.postedAt
241
+ ? OneUptimeDate.getDateAsFormattedString(note.postedAt)
242
+ : note.createdAt
243
+ ? OneUptimeDate.getDateAsFormattedString(note.createdAt)
244
+ : "N/A";
245
+ const createdBy: string =
246
+ note.createdByUser?.name?.toString() ||
247
+ note.createdByUser?.email?.toString() ||
248
+ "Unknown";
249
+
250
+ contextText += `**[${noteTime}] ${createdBy}:**\n`;
251
+ contextText += `${note.note || "N/A"}\n\n`;
252
+ }
253
+ }
254
+
255
+ // System prompt for note generation
256
+ let systemPrompt: string;
257
+
258
+ if (noteType === "public") {
259
+ if (template) {
260
+ systemPrompt = `You are an expert technical communicator. Your task is to fill in a public scheduled maintenance note template based on the provided maintenance event data.
261
+
262
+ CRITICAL INSTRUCTIONS:
263
+ - You MUST use ONLY the exact template structure provided below
264
+ - Fill in each section of the template with relevant information from the maintenance data
265
+ - Do NOT add any new sections, headers, or content that is not part of the template
266
+ - Do NOT add introductions, conclusions, or any text outside the template structure
267
+ - Write in a professional, clear, and customer-friendly manner
268
+ - Focus on what customers need to know: timing, impact, and what to expect
269
+ - Avoid technical jargon - keep it understandable for non-technical readers
270
+ - Be concise but informative
271
+
272
+ TEMPLATE TO FILL (use this exact structure):
273
+
274
+ ${template}`;
275
+ } else {
276
+ systemPrompt = `You are an expert technical communicator. Your task is to generate a public scheduled maintenance note that will be visible to customers on the status page.
277
+
278
+ The note should:
279
+ 1. Be written in a professional, customer-friendly tone
280
+ 2. Clearly communicate the current status of the maintenance
281
+ 3. Explain what work is being done and any impact on users
282
+ 4. Provide timing information (when it started, expected completion, etc.)
283
+ 5. Set appropriate expectations
284
+ 6. Be concise but informative
285
+
286
+ DO NOT include:
287
+ - Internal technical details that customers don't need
288
+ - Confidential information
289
+ - Excessive jargon
290
+
291
+ Write in markdown format for better readability.`;
292
+ }
293
+ } else if (template) {
294
+ // Internal note with template
295
+ systemPrompt = `You are an expert Site Reliability Engineer (SRE). Your task is to fill in an internal scheduled maintenance note template based on the provided maintenance event data.
296
+
297
+ CRITICAL INSTRUCTIONS:
298
+ - You MUST use ONLY the exact template structure provided below
299
+ - Fill in each section of the template with relevant information from the maintenance data
300
+ - Do NOT add any new sections, headers, or content that is not part of the template
301
+ - Do NOT add introductions, conclusions, or any text outside the template structure
302
+ - Be technical and detailed - this is for the internal team
303
+ - Include relevant technical details, progress updates, and observations
304
+
305
+ TEMPLATE TO FILL (use this exact structure):
306
+
307
+ ${template}`;
308
+ } else {
309
+ // Internal note without template
310
+ systemPrompt = `You are an expert Site Reliability Engineer (SRE). Your task is to generate an internal scheduled maintenance note for the team.
311
+
312
+ The note should:
313
+ 1. Provide technical details about the maintenance progress
314
+ 2. Document observations, findings, or actions taken
315
+ 3. Include any issues encountered or changes to the plan
316
+ 4. Be detailed enough to help team members understand the current status
317
+ 5. Use technical language appropriate for the engineering team
318
+
319
+ Write in markdown format for better readability. Be thorough and technical.`;
320
+ }
321
+
322
+ // Build user message
323
+ const userMessage: string = template
324
+ ? `Fill in the template above using ONLY the following scheduled maintenance data. Output only the filled template, nothing else:\n\n${contextText}`
325
+ : `Based on the following scheduled maintenance data, please generate ${noteType === "public" ? "a customer-facing public" : "an internal technical"} maintenance note:\n\n${contextText}`;
326
+
327
+ // Build messages array
328
+ const messages: Array<LLMMessage> = [
329
+ {
330
+ role: "system",
331
+ content: systemPrompt,
332
+ },
333
+ {
334
+ role: "user",
335
+ content: userMessage,
336
+ },
337
+ ];
338
+
339
+ return {
340
+ contextText,
341
+ systemPrompt,
342
+ messages,
343
+ };
344
+ }
345
+ }
@@ -10,6 +10,29 @@ import OneUptimeDate from "../../../../Types/Date";
10
10
  import { JSONArray, JSONObject } from "../../../../Types/JSON";
11
11
  import API from "../../../../Utils/API";
12
12
  import CaptureSpan from "../../Telemetry/CaptureSpan";
13
+ import {
14
+ GitHubAppId,
15
+ GitHubAppPrivateKey,
16
+ GitHubAppWebhookSecret,
17
+ } from "../../../EnvironmentConfig";
18
+ import BadDataException from "../../../../Types/Exception/BadDataException";
19
+ import * as crypto from "crypto";
20
+
21
+ export interface GitHubRepository {
22
+ id: number;
23
+ name: string;
24
+ fullName: string;
25
+ private: boolean;
26
+ htmlUrl: string;
27
+ description: string | null;
28
+ defaultBranch: string;
29
+ ownerLogin: string;
30
+ }
31
+
32
+ export interface GitHubInstallationToken {
33
+ token: string;
34
+ expiresAt: Date;
35
+ }
13
36
 
14
37
  export default class GitHubUtil extends HostedCodeRepository {
15
38
  private getPullRequestFromJSONObject(data: {
@@ -258,4 +281,207 @@ export default class GitHubUtil extends HostedCodeRepository {
258
281
  repositoryName: data.repositoryName,
259
282
  });
260
283
  }
284
+
285
+ // GitHub App Authentication Methods
286
+
287
+ /**
288
+ * Generates a JWT for GitHub App authentication
289
+ * @returns JWT string valid for 10 minutes
290
+ */
291
+ @CaptureSpan()
292
+ public static generateAppJWT(): string {
293
+ if (!GitHubAppId) {
294
+ throw new BadDataException(
295
+ "GITHUB_APP_ID environment variable is not set",
296
+ );
297
+ }
298
+
299
+ if (!GitHubAppPrivateKey) {
300
+ throw new BadDataException(
301
+ "GITHUB_APP_PRIVATE_KEY environment variable is not set",
302
+ );
303
+ }
304
+
305
+ const now: number = Math.floor(Date.now() / 1000);
306
+ const payload: JSONObject = {
307
+ iat: now - 60, // Issued at time (60 seconds in the past to allow for clock drift)
308
+ exp: now + 600, // Expiration time (10 minutes from now)
309
+ iss: GitHubAppId,
310
+ };
311
+
312
+ // Create JWT header
313
+ const header: JSONObject = {
314
+ alg: "RS256",
315
+ typ: "JWT",
316
+ };
317
+
318
+ const encodedHeader: string = Buffer.from(JSON.stringify(header)).toString(
319
+ "base64url",
320
+ );
321
+ const encodedPayload: string = Buffer.from(
322
+ JSON.stringify(payload),
323
+ ).toString("base64url");
324
+
325
+ const signatureInput: string = `${encodedHeader}.${encodedPayload}`;
326
+
327
+ // Sign with private key
328
+ const sign: crypto.Sign = crypto.createSign("RSA-SHA256");
329
+ sign.update(signatureInput);
330
+ const signature: string = sign.sign(GitHubAppPrivateKey, "base64url");
331
+
332
+ return `${signatureInput}.${signature}`;
333
+ }
334
+
335
+ /**
336
+ * Gets an installation access token for a GitHub App installation
337
+ * @param installationId - The GitHub App installation ID
338
+ * @returns Installation token and expiration date
339
+ */
340
+ @CaptureSpan()
341
+ public static async getInstallationAccessToken(
342
+ installationId: string,
343
+ ): Promise<GitHubInstallationToken> {
344
+ const jwt: string = GitHubUtil.generateAppJWT();
345
+
346
+ const url: URL = URL.fromString(
347
+ `https://api.github.com/app/installations/${installationId}/access_tokens`,
348
+ );
349
+
350
+ const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post(
351
+ {
352
+ url: url,
353
+ data: {},
354
+ headers: {
355
+ Authorization: `Bearer ${jwt}`,
356
+ Accept: "application/vnd.github+json",
357
+ "X-GitHub-Api-Version": "2022-11-28",
358
+ },
359
+ },
360
+ );
361
+
362
+ if (result instanceof HTTPErrorResponse) {
363
+ throw result;
364
+ }
365
+
366
+ return {
367
+ token: result.data["token"] as string,
368
+ expiresAt: OneUptimeDate.fromString(result.data["expires_at"] as string),
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Lists repositories accessible to a GitHub App installation
374
+ * @param installationId - The GitHub App installation ID
375
+ * @returns Array of repositories
376
+ */
377
+ @CaptureSpan()
378
+ public static async listRepositoriesForInstallation(
379
+ installationId: string,
380
+ ): Promise<Array<GitHubRepository>> {
381
+ const tokenData: GitHubInstallationToken =
382
+ await GitHubUtil.getInstallationAccessToken(installationId);
383
+
384
+ const allRepositories: Array<GitHubRepository> = [];
385
+ let page: number = 1;
386
+ let hasMore: boolean = true;
387
+
388
+ while (hasMore) {
389
+ const url: URL = URL.fromString(
390
+ `https://api.github.com/installation/repositories?per_page=100&page=${page}`,
391
+ );
392
+
393
+ const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
394
+ await API.get({
395
+ url: url,
396
+ data: {},
397
+ headers: {
398
+ Authorization: `Bearer ${tokenData.token}`,
399
+ Accept: "application/vnd.github+json",
400
+ "X-GitHub-Api-Version": "2022-11-28",
401
+ },
402
+ });
403
+
404
+ if (result instanceof HTTPErrorResponse) {
405
+ throw result;
406
+ }
407
+
408
+ const repositories: JSONArray =
409
+ (result.data["repositories"] as JSONArray) || [];
410
+
411
+ for (const repo of repositories) {
412
+ const repoData: JSONObject = repo as JSONObject;
413
+ const owner: JSONObject = repoData["owner"] as JSONObject;
414
+
415
+ allRepositories.push({
416
+ id: repoData["id"] as number,
417
+ name: repoData["name"] as string,
418
+ fullName: repoData["full_name"] as string,
419
+ private: repoData["private"] as boolean,
420
+ htmlUrl: repoData["html_url"] as string,
421
+ description: (repoData["description"] as string) || null,
422
+ defaultBranch: repoData["default_branch"] as string,
423
+ ownerLogin: owner["login"] as string,
424
+ });
425
+ }
426
+
427
+ // Check if there are more pages
428
+ if (repositories.length < 100) {
429
+ hasMore = false;
430
+ } else {
431
+ page++;
432
+ }
433
+ }
434
+
435
+ return allRepositories;
436
+ }
437
+
438
+ /**
439
+ * Verifies a GitHub webhook signature
440
+ * @param payload - The raw request body
441
+ * @param signature - The X-Hub-Signature-256 header value
442
+ * @returns true if signature is valid
443
+ */
444
+ public static verifyWebhookSignature(
445
+ payload: string,
446
+ signature: string,
447
+ ): boolean {
448
+ if (!GitHubAppWebhookSecret) {
449
+ logger.warn(
450
+ "GITHUB_APP_WEBHOOK_SECRET is not set, skipping verification",
451
+ );
452
+ return true;
453
+ }
454
+
455
+ const expectedSignature: string = `sha256=${crypto
456
+ .createHmac("sha256", GitHubAppWebhookSecret)
457
+ .update(payload)
458
+ .digest("hex")}`;
459
+
460
+ try {
461
+ return crypto.timingSafeEqual(
462
+ Buffer.from(signature) as Uint8Array,
463
+ Buffer.from(expectedSignature) as Uint8Array,
464
+ );
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Gets the GitHub App installation URL for a project to install the app
472
+ * @returns The installation URL
473
+ */
474
+ public static getAppInstallationUrl(): string {
475
+ if (!GitHubAppId) {
476
+ throw new BadDataException(
477
+ "GITHUB_APP_ID environment variable is not set",
478
+ );
479
+ }
480
+
481
+ /*
482
+ * This is the standard GitHub App installation URL format
483
+ * The app slug would typically come from another env var, but we can use the client ID approach
484
+ */
485
+ return `https://github.com/apps`;
486
+ }
261
487
  }