@oneuptime/common 14.0.6 → 14.0.8

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 (556) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  6. package/Models/DatabaseModels/StatusPage.ts +55 -0
  7. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  8. package/Server/API/IncidentAPI.ts +162 -0
  9. package/Server/API/StatusPageAPI.ts +487 -171
  10. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  20. package/Server/Infrastructure/Semaphore.ts +10 -0
  21. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  22. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  23. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  24. package/Server/Services/AlertService.ts +32 -36
  25. package/Server/Services/DatabaseService.ts +93 -10
  26. package/Server/Services/IncidentAlertService.ts +609 -2
  27. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  28. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  29. package/Server/Services/IncidentEpisodeService.ts +15 -0
  30. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  31. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  32. package/Server/Services/IncidentService.ts +1663 -103
  33. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  34. package/Server/Services/IncidentTemplateService.ts +109 -0
  35. package/Server/Services/InventoryItemService.ts +38 -0
  36. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  37. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  38. package/Server/Services/ScheduledMaintenanceService.ts +118 -64
  39. package/Server/Services/SmsService.ts +10 -0
  40. package/Server/Services/StatusPageService.ts +65 -8
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  42. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  43. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  44. package/Server/Services/UserNotificationRuleService.ts +55 -53
  45. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  46. package/Server/Types/Markdown.ts +153 -6
  47. package/Server/Types/Workflow/Components/Email.ts +61 -2
  48. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  49. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  50. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  51. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  52. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  53. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  54. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  55. package/Server/Utils/Response.ts +31 -0
  56. package/Server/Utils/SSRFProtection.ts +76 -16
  57. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  58. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  59. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  60. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  61. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  62. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  63. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  64. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  65. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  66. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  67. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  68. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  69. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  70. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  71. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  72. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  73. package/Server/Utils/StatusPageResource.ts +51 -20
  74. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  75. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  76. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  77. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  78. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  79. package/Server/Utils/VM/VMAPI.ts +1 -0
  80. package/Server/Utils/VM/VMRunner.ts +10 -0
  81. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  82. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  83. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  84. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  85. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  86. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  87. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  88. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  89. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  90. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  91. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  92. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  93. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  94. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  95. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  96. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  97. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  98. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  99. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  100. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  101. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  102. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  103. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +534 -6
  104. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  105. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  106. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  107. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  108. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  109. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  110. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  111. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  112. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  113. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  114. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  115. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  116. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  117. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  118. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  119. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  120. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  121. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  122. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  123. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  124. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  125. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  126. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  127. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  128. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  129. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  130. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  131. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  132. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  133. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  134. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  135. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  136. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  137. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  138. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  139. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  140. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  141. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  142. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  143. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  144. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  145. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  146. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  147. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  148. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  149. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  151. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  152. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  153. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  154. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  155. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  156. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  157. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  158. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  159. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  160. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  161. package/Tests/Server/Services/DomainService.test.ts +525 -0
  162. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  163. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  164. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  165. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  166. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  167. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  168. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  169. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  170. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  171. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  172. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  173. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  174. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  175. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  176. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  177. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  178. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  179. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  180. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  181. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  182. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  183. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  184. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  185. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  186. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  187. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  188. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  189. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  190. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  191. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  192. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  193. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  194. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  195. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  196. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  197. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  198. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  199. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  200. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  201. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  202. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  203. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  204. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  205. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  206. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  207. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  208. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  209. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  210. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  211. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  212. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  213. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  214. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  215. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  216. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  217. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  218. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  219. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  220. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  221. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  222. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  223. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  224. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  225. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  226. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  227. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  228. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  229. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  230. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  231. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  232. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  233. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  234. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  235. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  236. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  237. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  238. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  239. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  240. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  241. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  242. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  243. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  244. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  245. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  246. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  247. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  248. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  249. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  250. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  251. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  252. package/Tests/Types/SafeHtml.test.ts +102 -0
  253. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  254. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  255. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  256. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  257. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  258. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  259. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  260. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  261. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  262. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  263. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  264. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  265. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  266. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  267. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  268. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  269. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  270. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  271. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  272. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  273. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  274. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  275. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  276. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  277. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  278. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  279. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  280. package/Types/CustomField/CustomFieldType.ts +14 -0
  281. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  282. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  283. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  284. package/Types/Incident/IncidentAlertLink.ts +19 -0
  285. package/Types/SafeHtml.ts +93 -0
  286. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  287. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  288. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  289. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  290. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  291. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  292. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  293. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  294. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  295. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  296. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  297. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  298. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  299. package/Types/Topology/TopologyApi.ts +428 -0
  300. package/Types/Topology/TopologyTypeRules.ts +191 -0
  301. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  302. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  303. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  304. package/UI/Components/Forms/ModelForm.tsx +70 -8
  305. package/UI/Components/Forms/Validation.ts +1 -1
  306. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  307. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  308. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  309. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  310. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  311. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  312. package/Utils/API.ts +247 -2
  313. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  314. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  315. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  316. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  317. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  318. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  319. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  320. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  321. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  322. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  323. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  324. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  325. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  326. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  327. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  328. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  329. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  330. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  331. package/build/dist/Server/API/IncidentAPI.js +100 -1
  332. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  333. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  334. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  335. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  336. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  337. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  338. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  339. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  340. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  341. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  342. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  343. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  344. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  345. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  346. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  348. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  349. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  350. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  351. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  352. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  353. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  354. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  355. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  356. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  357. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  358. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  359. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  360. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  361. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  362. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  363. package/build/dist/Server/Services/AlertService.js +21 -33
  364. package/build/dist/Server/Services/AlertService.js.map +1 -1
  365. package/build/dist/Server/Services/DatabaseService.js +56 -7
  366. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  367. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  368. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  369. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  370. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  371. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  372. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  373. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  374. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  375. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  376. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  377. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  378. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  379. package/build/dist/Server/Services/IncidentService.js +1148 -42
  380. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  381. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  382. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  383. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  384. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  385. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  386. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  387. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  388. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  389. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  390. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  391. package/build/dist/Server/Services/ScheduledMaintenanceService.js +84 -57
  392. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  393. package/build/dist/Server/Services/SmsService.js +2 -22
  394. package/build/dist/Server/Services/SmsService.js.map +1 -1
  395. package/build/dist/Server/Services/StatusPageService.js +47 -9
  396. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  397. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  398. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  399. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  400. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  401. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  402. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  403. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  404. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  405. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  406. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  407. package/build/dist/Server/Types/Markdown.js +110 -6
  408. package/build/dist/Server/Types/Markdown.js.map +1 -1
  409. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  410. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  411. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  412. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  413. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  414. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  415. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  416. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  417. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  418. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  419. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  420. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  421. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  422. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  425. package/build/dist/Server/Utils/Response.js +30 -0
  426. package/build/dist/Server/Utils/Response.js.map +1 -1
  427. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  428. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  429. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  430. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  431. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  432. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  433. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  434. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  435. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  436. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  437. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  438. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  439. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  440. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  441. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  442. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  443. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  444. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  445. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  446. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  447. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  448. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  449. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  450. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  451. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  452. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  453. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  454. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  455. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  456. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  457. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  458. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  459. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  460. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  461. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  462. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  463. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  464. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  465. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  466. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  467. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  468. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  469. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  470. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  471. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  472. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  473. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  474. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  475. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  476. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  477. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  478. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  479. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  480. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  481. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  482. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  483. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  484. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  485. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  486. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  487. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  488. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  489. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  490. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  491. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  492. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  493. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  494. package/build/dist/Types/SafeHtml.js +84 -0
  495. package/build/dist/Types/SafeHtml.js.map +1 -0
  496. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  497. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  498. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  499. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  500. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  501. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  502. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  503. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  504. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  505. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  506. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  507. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  508. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  509. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  510. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  511. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  512. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  513. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  514. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  515. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  516. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  517. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  518. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  519. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  520. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  521. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  522. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  523. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  524. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  525. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  526. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  527. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  528. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  529. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  530. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  531. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  532. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  533. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  534. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  535. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  536. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  537. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  538. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  539. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  540. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  541. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  542. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  543. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  544. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  545. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  546. package/build/dist/Utils/API.js +177 -1
  547. package/build/dist/Utils/API.js.map +1 -1
  548. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  549. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  550. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  551. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  552. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  553. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  554. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  555. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  556. package/package.json +1 -1
@@ -10,6 +10,14 @@ import QueryHelper from "../Types/Database/QueryHelper";
10
10
  import DatabaseService from "./DatabaseService";
11
11
  import IncidentCustomField from "../../Models/DatabaseModels/IncidentCustomField";
12
12
  import CustomFieldMappingService from "./CustomFieldMappingService";
13
+ import IncidentCustomFieldService from "./IncidentCustomFieldService";
14
+ import { CustomFieldDefinition } from "../../Types/CustomField/CustomFieldDefinition";
15
+ import { mergeTemplateCustomFields } from "../../Types/CustomField/CustomFieldTemplateMerge";
16
+ import {
17
+ CustomFieldValueValidationError,
18
+ formatCustomFieldValueValidationErrors,
19
+ validateCustomFieldValues,
20
+ } from "../../Types/CustomField/CustomFieldValueValidator";
13
21
  import AIRunService from "./AIRunService";
14
22
  import AIRunType from "../../Types/AI/AIRunType";
15
23
  import AIRunStatus from "../../Types/AI/AIRunStatus";
@@ -24,7 +32,9 @@ import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
24
32
  import TeamMemberService from "./TeamMemberService";
25
33
  import UserService from "./UserService";
26
34
  import URL from "../../Types/API/URL";
27
- import { getSloAffectedResourceMarkdownLines } from "../../Utils/Slo/SloAffectedResourceMarkdown";
35
+ import LinkedAffectedResources, {
36
+ LinkedAffectedResource,
37
+ } from "../Utils/AffectedResources/LinkedAffectedResources";
28
38
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
29
39
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
30
40
  import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -46,6 +56,18 @@ import DatabaseBaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/Dat
46
56
  import SloRecordReferenceValidator from "../Utils/Slo/SloRecordReferenceValidator";
47
57
  import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
48
58
  import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
59
+ import IncidentCreatedRenotify from "../../Types/StatusPage/IncidentCreatedRenotify";
60
+ import IncidentCreatedResend from "../../Types/StatusPage/IncidentCreatedResend";
61
+ import IncidentScopeAddedPagesNotification, {
62
+ IncidentScopeAddedPagesNotificationAction,
63
+ StatusPageScopeChange,
64
+ } from "../../Types/StatusPage/IncidentScopeAddedPagesNotification";
65
+ import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
66
+ import StatusPage from "../../Models/DatabaseModels/StatusPage";
67
+ import StatusPageService from "./StatusPageService";
68
+ import StatusPageReadAccess from "../Utils/StatusPage/StatusPageReadAccess";
69
+ import SubscriberNotificationResendAccess from "../Utils/StatusPage/SubscriberNotificationResendAccess";
70
+ import Select from "../Types/Database/Select";
49
71
  import DockerHost from "../../Models/DatabaseModels/DockerHost";
50
72
  import PodmanHost from "../../Models/DatabaseModels/PodmanHost";
51
73
  import Host from "../../Models/DatabaseModels/Host";
@@ -71,6 +93,7 @@ import ServiceType from "../../Types/Telemetry/ServiceType";
71
93
  import OneUptimeDate from "../../Types/Date";
72
94
  import TelemetryUtil from "../Utils/Telemetry/Telemetry";
73
95
  import MetricResourceAttributeUtil from "../../Utils/Metrics/MetricResourceAttributeUtil";
96
+ import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
74
97
  import logger, { LogAttributes } from "../Utils/Logger";
75
98
  import ProductAnalytics from "../Utils/ProductAnalytics";
76
99
  import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
@@ -112,9 +135,39 @@ import IncidentAIContextBuilder, {
112
135
  IncidentContextData,
113
136
  } from "../Utils/AI/IncidentAIContextBuilder";
114
137
  import IncidentAlertService, {
138
+ AcknowledgeDeclaredAlertsResult,
139
+ AlertsToAcknowledgeOnDeclare,
115
140
  LinkAlertsToIncidentResult,
116
141
  } from "./IncidentAlertService";
117
- import { INCIDENT_ALERT_IDS_TO_LINK_KEY } from "../../Types/Incident/IncidentAlertLink";
142
+ import {
143
+ INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
144
+ INCIDENT_ALERT_IDS_TO_LINK_KEY,
145
+ } from "../../Types/Incident/IncidentAlertLink";
146
+
147
+ /*
148
+ * How an update changed an incident's status page scope, for its feed item.
149
+ * notificationQueued: the update also queued the 'created' notification for
150
+ * the added pages (IncidentScopeAddedPagesNotification).
151
+ * notificationAlreadyQueued: that notification was Pending or being sent, so
152
+ * the update left it alone; onUpdateSuccess checks it did not finish without
153
+ * the added pages in the meantime.
154
+ */
155
+ type StatusPageScopeCarryForward = StatusPageScopeChange & {
156
+ isScoped: boolean;
157
+ notificationQueued: boolean;
158
+ notificationAlreadyQueued?: boolean | undefined;
159
+ };
160
+
161
+ /*
162
+ * What adding status pages does to one matched incident's 'created'
163
+ * notification, and the record of told pages to write if it is queued with
164
+ * none (IncidentScopeAddedPagesNotification.getRecordToSeedOnQueue).
165
+ */
166
+ interface AddedStatusPagesDecision {
167
+ incidentId: string;
168
+ action: IncidentScopeAddedPagesNotificationAction;
169
+ recordToSeed: Array<string> | undefined;
170
+ }
118
171
 
119
172
  // key is incidentId for this dictionary.
120
173
  type UpdateCarryForward = Dictionary<{
@@ -122,6 +175,7 @@ type UpdateCarryForward = Dictionary<{
122
175
  monitorsAdded: Array<Monitor>;
123
176
  oldChangeMonitorStatusIdTo: ObjectID | undefined;
124
177
  newMonitorChangeStatusIdTo: ObjectID | undefined;
178
+ statusPageScopeChange?: StatusPageScopeCarryForward | undefined;
125
179
  }>;
126
180
 
127
181
  /*
@@ -131,6 +185,14 @@ type UpdateCarryForward = Dictionary<{
131
185
  type IncidentCreateCarryForward = {
132
186
  // Validated, deduplicated alert ids to link once the incident exists.
133
187
  alertIdsToLink: Array<ObjectID>;
188
+ /*
189
+ * Acknowledge alerts once they are linked, as the declaring user
190
+ * (INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY) - what stops their escalation.
191
+ * When asked to: the project's Acknowledged alert state, and the alerts
192
+ * not acknowledged yet, which the caller was checked for. Null otherwise.
193
+ */
194
+ acknowledgedAlertStateId: ObjectID | null;
195
+ alertIdsToAcknowledge: Array<ObjectID>;
134
196
  } | null;
135
197
 
136
198
  type IncidentUpdatePayload = {
@@ -475,6 +537,19 @@ export class Service extends DatabaseService<Model> {
475
537
  updateBy.data.isVisibleOnStatusPage = false;
476
538
  }
477
539
 
540
+ this.stripServiceOwnedScopeColumns(updateBy);
541
+
542
+ /*
543
+ * Before anything below adds values of its own to customFields (the
544
+ * mapped values copied from monitors), so only what the caller wrote is
545
+ * checked.
546
+ */
547
+ await this.validateCustomFieldValuesOnUpdate(updateBy);
548
+
549
+ // Before the hooks below add a Pending of their own.
550
+ const isCreatedNotificationResendRequested: boolean =
551
+ this.isCreatedNotificationResendRequested(updateBy);
552
+
478
553
  await this.validateProjectScopedReferences(updateBy);
479
554
 
480
555
  const carryForward: UpdateCarryForward = {};
@@ -538,65 +613,1234 @@ export class Service extends DatabaseService<Model> {
538
613
  // check if this monitor is actually added.
539
614
  let isAdded: boolean = true;
540
615
 
541
- for (const existingMonitor of incident.monitors || []) {
542
- if (
543
- existingMonitor._id &&
544
- existingMonitor._id.toString() === monitor._id?.toString()
545
- ) {
546
- isAdded = false;
547
- break;
548
- }
549
- }
616
+ for (const existingMonitor of incident.monitors || []) {
617
+ if (
618
+ existingMonitor._id &&
619
+ existingMonitor._id.toString() === monitor._id?.toString()
620
+ ) {
621
+ isAdded = false;
622
+ break;
623
+ }
624
+ }
625
+
626
+ if (isAdded) {
627
+ // this monitor is added.
628
+ carryForward[incident.id!.toString()]?.monitorsAdded?.push(
629
+ monitor,
630
+ );
631
+ }
632
+ }
633
+ }
634
+ }
635
+ }
636
+
637
+ // Set notification status based on shouldStatusPageSubscribersBeNotifiedOnIncidentCreated if it's being updated
638
+ if (
639
+ updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated !==
640
+ undefined
641
+ ) {
642
+ if (
643
+ updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
644
+ false
645
+ ) {
646
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated =
647
+ StatusPageSubscriberNotificationStatus.Skipped;
648
+ updateBy.data.subscriberNotificationStatusMessage =
649
+ "Notifications skipped as subscribers are not to be notified for this incident.";
650
+ } else if (
651
+ updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
652
+ true
653
+ ) {
654
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated =
655
+ StatusPageSubscriberNotificationStatus.Pending;
656
+ }
657
+ }
658
+
659
+ await this.queueCreatedNotificationResendToAllStatusPagesIfRequested(
660
+ updateBy,
661
+ );
662
+
663
+ /*
664
+ * Retry or Resend - a user's Pending - over a notification that is
665
+ * being sent would let a second run send it alongside, or be
666
+ * overwritten when the send settles (SubscriberNotificationResendAccess).
667
+ * 'Resend to all pages' above has refused that already, with its own
668
+ * reason; the hooks below queue their own Pending only from a settled
669
+ * state.
670
+ */
671
+ await SubscriberNotificationResendAccess.assertNotQueuedWhileBeingSent({
672
+ modelType: Model,
673
+ service: this,
674
+ updateBy: updateBy,
675
+ statusColumns: [
676
+ "subscriberNotificationStatusOnIncidentCreated",
677
+ "subscriberNotificationStatusOnPostmortemPublished",
678
+ ],
679
+ });
680
+
681
+ await this.clearCreatedNotificationRecordOnResend({
682
+ updateBy: updateBy,
683
+ isResendRequested: isCreatedNotificationResendRequested,
684
+ });
685
+
686
+ await this.queueCreatedNotificationOnPublishIfRequested(updateBy);
687
+
688
+ await this.applyStatusPageScopeToUpdate(updateBy, carryForward);
689
+
690
+ /*
691
+ * Re-apply mapped custom field values. Covers the Custom Fields modal
692
+ * saving the whole bag back over a mapped value, and the incident's
693
+ * monitors being added or removed — both change what a mapped field should
694
+ * hold, and folding the answer into this payload keeps it one write with a
695
+ * truthful audit entry.
696
+ */
697
+ await CustomFieldMappingService.applyMappingsToUpdate({
698
+ definitionModelType: IncidentCustomField,
699
+ updateBy: updateBy,
700
+ });
701
+
702
+ return {
703
+ updateBy: updateBy,
704
+ carryForward: carryForward,
705
+ };
706
+ }
707
+
708
+ /*
709
+ * The incidents an update will write to, as they are stored, for the hooks
710
+ * below that decide what else joins the update.
711
+ *
712
+ * Read as root: the answer only decides which columns join this update, and
713
+ * the update itself is still checked against the caller's permissions
714
+ * afterwards. Reading with the caller's props would fail the whole edit for
715
+ * a role that may edit incidents but not read these columns. The query
716
+ * already carries the caller's privacy filter (applyIncidentSelfPrivacyFilter),
717
+ * but not their tenant: the update's permission check adds it only after
718
+ * this hook runs. So a non-root caller's read is limited to their own
719
+ * project here, or a request for another project's incident id would be
720
+ * answered with that incident's state (a refusal that depends on it)
721
+ * instead of the usual "nothing updated".
722
+ */
723
+ private async findIncidentsForUpdateHook(data: {
724
+ updateBy: UpdateBy<Model>;
725
+ select: Select<Model>;
726
+ }): Promise<Array<Model>> {
727
+ const updateBy: UpdateBy<Model> = data.updateBy;
728
+
729
+ const query: Query<Model> =
730
+ !updateBy.props.isRoot && updateBy.props.tenantId
731
+ ? {
732
+ ...updateBy.query,
733
+ projectId: updateBy.props.tenantId,
734
+ }
735
+ : updateBy.query;
736
+
737
+ return await this.findBy({
738
+ query: query,
739
+ select: data.select,
740
+ limit: LIMIT_MAX,
741
+ skip: 0,
742
+ props: {
743
+ isRoot: true,
744
+ },
745
+ });
746
+ }
747
+
748
+ /*
749
+ * CUSTOM FIELD VALUES must fit their fields: a Number field holds a number,
750
+ * a Dropdown one of its options (CustomFieldValueValidator has the rules).
751
+ *
752
+ * Only for writes a user or an API key makes. Root writes - the server's
753
+ * own, such as values copied from monitors or an incident template - are
754
+ * trusted, and the mapped values are added after this runs, so they never
755
+ * reach it. Only the keys a write changes are checked, so a value stored
756
+ * before these checks, or a dropdown option removed since, does not stop
757
+ * someone saving the rest of the card.
758
+ *
759
+ * "Required on create" is not enforced here: incidents created by
760
+ * monitors, the API, Slack, Microsoft Teams or AI cannot fill in a form.
761
+ */
762
+ private async getCustomFieldDefinitionsForValidation(
763
+ projectId: ObjectID,
764
+ ): Promise<Array<CustomFieldDefinition>> {
765
+ const definitions: Array<IncidentCustomField> =
766
+ await IncidentCustomFieldService.findBy({
767
+ query: {
768
+ projectId: projectId,
769
+ },
770
+ select: {
771
+ name: true,
772
+ customFieldType: true,
773
+ dropdownOptions: true,
774
+ },
775
+ limit: LIMIT_PER_PROJECT,
776
+ skip: 0,
777
+ props: {
778
+ isRoot: true,
779
+ },
780
+ });
781
+
782
+ const result: Array<CustomFieldDefinition> = [];
783
+
784
+ for (const definition of definitions) {
785
+ if (typeof definition.name !== "string") {
786
+ continue;
787
+ }
788
+
789
+ result.push({
790
+ name: definition.name,
791
+ customFieldType: definition.customFieldType,
792
+ dropdownOptions: definition.dropdownOptions,
793
+ });
794
+ }
795
+
796
+ return result;
797
+ }
798
+
799
+ private hasCustomFieldValuesToValidate(customFields: unknown): boolean {
800
+ return (
801
+ customFields !== null &&
802
+ typeof customFields === "object" &&
803
+ !Array.isArray(customFields) &&
804
+ Object.keys(customFields as JSONObject).length > 0
805
+ );
806
+ }
807
+
808
+ private throwIfCustomFieldValuesInvalid(
809
+ errors: Array<CustomFieldValueValidationError>,
810
+ ): void {
811
+ if (errors.length > 0) {
812
+ throw new BadDataException(
813
+ formatCustomFieldValueValidationErrors(errors),
814
+ );
815
+ }
816
+ }
817
+
818
+ private async validateCustomFieldValuesOnCreate(data: {
819
+ createBy: CreateBy<Model>;
820
+ projectId: ObjectID;
821
+ }): Promise<void> {
822
+ const customFields: unknown = data.createBy.data.customFields;
823
+
824
+ if (
825
+ data.createBy.props.isRoot ||
826
+ !data.projectId ||
827
+ !this.hasCustomFieldValuesToValidate(customFields)
828
+ ) {
829
+ return;
830
+ }
831
+
832
+ this.throwIfCustomFieldValuesInvalid(
833
+ validateCustomFieldValues({
834
+ definitions: await this.getCustomFieldDefinitionsForValidation(
835
+ data.projectId,
836
+ ),
837
+ customFields: customFields,
838
+ storedCustomFields: {},
839
+ }),
840
+ );
841
+ }
842
+
843
+ /*
844
+ * Checked against each incident the update writes, because what counts as
845
+ * changed depends on what that incident already holds.
846
+ */
847
+ private async validateCustomFieldValuesOnUpdate(
848
+ updateBy: UpdateBy<Model>,
849
+ ): Promise<void> {
850
+ const customFields: unknown = updateBy.data.customFields;
851
+
852
+ if (
853
+ updateBy.props.isRoot ||
854
+ !this.hasCustomFieldValuesToValidate(customFields)
855
+ ) {
856
+ return;
857
+ }
858
+
859
+ const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
860
+ updateBy: updateBy,
861
+ select: {
862
+ _id: true,
863
+ projectId: true,
864
+ customFields: true,
865
+ },
866
+ });
867
+
868
+ const definitionsByProject: Map<
869
+ string,
870
+ Array<CustomFieldDefinition>
871
+ > = new Map<string, Array<CustomFieldDefinition>>();
872
+
873
+ for (const incident of incidents) {
874
+ if (!incident.projectId) {
875
+ continue;
876
+ }
877
+
878
+ const projectKey: string = incident.projectId.toString();
879
+
880
+ if (!definitionsByProject.has(projectKey)) {
881
+ definitionsByProject.set(
882
+ projectKey,
883
+ await this.getCustomFieldDefinitionsForValidation(incident.projectId),
884
+ );
885
+ }
886
+
887
+ this.throwIfCustomFieldValuesInvalid(
888
+ validateCustomFieldValues({
889
+ definitions: definitionsByProject.get(projectKey)!,
890
+ customFields: customFields,
891
+ storedCustomFields: incident.customFields,
892
+ }),
893
+ );
894
+ }
895
+ }
896
+
897
+ /*
898
+ * isScopedToStatusPages and statusPagesNotifiedOnCreation are this
899
+ * service's to write. A client value for either is dropped before any hook
900
+ * runs, so what the hooks write is all that is written: the flag is derived
901
+ * from statusPages (applyStatusPageScopeToUpdate), and the record is the
902
+ * notification job's - a client must not be able to seed it and so keep
903
+ * pages from ever hearing about the incident, or clear it and have them
904
+ * told twice. Root (internal) callers keep the record they send.
905
+ */
906
+ private stripServiceOwnedScopeColumns(updateBy: UpdateBy<Model>): void {
907
+ const data: Dictionary<unknown> = updateBy.data as Dictionary<unknown>;
908
+
909
+ delete data["isScopedToStatusPages"];
910
+
911
+ if (!updateBy.props.isRoot) {
912
+ delete data["statusPagesNotifiedOnCreation"];
913
+ }
914
+ }
915
+
916
+ /*
917
+ * Whether an update itself asks for the 'created' notification to go out
918
+ * again: it sets the status to Pending - the API route of resending it,
919
+ * and the dashboard's Retry - turns notifying on creation on, which
920
+ * onBeforeUpdate maps to Pending, or asks for it to be sent to every
921
+ * status page again (IncidentCreatedResend, the dashboard's Resend). Read
922
+ * before any hook adds a Pending of its own.
923
+ */
924
+ private isCreatedNotificationResendRequested(
925
+ updateBy: UpdateBy<Model>,
926
+ ): boolean {
927
+ return (
928
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
929
+ StatusPageSubscriberNotificationStatus.Pending ||
930
+ updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
931
+ true ||
932
+ IncidentCreatedResend.isRequested(updateBy.miscDataProps)
933
+ );
934
+ }
935
+
936
+ /*
937
+ * 'Resend to all pages' (see IncidentCreatedResend): the 'created'
938
+ * notification goes back to Pending with a message saying so, and
939
+ * clearCreatedNotificationRecordOnResend empties its record of told pages
940
+ * in the same write, so the job sends it to every page the incident
941
+ * reaches now - after a failure too, where a plain Retry would resume.
942
+ *
943
+ * Everything is written into the caller's own update, so it lands with the
944
+ * rest of it or not at all, and the column check that runs after this hook
945
+ * still decides whether the caller may write the status. That check is
946
+ * also made here first, before the incidents are read: the refusals below
947
+ * depend on the notification's state, and a caller who may not send it
948
+ * again is told only that. The incidents are then read with the caller's
949
+ * own permissions (and the privacy filter already on the query), so an
950
+ * incident they cannot see never decides the answer; when they can see
951
+ * none of those the update matches, nothing is queued.
952
+ *
953
+ * Refused - with the reason, rather than silently dropped, because asking
954
+ * for it is the whole point of the request - when any matched incident's
955
+ * notification did not go out (Skipped) or is on its way (Pending,
956
+ * InProgress), and when the same update sets the status to anything but
957
+ * Pending.
958
+ */
959
+ private async queueCreatedNotificationResendToAllStatusPagesIfRequested(
960
+ updateBy: UpdateBy<Model>,
961
+ ): Promise<void> {
962
+ if (!IncidentCreatedResend.isRequested(updateBy.miscDataProps)) {
963
+ return;
964
+ }
965
+
966
+ const requestedStatus: unknown =
967
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated;
968
+
969
+ if (
970
+ requestedStatus !== undefined &&
971
+ requestedStatus !== StatusPageSubscriberNotificationStatus.Pending
972
+ ) {
973
+ throw new BadDataException(
974
+ IncidentCreatedResend.conflictingStatusMessage,
975
+ );
976
+ }
977
+
978
+ SubscriberNotificationResendAccess.assertCallerMayUpdateColumns({
979
+ modelType: Model,
980
+ columns: {
981
+ subscriberNotificationStatusOnIncidentCreated:
982
+ StatusPageSubscriberNotificationStatus.Pending,
983
+ subscriberNotificationStatusMessage:
984
+ IncidentCreatedResend.queuedMessage,
985
+ statusPagesNotifiedOnCreation: [],
986
+ },
987
+ props: updateBy.props,
988
+ refusal: IncidentCreatedResend.noPermissionMessage,
989
+ });
990
+
991
+ const incidents: Array<Model> = await this.findBy({
992
+ query: updateBy.query,
993
+ select: {
994
+ _id: true,
995
+ subscriberNotificationStatusOnIncidentCreated: true,
996
+ },
997
+ limit: LIMIT_MAX,
998
+ skip: 0,
999
+ props: updateBy.props,
1000
+ });
1001
+
1002
+ for (const incident of incidents) {
1003
+ const refusal: string | null = IncidentCreatedResend.getRefusalReason(
1004
+ incident.subscriberNotificationStatusOnIncidentCreated,
1005
+ );
1006
+
1007
+ if (refusal) {
1008
+ throw new BadDataException(refusal);
1009
+ }
1010
+ }
1011
+
1012
+ if (incidents.length === 0) {
1013
+ return;
1014
+ }
1015
+
1016
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated =
1017
+ StatusPageSubscriberNotificationStatus.Pending;
1018
+
1019
+ if (updateBy.data.subscriberNotificationStatusMessage === undefined) {
1020
+ updateBy.data.subscriberNotificationStatusMessage =
1021
+ IncidentCreatedResend.queuedMessage;
1022
+ }
1023
+ }
1024
+
1025
+ /*
1026
+ * A resend asked for by the update itself (see
1027
+ * isCreatedNotificationResendRequested) goes to every status page, as it
1028
+ * always did. The job skips the pages in its record of told pages
1029
+ * (Incident.statusPagesNotifiedOnCreation), so without this a resend of a
1030
+ * notification that went out would reach nobody: the record is emptied in
1031
+ * the same write.
1032
+ *
1033
+ * It is kept in three cases:
1034
+ * - Failed: Retry resumes where the failed send stopped, so the pages it
1035
+ * told are not told twice - unless the update asks for it to go to every
1036
+ * page again (IncidentCreatedResend), which is what that request is for;
1037
+ * - Pending or InProgress: the notification is on its way already, and its
1038
+ * record is what keeps a send queued for added pages from reaching the
1039
+ * pages told before (IncidentCreatedResend refuses these);
1040
+ * - a root caller that writes the record itself.
1041
+ *
1042
+ * An update matching several incidents empties the record when any of them
1043
+ * would otherwise be resent to nobody, because the write applies the same
1044
+ * data to all of them.
1045
+ */
1046
+ private async clearCreatedNotificationRecordOnResend(data: {
1047
+ updateBy: UpdateBy<Model>;
1048
+ isResendRequested: boolean;
1049
+ }): Promise<void> {
1050
+ const updateBy: UpdateBy<Model> = data.updateBy;
1051
+
1052
+ if (!data.isResendRequested) {
1053
+ return;
1054
+ }
1055
+
1056
+ if (
1057
+ (updateBy.data as Dictionary<unknown>)[
1058
+ "statusPagesNotifiedOnCreation"
1059
+ ] !== undefined
1060
+ ) {
1061
+ return;
1062
+ }
1063
+
1064
+ if (IncidentCreatedResend.isRequested(updateBy.miscDataProps)) {
1065
+ /*
1066
+ * Queued for every page by
1067
+ * queueCreatedNotificationResendToAllStatusPagesIfRequested, which
1068
+ * left the status alone when the caller can see none of the incidents
1069
+ * the update matches: nothing to read to decide.
1070
+ */
1071
+ if (
1072
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
1073
+ StatusPageSubscriberNotificationStatus.Pending
1074
+ ) {
1075
+ updateBy.data.statusPagesNotifiedOnCreation = [];
1076
+ }
1077
+
1078
+ return;
1079
+ }
1080
+
1081
+ const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
1082
+ updateBy: updateBy,
1083
+ select: {
1084
+ _id: true,
1085
+ subscriberNotificationStatusOnIncidentCreated: true,
1086
+ },
1087
+ });
1088
+
1089
+ const someIncidentWouldReachNobody: boolean = incidents.some(
1090
+ (incident: Model): boolean => {
1091
+ return ![
1092
+ StatusPageSubscriberNotificationStatus.Failed,
1093
+ StatusPageSubscriberNotificationStatus.Pending,
1094
+ StatusPageSubscriberNotificationStatus.InProgress,
1095
+ ].includes(
1096
+ incident.subscriberNotificationStatusOnIncidentCreated as StatusPageSubscriberNotificationStatus,
1097
+ );
1098
+ },
1099
+ );
1100
+
1101
+ if (someIncidentWouldReachNobody) {
1102
+ updateBy.data.statusPagesNotifiedOnCreation = [];
1103
+ }
1104
+ }
1105
+
1106
+ /*
1107
+ * Publishing a hidden incident can announce it: when the editor turns
1108
+ * 'Visible on Status Page' on and ticks "Notify subscribers that this
1109
+ * incident was created" (see IncidentCreatedRenotify), the incident's
1110
+ * 'created' notification goes back to Pending, and the
1111
+ * Incident:SendNotificationToSubscribers job sends it now that the incident
1112
+ * is visible - to the pages its record of told pages does not list, so an
1113
+ * incident whose notification went out before it was hidden tells only the
1114
+ * pages added while it was hidden.
1115
+ *
1116
+ * It only ever re-queues a notification with someone left to tell, for an
1117
+ * incident that is hidden today, not private, and meant to notify on
1118
+ * creation, so a stray request cannot email subscribers twice or break a
1119
+ * deliberate "don't notify". The status is written into this same update,
1120
+ * so it lands together with the visibility change or not at all. The
1121
+ * column (and its message) are computed, but their update ACL lists the
1122
+ * incident roles, so a non-root edit still passes the column check that
1123
+ * runs after this hook.
1124
+ *
1125
+ * An update that sets the status itself - the API route of resetting it to
1126
+ * Pending, or the notify-on-create flag above - is left alone. An update
1127
+ * that matches several incidents queues only when every one of them
1128
+ * qualifies, because the write applies the same data to all of them.
1129
+ */
1130
+ private async queueCreatedNotificationOnPublishIfRequested(
1131
+ updateBy: UpdateBy<Model>,
1132
+ ): Promise<void> {
1133
+ if (!IncidentCreatedRenotify.isRequested(updateBy.miscDataProps)) {
1134
+ return;
1135
+ }
1136
+
1137
+ if (updateBy.data.isVisibleOnStatusPage !== true) {
1138
+ return;
1139
+ }
1140
+
1141
+ if (
1142
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated !== undefined
1143
+ ) {
1144
+ return;
1145
+ }
1146
+
1147
+ const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
1148
+ updateBy: updateBy,
1149
+ select: {
1150
+ _id: true,
1151
+ isVisibleOnStatusPage: true,
1152
+ isPrivate: true,
1153
+ subscriberNotificationStatusOnIncidentCreated: true,
1154
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
1155
+ statusPagesNotifiedOnCreation: true,
1156
+ statusPages: {
1157
+ _id: true,
1158
+ },
1159
+ },
1160
+ });
1161
+
1162
+ if (incidents.length === 0) {
1163
+ return;
1164
+ }
1165
+
1166
+ const everyIncidentQualifies: boolean = incidents.every(
1167
+ (incident: Model): boolean => {
1168
+ return IncidentCreatedRenotify.canRenotifyOnPublish({
1169
+ isVisibleOnStatusPage: incident.isVisibleOnStatusPage,
1170
+ isPrivate:
1171
+ updateBy.data.isPrivate !== undefined &&
1172
+ updateBy.data.isPrivate !== null
1173
+ ? (updateBy.data.isPrivate as boolean)
1174
+ : incident.isPrivate,
1175
+ subscriberNotificationStatusOnIncidentCreated:
1176
+ incident.subscriberNotificationStatusOnIncidentCreated,
1177
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated:
1178
+ incident.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
1179
+ // The pages it is limited to once this update is written.
1180
+ statusPages:
1181
+ updateBy.data.statusPages !== undefined
1182
+ ? updateBy.data.statusPages
1183
+ : incident.statusPages,
1184
+ statusPagesNotifiedOnCreation: incident.statusPagesNotifiedOnCreation,
1185
+ });
1186
+ },
1187
+ );
1188
+
1189
+ if (!everyIncidentQualifies) {
1190
+ logger.debug(
1191
+ `Not re-queueing the incident created notification on publish: ${incidents.length} incident(s) matched and not all of them were hidden, set to notify subscribers and had subscribers left to tell.`,
1192
+ );
1193
+ return;
1194
+ }
1195
+
1196
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated =
1197
+ StatusPageSubscriberNotificationStatus.Pending;
1198
+ updateBy.data.subscriberNotificationStatusMessage =
1199
+ IncidentCreatedRenotify.queuedMessage;
1200
+ }
1201
+
1202
+ /*
1203
+ * The status pages a new incident is limited to (Incident.statusPages), and
1204
+ * the two columns that follow from them.
1205
+ *
1206
+ * isScopedToStatusPages is derived from the list, whatever the caller sent
1207
+ * for it: the column is computed, which exempts it from the column check on
1208
+ * create, so a client value would otherwise be stored as is - and a scoped
1209
+ * flag with no pages hides the incident from every status page.
1210
+ *
1211
+ * statusPagesNotifiedOnCreation is the created-notification job's record of
1212
+ * the pages it told. A client must not be able to seed it and so keep pages
1213
+ * from ever hearing about the incident; only root (internal) callers keep
1214
+ * what they send.
1215
+ *
1216
+ * The pages a non-root caller picks must be pages they can read (see
1217
+ * StatusPageReadAccess), except the pages of an incident template they can
1218
+ * read: the dashboard fills a template's pages in for everyone who declares
1219
+ * from it, which is what the template is for. Pages copied from a template
1220
+ * on the server are not the caller's pick. That is checked once the pages
1221
+ * are known to be the project's (assertCallerCanPickStatusPagesOnCreate).
1222
+ *
1223
+ * `isScopedToNothingByTemplate`: declared through the API from a template
1224
+ * whose status pages were all deleted. The incident is scoped with no
1225
+ * pages - hidden from every status page - rather than unscoped.
1226
+ */
1227
+ private applyStatusPageScopeToCreate(data: {
1228
+ createBy: CreateBy<Model>;
1229
+ isScopedToNothingByTemplate?: boolean | undefined;
1230
+ }): void {
1231
+ const createBy: CreateBy<Model> = data.createBy;
1232
+
1233
+ if (!createBy.props.isRoot) {
1234
+ delete createBy.data.statusPagesNotifiedOnCreation;
1235
+ }
1236
+
1237
+ const statusPageIds: Array<string> =
1238
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1239
+ createBy.data.statusPages,
1240
+ );
1241
+
1242
+ if (
1243
+ createBy.data.statusPages !== undefined &&
1244
+ createBy.data.statusPages !== null
1245
+ ) {
1246
+ createBy.data.statusPages = this.toStatusPageStubs(statusPageIds);
1247
+ }
1248
+
1249
+ createBy.data.isScopedToStatusPages =
1250
+ statusPageIds.length > 0 || data.isScopedToNothingByTemplate === true;
1251
+ }
1252
+
1253
+ /*
1254
+ * The pages a non-root caller picks for a new incident must be pages they
1255
+ * can read (see applyStatusPageScopeToCreate). Runs after the project check,
1256
+ * so another project's page, or one that does not exist, is reported as
1257
+ * such rather than as a page the caller cannot read.
1258
+ */
1259
+ private async assertCallerCanPickStatusPagesOnCreate(data: {
1260
+ createBy: CreateBy<Model>;
1261
+ projectId: ObjectID;
1262
+ statusPagesFromCaller: boolean;
1263
+ }): Promise<void> {
1264
+ const createBy: CreateBy<Model> = data.createBy;
1265
+
1266
+ const statusPageIds: Array<string> =
1267
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1268
+ createBy.data.statusPages,
1269
+ );
1270
+
1271
+ if (
1272
+ createBy.props.isRoot ||
1273
+ !data.statusPagesFromCaller ||
1274
+ statusPageIds.length === 0
1275
+ ) {
1276
+ return;
1277
+ }
1278
+
1279
+ const readableStatusPageIds: Array<string> =
1280
+ await StatusPageReadAccess.getReadableStatusPageIds({
1281
+ statusPageIds: statusPageIds,
1282
+ props: createBy.props,
1283
+ });
1284
+
1285
+ let unreadableStatusPageIds: Array<string> = statusPageIds.filter(
1286
+ (id: string): boolean => {
1287
+ return !readableStatusPageIds.includes(id);
1288
+ },
1289
+ );
1290
+
1291
+ if (unreadableStatusPageIds.length > 0) {
1292
+ const templateStatusPageIds: Array<string> =
1293
+ await this.getStatusPageIdsOfTemplatesReadableByCaller({
1294
+ projectId: data.projectId,
1295
+ props: createBy.props,
1296
+ });
1297
+
1298
+ unreadableStatusPageIds = unreadableStatusPageIds.filter(
1299
+ (id: string): boolean => {
1300
+ return !templateStatusPageIds.includes(id);
1301
+ },
1302
+ );
1303
+ }
1304
+
1305
+ if (unreadableStatusPageIds.length > 0) {
1306
+ throw new NotAuthorizedException(
1307
+ StatusPageReadAccess.getRefusalMessage("incident"),
1308
+ );
1309
+ }
1310
+ }
1311
+
1312
+ /*
1313
+ * The status pages the incident templates the caller can read are limited
1314
+ * to. Such a page may be picked for a new incident without reading it:
1315
+ * declaring from the template picks it anyway. The templates are read with
1316
+ * the caller's permissions (and labels); their pages as root. A caller who
1317
+ * cannot read templates at all gets none.
1318
+ */
1319
+ private async getStatusPageIdsOfTemplatesReadableByCaller(data: {
1320
+ projectId: ObjectID;
1321
+ props: DatabaseCommonInteractionProps;
1322
+ }): Promise<Array<string>> {
1323
+ let templates: Array<IncidentTemplate> = [];
1324
+
1325
+ try {
1326
+ templates = await IncidentTemplateService.findBy({
1327
+ query: {
1328
+ projectId: data.projectId,
1329
+ },
1330
+ select: {
1331
+ _id: true,
1332
+ },
1333
+ limit: LIMIT_MAX,
1334
+ skip: 0,
1335
+ props: data.props,
1336
+ });
1337
+ } catch (err) {
1338
+ // No access to templates (or not on a plan that has them): no pages.
1339
+ logger.debug(
1340
+ `Could not read the caller's incident templates while checking the status pages of a new incident: ${err}`,
1341
+ );
1342
+ return [];
1343
+ }
1344
+
1345
+ if (templates.length === 0) {
1346
+ return [];
1347
+ }
1348
+
1349
+ const templatesWithPages: Array<IncidentTemplate> =
1350
+ await IncidentTemplateService.findBy({
1351
+ query: {
1352
+ _id: QueryHelper.any(
1353
+ templates.map((template: IncidentTemplate): string => {
1354
+ return template.id!.toString();
1355
+ }),
1356
+ ),
1357
+ projectId: data.projectId,
1358
+ },
1359
+ select: {
1360
+ _id: true,
1361
+ statusPages: {
1362
+ _id: true,
1363
+ },
1364
+ },
1365
+ limit: LIMIT_MAX,
1366
+ skip: 0,
1367
+ props: {
1368
+ isRoot: true,
1369
+ },
1370
+ });
1371
+
1372
+ return IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1373
+ templatesWithPages.flatMap(
1374
+ (template: IncidentTemplate): Array<StatusPage> => {
1375
+ return template.statusPages || [];
1376
+ },
1377
+ ),
1378
+ );
1379
+ }
1380
+
1381
+ /*
1382
+ * The status pages an incident is limited to, on update. Everything that
1383
+ * follows from a write to Incident.statusPages happens here, in the
1384
+ * caller's own update, so the list, the flag and a re-queued notification
1385
+ * land together or not at all:
1386
+ *
1387
+ * - isScopedToStatusPages is derived from the list the update writes (a
1388
+ * value the caller sent for it was dropped by
1389
+ * stripServiceOwnedScopeColumns). Nothing else ever sets it, and it is
1390
+ * never recomputed when join rows disappear: deleting the only page an
1391
+ * incident is scoped to leaves it scoped to nothing, hidden from every
1392
+ * page, rather than widened to all of them;
1393
+ * - a non-root caller's list is topped up with the pages the incident is
1394
+ * scoped to that the caller cannot read. Status pages are label-scoped,
1395
+ * so an editor may see only some of an incident's pages, and a list write
1396
+ * replaces the whole list (the dashboard sends back what the editor
1397
+ * sees). Without this, saving the incident would silently drop every page
1398
+ * the editor was not shown;
1399
+ * - the pages a non-root caller adds must be pages they can read (see
1400
+ * StatusPageReadAccess);
1401
+ * - with IncidentScopeAddedPagesNotification requested, the 'created'
1402
+ * notification is queued again for the pages the update adds;
1403
+ * - what was added and removed is carried forward for the feed item
1404
+ * onUpdateSuccess writes, and so is whether the notification was on its
1405
+ * way already, which onUpdateSuccess checks again once the pages are
1406
+ * written.
1407
+ *
1408
+ * The flag and the notification columns are computed, but their update
1409
+ * access control lists every role that may edit statusPages, so a non-root
1410
+ * edit passes the column check that runs after this hook.
1411
+ */
1412
+ private async applyStatusPageScopeToUpdate(
1413
+ updateBy: UpdateBy<Model>,
1414
+ carryForward: UpdateCarryForward,
1415
+ ): Promise<void> {
1416
+ const data: Dictionary<unknown> = updateBy.data as Dictionary<unknown>;
1417
+
1418
+ if (data["statusPages"] === undefined) {
1419
+ return;
1420
+ }
1421
+
1422
+ const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
1423
+ updateBy: updateBy,
1424
+ select: {
1425
+ _id: true,
1426
+ statusPages: {
1427
+ _id: true,
1428
+ },
1429
+ isVisibleOnStatusPage: true,
1430
+ isPrivate: true,
1431
+ subscriberNotificationStatusOnIncidentCreated: true,
1432
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
1433
+ statusPagesNotifiedOnCreation: true,
1434
+ },
1435
+ });
1436
+
1437
+ const requestedStatusPageIds: Array<string> =
1438
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1439
+ data["statusPages"],
1440
+ );
1441
+
1442
+ const hiddenStatusPageIds: Array<string> = updateBy.props.isRoot
1443
+ ? []
1444
+ : await this.getScopedStatusPageIdsHiddenFromCaller({
1445
+ incidents: incidents,
1446
+ props: updateBy.props,
1447
+ });
1448
+
1449
+ const statusPageIds: Array<string> = [
1450
+ ...requestedStatusPageIds,
1451
+ ...hiddenStatusPageIds.filter((id: string) => {
1452
+ return !requestedStatusPageIds.includes(id);
1453
+ }),
1454
+ ];
1455
+
1456
+ if (!updateBy.props.isRoot) {
1457
+ await StatusPageReadAccess.assertCallerCanPickStatusPages({
1458
+ statusPageIds: this.getStatusPageIdsAddedToAny({
1459
+ incidents: incidents,
1460
+ statusPageIds: statusPageIds,
1461
+ }),
1462
+ props: updateBy.props,
1463
+ subject: "incident",
1464
+ });
1465
+ }
1466
+
1467
+ data["statusPages"] = this.toStatusPageStubs(statusPageIds);
1468
+ data["isScopedToStatusPages"] = statusPageIds.length > 0;
1469
+
1470
+ const decisions: Array<AddedStatusPagesDecision> = [];
1471
+
1472
+ for (const incident of incidents) {
1473
+ const incidentId: string = incident.id!.toString();
1474
+
1475
+ const change: StatusPageScopeChange =
1476
+ IncidentScopeAddedPagesNotification.getScopeChange({
1477
+ before: incident.statusPages,
1478
+ after: statusPageIds,
1479
+ });
1480
+
1481
+ const action: IncidentScopeAddedPagesNotificationAction =
1482
+ IncidentScopeAddedPagesNotification.getAction({
1483
+ addedStatusPageIds: change.addedStatusPageIds,
1484
+ incident: {
1485
+ subscriberNotificationStatusOnIncidentCreated:
1486
+ incident.subscriberNotificationStatusOnIncidentCreated,
1487
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated:
1488
+ incident.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
1489
+ isVisibleOnStatusPage: this.getValueAfterUpdate(
1490
+ updateBy.data.isVisibleOnStatusPage,
1491
+ incident.isVisibleOnStatusPage,
1492
+ ),
1493
+ isPrivate: this.getValueAfterUpdate(
1494
+ updateBy.data.isPrivate,
1495
+ incident.isPrivate,
1496
+ ),
1497
+ statusPagesNotifiedOnCreation:
1498
+ incident.statusPagesNotifiedOnCreation,
1499
+ },
1500
+ });
1501
+
1502
+ carryForward[incidentId] = {
1503
+ monitorsRemoved: [],
1504
+ monitorsAdded: [],
1505
+ oldChangeMonitorStatusIdTo: undefined,
1506
+ newMonitorChangeStatusIdTo: undefined,
1507
+ ...carryForward[incidentId],
1508
+ statusPageScopeChange: {
1509
+ ...change,
1510
+ isScoped: statusPageIds.length > 0,
1511
+ notificationQueued: false,
1512
+ /*
1513
+ * On its way already, and nothing in this update sets the status:
1514
+ * onUpdateSuccess makes sure the send did not finish without the
1515
+ * added pages in the meantime.
1516
+ */
1517
+ notificationAlreadyQueued:
1518
+ action ===
1519
+ IncidentScopeAddedPagesNotificationAction.AlreadyQueued &&
1520
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
1521
+ undefined,
1522
+ },
1523
+ };
1524
+
1525
+ decisions.push({
1526
+ incidentId: incidentId,
1527
+ action: action,
1528
+ recordToSeed:
1529
+ action === IncidentScopeAddedPagesNotificationAction.Queue
1530
+ ? IncidentScopeAddedPagesNotification.getRecordToSeedOnQueue({
1531
+ statusPagesNotifiedOnCreation:
1532
+ incident.statusPagesNotifiedOnCreation,
1533
+ statusPageIdsBeforeUpdate: incident.statusPages,
1534
+ })
1535
+ : undefined,
1536
+ });
1537
+ }
1538
+
1539
+ this.queueCreatedNotificationForAddedStatusPagesIfRequested({
1540
+ updateBy: updateBy,
1541
+ decisions: decisions,
1542
+ carryForward: carryForward,
1543
+ });
1544
+ }
1545
+
1546
+ /*
1547
+ * The pages an update writing `statusPageIds` adds to at least one of the
1548
+ * incidents it matches.
1549
+ */
1550
+ private getStatusPageIdsAddedToAny(data: {
1551
+ incidents: Array<Model>;
1552
+ statusPageIds: Array<string>;
1553
+ }): Array<string> {
1554
+ const added: Array<string> = [];
1555
+
1556
+ for (const incident of data.incidents) {
1557
+ for (const id of IncidentScopeAddedPagesNotification.getScopeChange({
1558
+ before: incident.statusPages,
1559
+ after: data.statusPageIds,
1560
+ }).addedStatusPageIds) {
1561
+ if (!added.includes(id)) {
1562
+ added.push(id);
1563
+ }
1564
+ }
1565
+ }
1566
+
1567
+ return added;
1568
+ }
1569
+
1570
+ /*
1571
+ * Adding status pages to an incident's scope can tell them the incident was
1572
+ * created: with IncidentScopeAddedPagesNotification requested, the 'created'
1573
+ * notification goes back to Pending, and the job sends it to the pages it
1574
+ * has no record of telling (Incident.statusPagesNotifiedOnCreation). See
1575
+ * IncidentScopeAddedPagesNotification.getAction for when it applies.
1576
+ *
1577
+ * A notification that was never sent has no record yet, so the pages the
1578
+ * incident was limited to before are written in as the record in the same
1579
+ * write (getRecordToSeedOnQueue): the job then tells only the added pages.
1580
+ *
1581
+ * An update that sets the status itself - the API route of resetting it to
1582
+ * Pending, the notify-on-create flag, or publishing a hidden incident - is
1583
+ * left alone. An update that matches several incidents queues only when it
1584
+ * may for every one of them, because the write applies the same data to
1585
+ * all. For the same reason it cannot write a record that differs between
1586
+ * them, or overwrite the record of an incident that has one, and is refused
1587
+ * instead.
1588
+ */
1589
+ private queueCreatedNotificationForAddedStatusPagesIfRequested(data: {
1590
+ updateBy: UpdateBy<Model>;
1591
+ decisions: Array<AddedStatusPagesDecision>;
1592
+ carryForward: UpdateCarryForward;
1593
+ }): void {
1594
+ const updateBy: UpdateBy<Model> = data.updateBy;
1595
+
1596
+ if (
1597
+ !IncidentScopeAddedPagesNotification.isRequested(updateBy.miscDataProps)
1598
+ ) {
1599
+ return;
1600
+ }
1601
+
1602
+ if (
1603
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated !== undefined
1604
+ ) {
1605
+ return;
1606
+ }
1607
+
1608
+ const actions: Array<IncidentScopeAddedPagesNotificationAction> =
1609
+ data.decisions.map(
1610
+ (
1611
+ decision: AddedStatusPagesDecision,
1612
+ ): IncidentScopeAddedPagesNotificationAction => {
1613
+ return decision.action;
1614
+ },
1615
+ );
1616
+
1617
+ if (!actions.includes(IncidentScopeAddedPagesNotificationAction.Queue)) {
1618
+ return;
1619
+ }
1620
+
1621
+ const everyIncidentMayQueue: boolean = actions.every(
1622
+ (action: IncidentScopeAddedPagesNotificationAction): boolean => {
1623
+ return (
1624
+ action === IncidentScopeAddedPagesNotificationAction.Queue ||
1625
+ action === IncidentScopeAddedPagesNotificationAction.AlreadyQueued
1626
+ );
1627
+ },
1628
+ );
1629
+
1630
+ if (!everyIncidentMayQueue) {
1631
+ logger.debug(
1632
+ `Not queueing the incident created notification for added status pages: ${actions.length} incident(s) matched and not all of them gained status pages and may be announced.`,
1633
+ );
1634
+ return;
1635
+ }
1636
+
1637
+ const recordsToSeed: Array<Array<string>> = data.decisions
1638
+ .map((decision: AddedStatusPagesDecision): Array<string> | undefined => {
1639
+ return decision.recordToSeed;
1640
+ })
1641
+ .filter((record: Array<string> | undefined): boolean => {
1642
+ return record !== undefined;
1643
+ }) as Array<Array<string>>;
1644
+
1645
+ if (recordsToSeed.length > 0) {
1646
+ const recordToSeed: Array<string> = recordsToSeed[0]!;
1647
+
1648
+ const everyIncidentTakesTheSameRecord: boolean =
1649
+ recordsToSeed.length === data.decisions.length &&
1650
+ recordsToSeed.every((record: Array<string>): boolean => {
1651
+ return (
1652
+ [...record].sort().join(",") === [...recordToSeed].sort().join(",")
1653
+ );
1654
+ });
1655
+
1656
+ if (!everyIncidentTakesTheSameRecord) {
1657
+ throw new BadDataException(
1658
+ "These incidents cannot be sent the incident-created notification for their added status pages together: they were limited to different status pages, or not all of them were ever announced. Change them one at a time.",
1659
+ );
1660
+ }
1661
+
1662
+ updateBy.data.statusPagesNotifiedOnCreation = recordToSeed;
1663
+ }
1664
+
1665
+ updateBy.data.subscriberNotificationStatusOnIncidentCreated =
1666
+ StatusPageSubscriberNotificationStatus.Pending;
1667
+ updateBy.data.subscriberNotificationStatusMessage =
1668
+ IncidentScopeAddedPagesNotification.queuedMessage;
1669
+
1670
+ for (const decision of data.decisions) {
1671
+ const scopeChange: StatusPageScopeCarryForward | undefined =
1672
+ data.carryForward[decision.incidentId]?.statusPageScopeChange;
1673
+
1674
+ if (
1675
+ scopeChange &&
1676
+ decision.action === IncidentScopeAddedPagesNotificationAction.Queue
1677
+ ) {
1678
+ scopeChange.notificationQueued = true;
1679
+ }
1680
+ }
1681
+ }
1682
+
1683
+ /*
1684
+ * An update that added pages while the 'created' notification was Pending
1685
+ * or being sent left the status alone: the queued send reads the scope when
1686
+ * it goes out, and a running send reads it again once it has finished (see
1687
+ * the Incident:SendNotificationToSubscribers job). One case slips between
1688
+ * the two: the whole send started after this update read the status and
1689
+ * finished before it wrote the pages. Once the pages are written, this
1690
+ * checks for exactly that - the send finished, and an added page is not in
1691
+ * its record - and queues the notification again for the pages it missed.
1692
+ *
1693
+ * Never throws: the update is already written.
1694
+ */
1695
+ private async requeueCreatedNotificationIfAddedPagesWereMissed(data: {
1696
+ incidentId: ObjectID;
1697
+ change: StatusPageScopeCarryForward | undefined;
1698
+ }): Promise<void> {
1699
+ if (
1700
+ !data.change?.notificationAlreadyQueued ||
1701
+ data.change.addedStatusPageIds.length === 0
1702
+ ) {
1703
+ return;
1704
+ }
1705
+
1706
+ try {
1707
+ const incident: Model | null = await this.findOneById({
1708
+ id: data.incidentId,
1709
+ select: {
1710
+ subscriberNotificationStatusOnIncidentCreated: true,
1711
+ statusPagesNotifiedOnCreation: true,
1712
+ },
1713
+ props: {
1714
+ isRoot: true,
1715
+ },
1716
+ });
1717
+
1718
+ if (
1719
+ incident?.subscriberNotificationStatusOnIncidentCreated !==
1720
+ StatusPageSubscriberNotificationStatus.Success
1721
+ ) {
1722
+ // Still on its way (it will see the pages), or settled otherwise.
1723
+ return;
1724
+ }
1725
+
1726
+ const recorded: Array<string> =
1727
+ IncidentScopeAddedPagesNotification.getRecordedStatusPageIds(
1728
+ incident.statusPagesNotifiedOnCreation,
1729
+ ) || [];
1730
+
1731
+ const missed: boolean = data.change.addedStatusPageIds.some(
1732
+ (id: string): boolean => {
1733
+ return !recorded.includes(id);
1734
+ },
1735
+ );
1736
+
1737
+ if (!missed) {
1738
+ return;
1739
+ }
1740
+
1741
+ await this.updateOneById({
1742
+ id: data.incidentId,
1743
+ data: {
1744
+ subscriberNotificationStatusOnIncidentCreated:
1745
+ StatusPageSubscriberNotificationStatus.Pending,
1746
+ subscriberNotificationStatusMessage:
1747
+ IncidentScopeAddedPagesNotification.queuedMessage,
1748
+ },
1749
+ props: {
1750
+ isRoot: true,
1751
+ ignoreHooks: true,
1752
+ },
1753
+ });
1754
+ } catch (err) {
1755
+ logger.error(
1756
+ `Failed to check whether the incident created notification reached the status pages added to the incident: ${err}`,
1757
+ {
1758
+ incidentId: data.incidentId?.toString(),
1759
+ } as LogAttributes,
1760
+ );
1761
+ }
1762
+ }
1763
+
1764
+ /*
1765
+ * The pages the matched incidents are scoped to that the caller cannot
1766
+ * read, which a list write from them must keep (see
1767
+ * applyStatusPageScopeToUpdate). A bulk update writes the same list onto
1768
+ * every incident it matches, so it can only keep them when every matched
1769
+ * incident holds the same hidden pages; otherwise it would scope some of
1770
+ * them to pages they were never scoped to, and it is refused.
1771
+ */
1772
+ private async getScopedStatusPageIdsHiddenFromCaller(data: {
1773
+ incidents: Array<Model>;
1774
+ props: DatabaseCommonInteractionProps;
1775
+ }): Promise<Array<string>> {
1776
+ const heldByIncident: Array<Array<string>> = data.incidents.map(
1777
+ (incident: Model) => {
1778
+ return IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1779
+ incident.statusPages,
1780
+ );
1781
+ },
1782
+ );
1783
+
1784
+ const heldIds: Array<string> =
1785
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1786
+ heldByIncident.flat(),
1787
+ );
550
1788
 
551
- if (isAdded) {
552
- // this monitor is added.
553
- carryForward[incident.id!.toString()]?.monitorsAdded?.push(
554
- monitor,
555
- );
556
- }
557
- }
558
- }
559
- }
1789
+ if (heldIds.length === 0) {
1790
+ return [];
560
1791
  }
561
1792
 
562
- // Set notification status based on shouldStatusPageSubscribersBeNotifiedOnIncidentCreated if it's being updated
563
- if (
564
- updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated !==
565
- undefined
566
- ) {
567
- if (
568
- updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
569
- false
570
- ) {
571
- updateBy.data.subscriberNotificationStatusOnIncidentCreated =
572
- StatusPageSubscriberNotificationStatus.Skipped;
573
- updateBy.data.subscriberNotificationStatusMessage =
574
- "Notifications skipped as subscribers are not to be notified for this incident.";
575
- } else if (
576
- updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
577
- true
578
- ) {
579
- updateBy.data.subscriberNotificationStatusOnIncidentCreated =
580
- StatusPageSubscriberNotificationStatus.Pending;
581
- }
1793
+ const readableIds: Array<string> =
1794
+ await StatusPageReadAccess.getReadableStatusPageIds({
1795
+ statusPageIds: heldIds,
1796
+ props: data.props,
1797
+ });
1798
+
1799
+ const hiddenByIncident: Array<Array<string>> = heldByIncident.map(
1800
+ (held: Array<string>) => {
1801
+ return held
1802
+ .filter((id: string) => {
1803
+ return !readableIds.includes(id);
1804
+ })
1805
+ .sort();
1806
+ },
1807
+ );
1808
+
1809
+ const hiddenIds: Array<string> = hiddenByIncident[0] || [];
1810
+
1811
+ const everyIncidentHidesTheSame: boolean = hiddenByIncident.every(
1812
+ (hidden: Array<string>) => {
1813
+ return hidden.join(",") === hiddenIds.join(",");
1814
+ },
1815
+ );
1816
+
1817
+ if (!everyIncidentHidesTheSame) {
1818
+ throw new BadDataException(
1819
+ "These incidents are limited to different status pages that you do not have access to, so their status pages cannot be changed together. Change them one at a time.",
1820
+ );
582
1821
  }
583
1822
 
584
- /*
585
- * Re-apply mapped custom field values. Covers the Custom Fields modal
586
- * saving the whole bag back over a mapped value, and the incident's
587
- * monitors being added or removed — both change what a mapped field should
588
- * hold, and folding the answer into this payload keeps it one write with a
589
- * truthful audit entry.
590
- */
591
- await CustomFieldMappingService.applyMappingsToUpdate({
592
- definitionModelType: IncidentCustomField,
593
- updateBy: updateBy,
1823
+ return hiddenIds;
1824
+ }
1825
+
1826
+ private toStatusPageStubs(statusPageIds: Array<string>): Array<StatusPage> {
1827
+ return statusPageIds.map((statusPageId: string) => {
1828
+ const statusPage: StatusPage = new StatusPage();
1829
+ statusPage._id = statusPageId;
1830
+ return statusPage;
594
1831
  });
1832
+ }
595
1833
 
596
- return {
597
- updateBy: updateBy,
598
- carryForward: carryForward,
599
- };
1834
+ // A column's value once an update is written: the update's, else the stored.
1835
+ private getValueAfterUpdate<T>(
1836
+ updatedValue: unknown,
1837
+ storedValue: T | undefined,
1838
+ ): T | undefined {
1839
+ if (updatedValue !== undefined && updatedValue !== null) {
1840
+ return updatedValue as T;
1841
+ }
1842
+
1843
+ return storedValue;
600
1844
  }
601
1845
 
602
1846
  /*
@@ -749,6 +1993,16 @@ export class Service extends DatabaseService<Model> {
749
1993
  modelName: "On-Call Policy",
750
1994
  service: OnCallDutyPolicyService,
751
1995
  },
1996
+ /*
1997
+ * The status pages the incident is limited to. Another project's page
1998
+ * here would put that project's page name into this incident's feed and
1999
+ * scope it to a page it can never show on.
2000
+ */
2001
+ {
2002
+ column: "statusPages",
2003
+ modelName: "Status Page",
2004
+ service: StatusPageService,
2005
+ },
752
2006
  ...getAffectedResourceRelations(this.getModel()),
753
2007
  ];
754
2008
  }
@@ -796,6 +2050,23 @@ export class Service extends DatabaseService<Model> {
796
2050
  const projectId: ObjectID =
797
2051
  createBy.props.tenantId || createBy.data.projectId!;
798
2052
 
2053
+ /*
2054
+ * The values the caller sent, before a template or a mapping adds any:
2055
+ * those are not the caller's to get wrong.
2056
+ */
2057
+ await this.validateCustomFieldValuesOnCreate({
2058
+ createBy: createBy,
2059
+ projectId: projectId,
2060
+ });
2061
+
2062
+ /*
2063
+ * Status pages the caller picked, as opposed to ones copied from a
2064
+ * template below (see applyStatusPageScopeToCreate).
2065
+ */
2066
+ const statusPagesFromCaller: boolean =
2067
+ createBy.data.statusPages !== undefined &&
2068
+ createBy.data.statusPages !== null;
2069
+
799
2070
  /*
800
2071
  * Declaring the incident from alerts. The alert ids are checked here,
801
2072
  * before the incident number is taken and before anything is written, so
@@ -809,14 +2080,36 @@ export class Service extends DatabaseService<Model> {
809
2080
  const alertIdsToLink: unknown =
810
2081
  createBy.miscDataProps?.[INCIDENT_ALERT_IDS_TO_LINK_KEY];
811
2082
 
812
- if (alertIdsToLink !== undefined && alertIdsToLink !== null) {
813
- carryForward = {
814
- alertIdsToLink:
815
- await IncidentAlertService.validateAlertIdsForNewIncident({
2083
+ const validatedAlertIds: Array<ObjectID> =
2084
+ alertIdsToLink !== undefined && alertIdsToLink !== null
2085
+ ? await IncidentAlertService.validateAlertIdsForNewIncident({
816
2086
  projectId: projectId,
817
2087
  alertIds: alertIdsToLink,
818
2088
  props: createBy.props,
819
- }),
2089
+ })
2090
+ : [];
2091
+
2092
+ /*
2093
+ * Asking to acknowledge the alerts is checked here too, with the alert
2094
+ * ids, so a request that cannot be honoured (no Acknowledged alert state,
2095
+ * or a caller who may not change these alerts' states) is refused before
2096
+ * the incident exists rather than leaving alerts that keep paging.
2097
+ */
2098
+ const alertsToAcknowledge: AlertsToAcknowledgeOnDeclare | null =
2099
+ await IncidentAlertService.validateAcknowledgeAlertsForNewIncident({
2100
+ projectId: projectId,
2101
+ acknowledgeAlerts:
2102
+ createBy.miscDataProps?.[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY],
2103
+ alertIds: validatedAlertIds,
2104
+ props: createBy.props,
2105
+ });
2106
+
2107
+ if (validatedAlertIds.length > 0) {
2108
+ carryForward = {
2109
+ alertIdsToLink: validatedAlertIds,
2110
+ acknowledgedAlertStateId:
2111
+ alertsToAcknowledge?.acknowledgedAlertStateId || null,
2112
+ alertIdsToAcknowledge: alertsToAcknowledge?.alertIdsToAcknowledge || [],
820
2113
  };
821
2114
  }
822
2115
 
@@ -849,6 +2142,9 @@ export class Service extends DatabaseService<Model> {
849
2142
  // Determine the initial incident state
850
2143
  let initialIncidentStateId: ObjectID | undefined = undefined;
851
2144
 
2145
+ // Declared from a template whose status pages were all deleted.
2146
+ let isScopedToNothingByTemplate: boolean = false;
2147
+
852
2148
  // If currentIncidentStateId is already provided (manual selection), use it
853
2149
  if (createBy.data.currentIncidentStateId) {
854
2150
  initialIncidentStateId = createBy.data.currentIncidentStateId;
@@ -904,6 +2200,9 @@ export class Service extends DatabaseService<Model> {
904
2200
  services: { _id: true },
905
2201
  onCallDutyPolicies: { _id: true },
906
2202
  labels: { _id: true },
2203
+ statusPages: { _id: true },
2204
+ isScopedToStatusPages: true,
2205
+ customFields: true,
907
2206
  },
908
2207
  props: {
909
2208
  isRoot: true,
@@ -1055,9 +2354,50 @@ export class Service extends DatabaseService<Model> {
1055
2354
  createBy.data.labels = stubs;
1056
2355
  }
1057
2356
  }
2357
+ // Applying a template that has status pages scopes the incident.
2358
+ if (createBy.data.statusPages === undefined) {
2359
+ const stubs: Array<StatusPage> | undefined = stubBy(
2360
+ StatusPage,
2361
+ incidentTemplate.statusPages,
2362
+ );
2363
+ if (stubs && stubs.length > 0) {
2364
+ createBy.data.statusPages = stubs;
2365
+ } else if (incidentTemplate.isScopedToStatusPages) {
2366
+ /*
2367
+ * A template limited to status pages that have all been deleted
2368
+ * since. Its incidents stay limited - to nothing - like an
2369
+ * incident whose pages were deleted, rather than reaching every
2370
+ * page that lists their monitors.
2371
+ */
2372
+ isScopedToNothingByTemplate = true;
2373
+ }
2374
+ }
2375
+
2376
+ /*
2377
+ * The template's custom field values fill in the fields the caller
2378
+ * left out; a value the caller sent always wins. Merged, not
2379
+ * replaced, so the caller's own values are never dropped. They are
2380
+ * the template's, so the value check above (the caller's values
2381
+ * only) does not see them, and the mapping below still has the last
2382
+ * word on a mapped field.
2383
+ */
2384
+ const customFieldsWithTemplate: JSONObject | undefined =
2385
+ mergeTemplateCustomFields({
2386
+ templateCustomFields: incidentTemplate.customFields,
2387
+ customFields: createBy.data.customFields,
2388
+ });
2389
+
2390
+ if (customFieldsWithTemplate) {
2391
+ createBy.data.customFields = customFieldsWithTemplate;
2392
+ }
1058
2393
  }
1059
2394
  }
1060
2395
 
2396
+ this.applyStatusPageScopeToCreate({
2397
+ createBy: createBy,
2398
+ isScopedToNothingByTemplate: isScopedToNothingByTemplate,
2399
+ });
2400
+
1061
2401
  // If no custom state is provided or found, fall back to default created state
1062
2402
  if (!initialIncidentStateId) {
1063
2403
  const incidentState: IncidentState | null =
@@ -1145,6 +2485,13 @@ export class Service extends DatabaseService<Model> {
1145
2485
  },
1146
2486
  );
1147
2487
 
2488
+ // Before the counter increment too, like the checks above.
2489
+ await this.assertCallerCanPickStatusPagesOnCreate({
2490
+ createBy: createBy,
2491
+ projectId: projectId,
2492
+ statusPagesFromCaller: statusPagesFromCaller,
2493
+ });
2494
+
1148
2495
  const incidentCounterResult: {
1149
2496
  counter: number;
1150
2497
  prefix: string | undefined;
@@ -1272,20 +2619,12 @@ export class Service extends DatabaseService<Model> {
1272
2619
  labels: {
1273
2620
  name: true,
1274
2621
  },
1275
- monitors: {
1276
- name: true,
1277
- _id: true,
1278
- },
1279
2622
  /*
1280
- * Named under "Resources Affected" in the created feed item. This read
1281
- * runs as root, so projectId comes along and the feed names only this
1282
- * project's SLOs (getSloAffectedResourceMarkdownLines).
2623
+ * The resources named under "Resources Affected" are read by the feed
2624
+ * builder itself (LinkedAffectedResources), one relation at a time.
1283
2625
  */
1284
- serviceLevelObjectives: {
1285
- name: true,
1286
- _id: true,
1287
- projectId: true,
1288
- },
2626
+ // The created feed item names the status pages a scoped incident is limited to.
2627
+ isScopedToStatusPages: true,
1289
2628
  },
1290
2629
  props: {
1291
2630
  isRoot: true,
@@ -1744,6 +3083,24 @@ export class Service extends DatabaseService<Model> {
1744
3083
  return carryForward?.alertIdsToLink || [];
1745
3084
  }
1746
3085
 
3086
+ /*
3087
+ * The alerts the declaration asked to acknowledge that were not
3088
+ * acknowledged yet when it was checked - the only ones the caller was
3089
+ * authorized for, so the only ones that may be written.
3090
+ */
3091
+ private getAlertIdsToAcknowledgeDeclaredWith(
3092
+ onCreate: OnCreate<Model>,
3093
+ ): Array<ObjectID> {
3094
+ const carryForward: IncidentCreateCarryForward =
3095
+ (onCreate.carryForward as IncidentCreateCarryForward) || null;
3096
+
3097
+ if (!carryForward?.acknowledgedAlertStateId) {
3098
+ return [];
3099
+ }
3100
+
3101
+ return carryForward.alertIdsToAcknowledge || [];
3102
+ }
3103
+
1747
3104
  /*
1748
3105
  * Who declared the incident, as "Linked by" on the links and the actor of
1749
3106
  * their feed entries. A user is recorded as themselves and an API key as
@@ -1778,6 +3135,13 @@ export class Service extends DatabaseService<Model> {
1778
3135
  * the incident's title when the incident is private, and a rule can make
1779
3136
  * it private after it was saved. (The alerts' owners are added to a private
1780
3137
  * incident later in the chain, once its workspace channels exist.)
3138
+ *
3139
+ * When the declaration asked for it, the alerts are then acknowledged as
3140
+ * the declaring user, which stops their on-call escalation at the
3141
+ * workers' next run. That is not waited for: each acknowledgement is a
3142
+ * full state change (feed entry, Slack / Microsoft Teams post, owner
3143
+ * notification), up to one per alert, and the page the user lands on does
3144
+ * not show the alerts' states. It never fails the incident.
1781
3145
  */
1782
3146
  @CaptureSpan()
1783
3147
  private async linkAlertsDeclaredWithIncident(
@@ -1793,6 +3157,8 @@ export class Service extends DatabaseService<Model> {
1793
3157
 
1794
3158
  await privacyRulesApplied;
1795
3159
 
3160
+ let linkedAlertIds: Array<ObjectID> = [];
3161
+
1796
3162
  try {
1797
3163
  const result: LinkAlertsToIncidentResult =
1798
3164
  await IncidentAlertService.linkAlertsToIncident({
@@ -1806,6 +3172,11 @@ export class Service extends DatabaseService<Model> {
1806
3172
  },
1807
3173
  });
1808
3174
 
3175
+ linkedAlertIds = [
3176
+ ...result.linkedAlertIds,
3177
+ ...result.alreadyLinkedAlertIds,
3178
+ ];
3179
+
1809
3180
  if (result.failed.length > 0) {
1810
3181
  logger.error(
1811
3182
  `${result.failed.length} of ${alertIds.length} alerts could not be linked to the incident they were declared with.`,
@@ -1824,6 +3195,44 @@ export class Service extends DatabaseService<Model> {
1824
3195
  } as LogAttributes,
1825
3196
  );
1826
3197
  }
3198
+
3199
+ const alertIdsToAcknowledge: Array<ObjectID> =
3200
+ this.getAlertIdsToAcknowledgeDeclaredWith(onCreate);
3201
+
3202
+ if (alertIdsToAcknowledge.length === 0) {
3203
+ return;
3204
+ }
3205
+
3206
+ const projectId: ObjectID = createdItem.projectId;
3207
+ const incidentId: ObjectID = createdItem.id;
3208
+
3209
+ IncidentAlertService.acknowledgeAlertsDeclaredWithIncident({
3210
+ projectId: projectId,
3211
+ incidentId: incidentId,
3212
+ alertIds: alertIdsToAcknowledge,
3213
+ linkedAlertIds: linkedAlertIds,
3214
+ acknowledgedByUserId: this.getDeclaringUserId(onCreate, createdItem),
3215
+ })
3216
+ .then((acknowledged: AcknowledgeDeclaredAlertsResult) => {
3217
+ if (acknowledged.failed.length > 0) {
3218
+ logger.error(
3219
+ `${acknowledged.failed.length} of ${alertIdsToAcknowledge.length} alerts could not be acknowledged when the incident was declared from them.`,
3220
+ {
3221
+ projectId: projectId.toString(),
3222
+ incidentId: incidentId.toString(),
3223
+ } as LogAttributes,
3224
+ );
3225
+ }
3226
+ })
3227
+ .catch((error: Error) => {
3228
+ logger.error(
3229
+ `Acknowledging the alerts an incident was declared from failed in IncidentService.onCreateSuccess: ${error}`,
3230
+ {
3231
+ projectId: projectId.toString(),
3232
+ incidentId: incidentId.toString(),
3233
+ } as LogAttributes,
3234
+ );
3235
+ });
1827
3236
  }
1828
3237
 
1829
3238
  @CaptureSpan()
@@ -1878,6 +3287,44 @@ export class Service extends DatabaseService<Model> {
1878
3287
  }
1879
3288
  }
1880
3289
 
3290
+ /*
3291
+ * The status pages a scoped incident is limited to, as a Markdown list for
3292
+ * its created feed item. Read on their own rather than in onCreateSuccess's
3293
+ * select: a find that selects several many-to-many lists returns a row for
3294
+ * every combination of them.
3295
+ */
3296
+ private async getScopedStatusPagesMarkdown(incident: Model): Promise<string> {
3297
+ const incidentWithStatusPages: Model | null = await this.findOneById({
3298
+ id: incident.id!,
3299
+ select: {
3300
+ statusPages: {
3301
+ _id: true,
3302
+ name: true,
3303
+ },
3304
+ },
3305
+ props: {
3306
+ isRoot: true,
3307
+ },
3308
+ });
3309
+
3310
+ const statusPages: Array<StatusPage> =
3311
+ incidentWithStatusPages?.statusPages || [];
3312
+
3313
+ if (statusPages.length === 0) {
3314
+ // Scoped to pages that have all been deleted since: hidden everywhere.
3315
+ return `- None: the status pages it was limited to have been deleted, so it is not shown on any status page.\n`;
3316
+ }
3317
+
3318
+ let markdown: string = "";
3319
+
3320
+ for (const statusPage of statusPages) {
3321
+ // A page name is free text: kept from turning into a link or an image.
3322
+ markdown += `- [${escapeMarkdownInline(statusPage.name || "Untitled status page")}](${(await StatusPageService.getStatusPageLinkInDashboard(incident.projectId!, statusPage.id!)).toString()})\n`;
3323
+ }
3324
+
3325
+ return markdown;
3326
+ }
3327
+
1881
3328
  @CaptureSpan()
1882
3329
  private async createIncidentFeedAsync(incident: Model): Promise<void> {
1883
3330
  try {
@@ -1905,37 +3352,36 @@ ${incident.description || "No description provided."}
1905
3352
  }
1906
3353
 
1907
3354
  /*
1908
- * Monitors, then the SLOs this incident is linked to. A burn-rate
1909
- * incident carries no monitors on purpose, so its SLO is the only
1910
- * resource there is to name - and the feed's only way back to the
1911
- * objective that declared it. The SLO link is built inline:
1912
- * ServiceLevelObjectiveService cannot be imported here (it reaches
1913
- * this service through the burn-rate rule service).
3355
+ * Everything the incident's Affected Resources card lists: monitors,
3356
+ * the hosts, clusters and services it is attached to, then its SLOs. A
3357
+ * burn-rate incident carries no monitors on purpose, so its SLO is the
3358
+ * only resource there is to name - and the feed's only way back to the
3359
+ * objective that declared it.
1914
3360
  */
1915
- const sloLines: Array<string> =
1916
- incident.serviceLevelObjectives &&
1917
- incident.serviceLevelObjectives.length > 0
1918
- ? getSloAffectedResourceMarkdownLines({
1919
- dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1920
- projectId: incident.projectId!,
1921
- serviceLevelObjectives: incident.serviceLevelObjectives,
1922
- })
1923
- : [];
3361
+ const resources: Array<LinkedAffectedResource> =
3362
+ await LinkedAffectedResources.readForIncident({
3363
+ service: this,
3364
+ projectId: incident.projectId!,
3365
+ incidentId: incident.id!,
3366
+ });
1924
3367
 
1925
- if (
1926
- (incident.monitors && incident.monitors.length > 0) ||
1927
- sloLines.length > 0
1928
- ) {
3368
+ if (resources.length > 0) {
1929
3369
  feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
1930
3370
 
1931
- for (const monitor of incident.monitors || []) {
1932
- feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(incident.projectId!, monitor.id!)).toString()})\n`;
3371
+ for (const resourceLine of LinkedAffectedResources.getMarkdownLines({
3372
+ dashboardUrl: await DatabaseConfig.getDashboardUrl(),
3373
+ projectId: incident.projectId!,
3374
+ resources: resources,
3375
+ })) {
3376
+ feedInfoInMarkdown += `${resourceLine}\n`;
1933
3377
  }
1934
3378
 
1935
- for (const sloLine of sloLines) {
1936
- feedInfoInMarkdown += `${sloLine}\n`;
1937
- }
3379
+ feedInfoInMarkdown += `\n\n`;
3380
+ }
1938
3381
 
3382
+ if (incident.isScopedToStatusPages) {
3383
+ feedInfoInMarkdown += `📣 **Limited to Status Pages**:\n`;
3384
+ feedInfoInMarkdown += await this.getScopedStatusPagesMarkdown(incident);
1939
3385
  feedInfoInMarkdown += `\n\n`;
1940
3386
  }
1941
3387
 
@@ -2879,14 +4325,8 @@ ${incidentSeverity.name}
2879
4325
  onUpdate.carryForward;
2880
4326
 
2881
4327
  if (carryForward) {
2882
- const incidentCarryForward:
2883
- | {
2884
- monitorsRemoved: Array<Monitor>;
2885
- monitorsAdded: Array<Monitor>;
2886
- oldChangeMonitorStatusIdTo: ObjectID | undefined;
2887
- newMonitorChangeStatusIdTo: ObjectID | undefined;
2888
- }
2889
- | undefined = carryForward[incidentId.toString()];
4328
+ const incidentCarryForward: UpdateCarryForward[string] | undefined =
4329
+ carryForward[incidentId.toString()];
2890
4330
 
2891
4331
  if (incidentCarryForward) {
2892
4332
  if (incidentCarryForward.monitorsRemoved.length > 0) {
@@ -2960,6 +4400,23 @@ ${incidentSeverity.name}
2960
4400
  shouldAddIncidentFeed = true;
2961
4401
  }
2962
4402
 
4403
+ const statusPageScopeMarkdown: string =
4404
+ await this.getStatusPageScopeFeedMarkdown({
4405
+ projectId: projectId,
4406
+ incidentId: incidentId,
4407
+ change: incidentCarryForward.statusPageScopeChange,
4408
+ });
4409
+
4410
+ if (statusPageScopeMarkdown) {
4411
+ feedInfoInMarkdown += statusPageScopeMarkdown;
4412
+ shouldAddIncidentFeed = true;
4413
+ }
4414
+
4415
+ await this.requeueCreatedNotificationIfAddedPagesWereMissed({
4416
+ incidentId: incidentId,
4417
+ change: incidentCarryForward.statusPageScopeChange,
4418
+ });
4419
+
2963
4420
  if (
2964
4421
  incidentCarryForward.oldChangeMonitorStatusIdTo &&
2965
4422
  incidentCarryForward.newMonitorChangeStatusIdTo
@@ -3057,6 +4514,109 @@ ${incidentSeverity.name}
3057
4514
  return onUpdate;
3058
4515
  }
3059
4516
 
4517
+ /*
4518
+ * The part of an update's feed item that records a change of the status
4519
+ * pages the incident is limited to: the pages added and removed, whether
4520
+ * the incident is now limited at all, and whether the added pages will be
4521
+ * sent the 'created' notification. Empty when the scope did not change.
4522
+ *
4523
+ * Page names are read as root, like the monitor names above. The pages were
4524
+ * validated as this project's when they were written, and the read is
4525
+ * filtered on the project anyway. Never throws: the update is already
4526
+ * written, and a missing line in the feed must not turn it into an error.
4527
+ */
4528
+ private async getStatusPageScopeFeedMarkdown(data: {
4529
+ projectId: ObjectID;
4530
+ incidentId: ObjectID;
4531
+ change: StatusPageScopeCarryForward | undefined;
4532
+ }): Promise<string> {
4533
+ const change: StatusPageScopeCarryForward | undefined = data.change;
4534
+
4535
+ if (
4536
+ !change ||
4537
+ (change.addedStatusPageIds.length === 0 &&
4538
+ change.removedStatusPageIds.length === 0)
4539
+ ) {
4540
+ return "";
4541
+ }
4542
+
4543
+ try {
4544
+ const statusPages: Array<StatusPage> = await StatusPageService.findBy({
4545
+ query: {
4546
+ _id: QueryHelper.any([
4547
+ ...change.addedStatusPageIds,
4548
+ ...change.removedStatusPageIds,
4549
+ ]),
4550
+ projectId: data.projectId,
4551
+ },
4552
+ select: {
4553
+ _id: true,
4554
+ name: true,
4555
+ },
4556
+ limit: LIMIT_MAX,
4557
+ skip: 0,
4558
+ props: {
4559
+ isRoot: true,
4560
+ },
4561
+ });
4562
+
4563
+ const statusPageLines: (ids: Array<string>) => Promise<string> = async (
4564
+ ids: Array<string>,
4565
+ ): Promise<string> => {
4566
+ let lines: string = "";
4567
+
4568
+ for (const id of ids) {
4569
+ const statusPage: StatusPage | undefined = statusPages.find(
4570
+ (page: StatusPage) => {
4571
+ return page.id?.toString().toLowerCase() === id;
4572
+ },
4573
+ );
4574
+
4575
+ if (!statusPage) {
4576
+ // Deleted since: its name is gone with it.
4577
+ lines += `- A deleted status page\n`;
4578
+ continue;
4579
+ }
4580
+
4581
+ // A page name is free text: kept from turning into a link or an image.
4582
+ lines += `- [${escapeMarkdownInline(statusPage.name || "Untitled status page")}](${(await StatusPageService.getStatusPageLinkInDashboard(data.projectId, statusPage.id!)).toString()})\n`;
4583
+ }
4584
+
4585
+ return lines;
4586
+ };
4587
+
4588
+ let markdown: string = "";
4589
+
4590
+ if (change.addedStatusPageIds.length > 0) {
4591
+ markdown += `\n\n**📣 Status Pages Added**:\n${await statusPageLines(change.addedStatusPageIds)}`;
4592
+ }
4593
+
4594
+ if (change.removedStatusPageIds.length > 0) {
4595
+ markdown += `\n\n**🔕 Status Pages Removed**:\n${await statusPageLines(change.removedStatusPageIds)}`;
4596
+ }
4597
+
4598
+ markdown += change.isScoped
4599
+ ? `\n\nThis incident is shown on, and notifies the subscribers of, only its selected status pages that list its monitors.\n`
4600
+ : `\n\nThis incident is no longer limited to specific status pages: it is shown on every status page that lists its monitors, except pages that only show incidents limited to them.\n`;
4601
+
4602
+ if (change.notificationQueued) {
4603
+ markdown += `\nSubscribers of the added status pages will be sent the notification that this incident was created.\n`;
4604
+ }
4605
+
4606
+ return markdown;
4607
+ } catch (err) {
4608
+ logger.error(
4609
+ `Failed to describe the status page scope change in the incident feed: ${err}`,
4610
+ {
4611
+ projectId: data.projectId?.toString(),
4612
+ incidentId: data.incidentId?.toString(),
4613
+ } as LogAttributes,
4614
+ );
4615
+
4616
+ return "";
4617
+ }
4618
+ }
4619
+
3060
4620
  @CaptureSpan()
3061
4621
  public async doesMonitorHasMoreActiveManualIncidents(
3062
4622
  monitorId: ObjectID,