@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
@@ -7,10 +7,13 @@ import {
7
7
  import ProjectSMTPConfigService from "../Services/ProjectSmtpConfigService";
8
8
  import CreateBy from "../Types/Database/CreateBy";
9
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
- import { OnCreate } from "../Types/Database/Hooks";
10
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
11
11
  import QueryHelper from "../Types/Database/QueryHelper";
12
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
13
+ import UpdateBy from "../Types/Database/UpdateBy";
12
14
  import logger, { LogAttributes } from "../Utils/Logger";
13
15
  import DatabaseService from "./DatabaseService";
16
+ import GlobalCache from "../Infrastructure/GlobalCache";
14
17
  import MailService from "./MailService";
15
18
  import ProjectCallSMSConfigService from "./ProjectCallSMSConfigService";
16
19
  import ProjectService, { CurrentPlan } from "./ProjectService";
@@ -33,6 +36,9 @@ import PositiveNumber from "../../Types/PositiveNumber";
33
36
  import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
34
37
  import StatusPageSubscriberNotificationEventType from "../../Types/StatusPage/StatusPageSubscriberNotificationEventType";
35
38
  import StatusPageSubscriberNotificationMethod from "../../Types/StatusPage/StatusPageSubscriberNotificationMethod";
39
+ import { IncidentSubscriberAudienceCounts } from "../../Types/StatusPage/IncidentSubscriberAudience";
40
+ import Dictionary from "../../Types/Dictionary";
41
+ import { JSONObject } from "../../Types/JSON";
36
42
  import NumberUtil from "../../Utils/Number";
37
43
  import SlackUtil from "../Utils/Workspace/Slack/Slack";
38
44
  import MicrosoftTeamsUtil from "../Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
@@ -41,12 +47,174 @@ import SSRFProtection from "../Utils/SSRFProtection";
41
47
  import StatusPageSubscriberNotificationTemplateService, {
42
48
  Service as StatusPageSubscriberNotificationTemplateServiceClass,
43
49
  } from "./StatusPageSubscriberNotificationTemplateService";
50
+ import TeamMemberService from "./TeamMemberService";
51
+ import UserService from "./UserService";
52
+ import User from "../../Models/DatabaseModels/User";
53
+ import OneUptimeDate from "../../Types/Date";
54
+ import ModelEventType from "../../Types/Realtime/ModelEventType";
55
+ import StatusPageSubscriberUnsubscribe, {
56
+ StatusPageSubscriberContact,
57
+ StatusPageSubscriberUnsubscribeDetails,
58
+ StatusPageSubscriberUnsubscribeState,
59
+ } from "../../Types/StatusPage/StatusPageSubscriberUnsubscribe";
60
+ import StatusPageSubscriberUnsubscribeToken from "../Utils/StatusPage/StatusPageSubscriberUnsubscribeToken";
61
+ import StatusPageSubscriberUnsubscribeNotice, {
62
+ StatusPageSubscriberUnsubscribeNoticeEmail,
63
+ StatusPageSubscriberUnsubscribeSource,
64
+ } from "../Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice";
65
+
66
+ /*
67
+ * For an UPDATE ... RETURNING, the postgres driver hands TypeORM's
68
+ * manager.query back `[rows, rowCount]` rather than the bare row array (see
69
+ * UserTwoFactorBackupCodeService.consumeCode). Read defensively: anything
70
+ * unrecognisable is "no row changed", never a silent success.
71
+ */
72
+ type ReturnedRowsFunction = (result: unknown) => Array<JSONObject>;
73
+
74
+ const returnedRows: ReturnedRowsFunction = (
75
+ result: unknown,
76
+ ): Array<JSONObject> => {
77
+ if (!Array.isArray(result)) {
78
+ return [];
79
+ }
80
+
81
+ if (
82
+ result.length === 2 &&
83
+ Array.isArray(result[0]) &&
84
+ typeof result[1] === "number"
85
+ ) {
86
+ return result[0] as Array<JSONObject>;
87
+ }
88
+
89
+ return result.filter((row: unknown): boolean => {
90
+ return Boolean(row) && typeof row === "object" && !Array.isArray(row);
91
+ }) as Array<JSONObject>;
92
+ };
93
+
94
+ /*
95
+ * How many rows an UPDATE without RETURNING changed: the postgres driver
96
+ * answers it with `[rows, rowCount]`. Anything else reads as none.
97
+ */
98
+ type AffectedRowCountFunction = (result: unknown) => number;
99
+
100
+ const affectedRowCount: AffectedRowCountFunction = (
101
+ result: unknown,
102
+ ): number => {
103
+ if (
104
+ Array.isArray(result) &&
105
+ result.length === 2 &&
106
+ typeof result[1] === "number"
107
+ ) {
108
+ return result[1];
109
+ }
110
+
111
+ return 0;
112
+ };
113
+
114
+ interface UnsubscribedAtCarryForward {
115
+ // The value this update writes to isUnsubscribed, when it writes one.
116
+ isUnsubscribed: boolean | null;
117
+ // The matched subscribers that are subscribed now, which this update cancels.
118
+ subscriberIdsBeingUnsubscribed: Array<string>;
119
+ }
120
+
121
+ /*
122
+ * The subscriber rows being created for a visitor who signed up on the status
123
+ * page itself (see createFromStatusPageSignUp). Every other create - a
124
+ * teammate on the dashboard, an API key, a workflow - is the team adding a
125
+ * subscriber, and onBeforeCreate marks it so (Is Added By Team).
126
+ *
127
+ * The very model instances, held only for the length of the create: nothing
128
+ * a request body carries can put a row in here, so no client can pass its
129
+ * subscriber off as a sign-up and keep the page's owners from hearing when
130
+ * it unsubscribes.
131
+ */
132
+ const statusPageSignUps: WeakSet<Model> = new WeakSet<Model>();
133
+
134
+ // How many subscribers the backfill and the token top-up handle per statement.
135
+ const UNSUBSCRIBE_COLUMNS_BATCH_SIZE: number = 1000;
136
+
137
+ export interface StatusPageSubscriberUnsubscribeBackfillResult {
138
+ // Subscribers given an unsubscribe token.
139
+ tokensGiven: number;
140
+ // Subscribers marked Is Added By Team because they have a creator.
141
+ markedAddedByTeam: number;
142
+ }
44
143
 
45
144
  export class Service extends DatabaseService<Model> {
46
145
  public constructor() {
47
146
  super(Model);
48
147
  }
49
148
 
149
+ /*
150
+ * A create hands its caller the row it saved, and every caller passes it
151
+ * on whole: BaseAPI serializes it into the response of POST
152
+ * /status-page-subscriber without a column read check (a teammate's Add
153
+ * Subscriber and Add in Bulk, any API key), and a workflow's Create Status
154
+ * Page Subscriber step returns it, to be kept in the workflow's logs.
155
+ *
156
+ * So the two secrets onBeforeCreate minted are taken off it first: the
157
+ * unsubscribe token, which lets whoever holds it cancel the subscription
158
+ * without signing in - and makes the team's notice say the subscriber did -
159
+ * and the six-digit confirmation code. Nobody can read either column
160
+ * (read: []). They leave the server only inside the messages to the
161
+ * subscription's own contact, which onCreateSuccess has sent by now; the
162
+ * confirmation email reads its code back from the database.
163
+ */
164
+ @CaptureSpan()
165
+ public override async create(createBy: CreateBy<Model>): Promise<Model> {
166
+ const created: Model = await super.create(createBy);
167
+
168
+ /*
169
+ * Cleared rather than deleted, as BaseAPI.createItem clears a model's
170
+ * `_id`: an unset column holds undefined, which serializes as nothing.
171
+ */
172
+ const row: Record<string, unknown> = created as unknown as Record<
173
+ string,
174
+ unknown
175
+ >;
176
+ row["unsubscribeToken"] = undefined;
177
+ row["subscriptionConfirmationToken"] = undefined;
178
+
179
+ return created;
180
+ }
181
+
182
+ /*
183
+ * Create the subscriber a visitor signed up for on the status page itself
184
+ * (StatusPageAPI's subscribe endpoint). The one create that is not the team
185
+ * adding a subscriber: it is created as root, like the rest of that
186
+ * endpoint, and Is Added By Team is left off.
187
+ */
188
+ @CaptureSpan()
189
+ public async createFromStatusPageSignUp(data: Model): Promise<Model> {
190
+ statusPageSignUps.add(data);
191
+
192
+ try {
193
+ return await this.create({
194
+ data: data,
195
+ props: {
196
+ isRoot: true,
197
+ },
198
+ });
199
+ } finally {
200
+ statusPageSignUps.delete(data);
201
+ }
202
+ }
203
+
204
+ /*
205
+ * Whether the team added this subscriber, rather than it signing up on the
206
+ * status page. Is Added By Team says so for every subscriber created since
207
+ * it existed; Created By is read as well for the ones created before, until
208
+ * the backfill has marked them (every create with a creator is the team's).
209
+ */
210
+ public isAddedByTeam(
211
+ subscriber: Pick<Model, "isAddedByTeam" | "createdByUserId">,
212
+ ): boolean {
213
+ return (
214
+ subscriber.isAddedByTeam === true || Boolean(subscriber.createdByUserId)
215
+ );
216
+ }
217
+
50
218
  @CaptureSpan()
51
219
  protected override async onBeforeCreate(
52
220
  data: CreateBy<Model>,
@@ -359,6 +527,36 @@ export class Service extends DatabaseService<Model> {
359
527
  100000,
360
528
  999999,
361
529
  ).toString();
530
+
531
+ /*
532
+ * The secret in this subscriber's unsubscribe link. Always minted here,
533
+ * whatever the request carried: the column is computed, so the create
534
+ * column check lets a client send one, and a token the client chose would
535
+ * be one somebody else could know. A re-subscribe deletes the old row
536
+ * above and gets a new token, so links to the old subscription stop
537
+ * working.
538
+ */
539
+ data.data.unsubscribeToken =
540
+ StatusPageSubscriberUnsubscribeToken.generate();
541
+
542
+ /*
543
+ * Never taken from a client either: whether the team added this
544
+ * subscriber. Only a sign-up on the status page is not the team's, and
545
+ * only createFromStatusPageSignUp creates one. A teammate's create carries
546
+ * a creator (Created By) but an API key's or a workflow's does not, so
547
+ * this is the column that says it for all of them.
548
+ */
549
+ data.data.isAddedByTeam = !statusPageSignUps.has(data.data);
550
+
551
+ /*
552
+ * Likewise never taken from a client. A subscriber is only ever created
553
+ * unsubscribed through the API, and then it was cancelled now.
554
+ */
555
+ if (data.data.isUnsubscribed) {
556
+ data.data.unsubscribedAt = OneUptimeDate.getCurrentDate();
557
+ } else {
558
+ delete data.data.unsubscribedAt;
559
+ }
362
560
  logger.debug(
363
561
  `Subscription Confirmation Token: ${data.data.subscriptionConfirmationToken}`,
364
562
  {
@@ -367,11 +565,8 @@ export class Service extends DatabaseService<Model> {
367
565
  } as LogAttributes,
368
566
  );
369
567
 
370
- logger.debug("onBeforeCreate processed data:", {
371
- projectId: data.data.projectId?.toString(),
372
- statusPageId: data.data.statusPageId?.toString(),
373
- } as LogAttributes);
374
- logger.debug(data, {
568
+ // Not the data itself: it now carries the unsubscribe token.
569
+ logger.debug("onBeforeCreate processed data.", {
375
570
  projectId: data.data.projectId?.toString(),
376
571
  statusPageId: data.data.statusPageId?.toString(),
377
572
  } as LogAttributes);
@@ -379,17 +574,543 @@ export class Service extends DatabaseService<Model> {
379
574
  return { createBy: data, carryForward: statuspage };
380
575
  }
381
576
 
577
+ /*
578
+ * Unsubscribed At follows Is Unsubscribed on every write that sets it - a
579
+ * teammate's toggle on the dashboard, the API, a workflow - not only on the
580
+ * unsubscribe link, which sets both itself (see unsubscribe()).
581
+ *
582
+ * Which rows an update cancels has to be known before it runs: afterwards
583
+ * every matched row reads unsubscribed, and a row that already was would
584
+ * get today's date for a cancellation that happened long ago (or before the
585
+ * column existed). The stamp itself is written after the update, as root,
586
+ * by a plain UPDATE: through updateBy it would be a second update of the
587
+ * row and fire the "on update" workflow trigger a second time.
588
+ */
589
+ @CaptureSpan()
590
+ protected override async onBeforeUpdate(
591
+ updateBy: UpdateBy<Model>,
592
+ ): Promise<OnUpdate<Model>> {
593
+ const isUnsubscribed: unknown = (
594
+ updateBy.data as unknown as JSONObject | undefined
595
+ )?.["isUnsubscribed"];
596
+
597
+ const carryForward: UnsubscribedAtCarryForward = {
598
+ isUnsubscribed:
599
+ typeof isUnsubscribed === "boolean" ? isUnsubscribed : null,
600
+ subscriberIdsBeingUnsubscribed: [],
601
+ };
602
+
603
+ if (carryForward.isUnsubscribed === true) {
604
+ const matched: Array<Model> = await this.findBy({
605
+ query: updateBy.query,
606
+ select: {
607
+ _id: true,
608
+ isUnsubscribed: true,
609
+ },
610
+ skip: 0,
611
+ limit: LIMIT_MAX,
612
+ props: {
613
+ isRoot: true,
614
+ ignoreHooks: true,
615
+ },
616
+ });
617
+
618
+ carryForward.subscriberIdsBeingUnsubscribed = matched
619
+ .filter((subscriber: Model): boolean => {
620
+ return Boolean(subscriber._id) && subscriber.isUnsubscribed !== true;
621
+ })
622
+ .map((subscriber: Model): string => {
623
+ return subscriber._id!.toString();
624
+ });
625
+ }
626
+
627
+ return { updateBy, carryForward };
628
+ }
629
+
630
+ @CaptureSpan()
631
+ protected override async onUpdateSuccess(
632
+ onUpdate: OnUpdate<Model>,
633
+ updatedItemIds: Array<ObjectID>,
634
+ ): Promise<OnUpdate<Model>> {
635
+ const carryForward: UnsubscribedAtCarryForward | undefined =
636
+ onUpdate.carryForward as UnsubscribedAtCarryForward | undefined;
637
+
638
+ if (!carryForward || carryForward.isUnsubscribed === null) {
639
+ return onUpdate;
640
+ }
641
+
642
+ const updatedIds: Array<string> = updatedItemIds.map(
643
+ (id: ObjectID): string => {
644
+ return id.toString();
645
+ },
646
+ );
647
+
648
+ if (carryForward.isUnsubscribed === true) {
649
+ const cancelledIds: Array<string> =
650
+ carryForward.subscriberIdsBeingUnsubscribed.filter(
651
+ (id: string): boolean => {
652
+ return updatedIds.includes(id);
653
+ },
654
+ );
655
+
656
+ if (cancelledIds.length > 0) {
657
+ await this.getRepository().manager.query(
658
+ `UPDATE "StatusPageSubscriber" SET "unsubscribedAt" = $1 WHERE "_id" = ANY($2::uuid[]) AND "isUnsubscribed" = true AND "unsubscribedAt" IS NULL`,
659
+ [OneUptimeDate.getCurrentDate(), cancelledIds],
660
+ );
661
+ }
662
+
663
+ return onUpdate;
664
+ }
665
+
666
+ // Subscribed again: the old date no longer describes this subscription.
667
+ if (updatedIds.length > 0) {
668
+ await this.getRepository().manager.query(
669
+ `UPDATE "StatusPageSubscriber" SET "unsubscribedAt" = NULL WHERE "_id" = ANY($1::uuid[]) AND "isUnsubscribed" = false AND "unsubscribedAt" IS NOT NULL`,
670
+ [updatedIds],
671
+ );
672
+ }
673
+
674
+ return onUpdate;
675
+ }
676
+
677
+ /*
678
+ * Cancel a subscription, once. Returns whether this call cancelled it:
679
+ * false when it was already cancelled (or does not exist), which makes the
680
+ * unsubscribe link idempotent.
681
+ *
682
+ * ONE STATEMENT, ON PURPOSE: the WHERE clause's `"isUnsubscribed" = false`
683
+ * lets Postgres decide which of two simultaneous requests - a double click,
684
+ * a mail client that follows the confirmation twice - cancels the
685
+ * subscription, so its Unsubscribed At is written once and the team is told
686
+ * once. `RETURNING` is what tells this call whether it was the one.
687
+ *
688
+ * Written as raw SQL because no DatabaseService write both takes that
689
+ * predicate and reports what it matched. That skips the update hooks, so
690
+ * the "on update" workflow trigger and the realtime event a DatabaseService
691
+ * update fires are fired here, by hand, with the two columns this wrote.
692
+ */
693
+ @CaptureSpan()
694
+ public async unsubscribe(data: {
695
+ subscriberId: ObjectID;
696
+ source: StatusPageSubscriberUnsubscribeSource;
697
+ }): Promise<boolean> {
698
+ const unsubscribedAt: Date = OneUptimeDate.getCurrentDate();
699
+
700
+ const rows: Array<JSONObject> = returnedRows(
701
+ await this.getRepository().manager.query(
702
+ `UPDATE "StatusPageSubscriber"
703
+ SET "isUnsubscribed" = true,
704
+ "unsubscribedAt" = $1,
705
+ "updatedAt" = CURRENT_TIMESTAMP
706
+ WHERE "_id" = $2
707
+ AND "isUnsubscribed" = false
708
+ AND "deletedAt" IS NULL
709
+ RETURNING "_id", "projectId"`,
710
+ [unsubscribedAt, data.subscriberId.toString()],
711
+ ),
712
+ );
713
+
714
+ const row: JSONObject | undefined = rows[0];
715
+
716
+ if (!row || !row["_id"]) {
717
+ return false;
718
+ }
719
+
720
+ const projectId: ObjectID | null = row["projectId"]
721
+ ? new ObjectID(row["projectId"].toString())
722
+ : null;
723
+
724
+ if (projectId) {
725
+ if (this.getModel().enableWorkflowOn?.update) {
726
+ await this.onTriggerWorkflow(
727
+ data.subscriberId,
728
+ projectId,
729
+ "on-update",
730
+ {
731
+ updatedFields: {
732
+ isUnsubscribed: true,
733
+ unsubscribedAt: unsubscribedAt.toISOString(),
734
+ },
735
+ },
736
+ );
737
+ }
738
+
739
+ await this.onTriggerRealtime(
740
+ data.subscriberId,
741
+ projectId,
742
+ ModelEventType.Update,
743
+ );
744
+ }
745
+
746
+ /*
747
+ * The subscription is cancelled either way; a failure to tell the team
748
+ * must not turn that into an error for the person who asked.
749
+ */
750
+ try {
751
+ await this.notifyTeamOfUnsubscribe({
752
+ subscriberId: data.subscriberId,
753
+ source: data.source,
754
+ unsubscribedAt: unsubscribedAt,
755
+ });
756
+ } catch (err) {
757
+ logger.error(err, {
758
+ statusPageSubscriberId: data.subscriberId.toString(),
759
+ } as LogAttributes);
760
+ }
761
+
762
+ return true;
763
+ }
764
+
765
+ /*
766
+ * What the status page's unsubscribe page shows for a link, before anything
767
+ * is changed. Opening the page calls this; only a POST unsubscribes.
768
+ *
769
+ * Every way a link can be wrong - a malformed id or token, no such
770
+ * subscriber, a deleted one, one on another status page, a token that does
771
+ * not match - reads the same, Invalid, after the same constant-time token
772
+ * comparison, so the answer cannot be used to tell them apart.
773
+ */
774
+ @CaptureSpan()
775
+ public async getUnsubscribeLinkDetails(data: {
776
+ statusPageId: string;
777
+ subscriberId: string;
778
+ token: string;
779
+ }): Promise<StatusPageSubscriberUnsubscribeDetails> {
780
+ const subscriber: Model | null =
781
+ await this.findSubscriberByUnsubscribeLink(data);
782
+
783
+ if (!subscriber) {
784
+ return { state: StatusPageSubscriberUnsubscribeState.Invalid };
785
+ }
786
+
787
+ const contact: StatusPageSubscriberContact | null =
788
+ StatusPageSubscriberUnsubscribe.describeContact(subscriber);
789
+
790
+ return {
791
+ state: subscriber.isUnsubscribed
792
+ ? StatusPageSubscriberUnsubscribeState.Unsubscribed
793
+ : StatusPageSubscriberUnsubscribeState.Subscribed,
794
+ channel: contact?.channel,
795
+ contact: contact?.contact,
796
+ wasAddedByTeam: this.isAddedByTeam(subscriber),
797
+ };
798
+ }
799
+
800
+ /*
801
+ * The unsubscribe page's POST: cancel the subscription the link belongs to.
802
+ * Idempotent - a second confirmation, or one for a subscription already
803
+ * cancelled some other way, answers Unsubscribed again and changes nothing.
804
+ * A bad link answers Invalid, exactly as getUnsubscribeLinkDetails does.
805
+ */
806
+ @CaptureSpan()
807
+ public async unsubscribeWithLink(data: {
808
+ statusPageId: string;
809
+ subscriberId: string;
810
+ token: string;
811
+ }): Promise<StatusPageSubscriberUnsubscribeState> {
812
+ const subscriber: Model | null =
813
+ await this.findSubscriberByUnsubscribeLink(data);
814
+
815
+ if (!subscriber || !subscriber.id) {
816
+ return StatusPageSubscriberUnsubscribeState.Invalid;
817
+ }
818
+
819
+ if (!subscriber.isUnsubscribed) {
820
+ await this.unsubscribe({
821
+ subscriberId: subscriber.id,
822
+ source: StatusPageSubscriberUnsubscribeSource.UnsubscribeLink,
823
+ });
824
+ }
825
+
826
+ return StatusPageSubscriberUnsubscribeState.Unsubscribed;
827
+ }
828
+
829
+ /*
830
+ * The subscriber a link belongs to, or null when the link is not good. The
831
+ * subscriber is found by its primary key, never by the token, and the token
832
+ * is then compared in constant time - a query on the token would let the
833
+ * database's own comparison time a guess. The comparison runs even when
834
+ * there is no subscriber to compare with.
835
+ */
836
+ private async findSubscriberByUnsubscribeLink(data: {
837
+ statusPageId: string;
838
+ subscriberId: string;
839
+ token: string;
840
+ }): Promise<Model | null> {
841
+ const isWellFormed: boolean =
842
+ ObjectID.isValidUUID(data.statusPageId) &&
843
+ ObjectID.isValidUUID(data.subscriberId);
844
+
845
+ const subscriber: Model | null = isWellFormed
846
+ ? await this.findOneBy({
847
+ query: {
848
+ _id: data.subscriberId,
849
+ statusPageId: new ObjectID(data.statusPageId),
850
+ },
851
+ select: {
852
+ _id: true,
853
+ statusPageId: true,
854
+ unsubscribeToken: true,
855
+ isUnsubscribed: true,
856
+ isAddedByTeam: true,
857
+ createdByUserId: true,
858
+ subscriberEmail: true,
859
+ subscriberPhone: true,
860
+ slackIncomingWebhookUrl: true,
861
+ slackWorkspaceName: true,
862
+ microsoftTeamsIncomingWebhookUrl: true,
863
+ microsoftTeamsWorkspaceName: true,
864
+ subscriberWebhook: true,
865
+ },
866
+ props: {
867
+ isRoot: true,
868
+ ignoreHooks: true,
869
+ },
870
+ })
871
+ : null;
872
+
873
+ const tokenMatches: boolean = StatusPageSubscriberUnsubscribeToken.matches({
874
+ stored: subscriber?.unsubscribeToken,
875
+ presented: data.token,
876
+ });
877
+
878
+ if (!subscriber || !tokenMatches) {
879
+ return null;
880
+ }
881
+
882
+ return subscriber;
883
+ }
884
+
885
+ /*
886
+ * Tell the team that a subscriber it added has unsubscribed itself (see
887
+ * StatusPageSubscriberUnsubscribeNotice for why). Subscribers people signed
888
+ * up for themselves on the status page tell nobody.
889
+ *
890
+ * A subscriber the team added is one with Is Added By Team - from the
891
+ * dashboard, with an API key or by a workflow - or, created before that
892
+ * column, with a creator (see isAddedByTeam).
893
+ *
894
+ * It goes to the status page's owners - its owner users and the members of
895
+ * its owner teams - and to the teammate who added the subscriber, when a
896
+ * teammate did, each once and only while they are still members of the
897
+ * project, as one plain email through MailService rather than a
898
+ * notification rule: it is about a subscriber list, not an event anyone is
899
+ * on call for. A page with no owners tells only the teammate who added the
900
+ * subscriber; one an API key or a workflow added, on a page with no owners,
901
+ * tells nobody.
902
+ */
903
+ @CaptureSpan()
904
+ public async notifyTeamOfUnsubscribe(data: {
905
+ subscriberId: ObjectID;
906
+ source: StatusPageSubscriberUnsubscribeSource;
907
+ unsubscribedAt: Date;
908
+ }): Promise<void> {
909
+ const subscriber: Model | null = await this.findOneById({
910
+ id: data.subscriberId,
911
+ select: {
912
+ _id: true,
913
+ projectId: true,
914
+ statusPageId: true,
915
+ isAddedByTeam: true,
916
+ createdByUserId: true,
917
+ createdAt: true,
918
+ subscriberEmail: true,
919
+ subscriberPhone: true,
920
+ slackIncomingWebhookUrl: true,
921
+ slackWorkspaceName: true,
922
+ microsoftTeamsIncomingWebhookUrl: true,
923
+ microsoftTeamsWorkspaceName: true,
924
+ subscriberWebhook: true,
925
+ },
926
+ props: {
927
+ isRoot: true,
928
+ ignoreHooks: true,
929
+ },
930
+ });
931
+
932
+ if (
933
+ !subscriber ||
934
+ !this.isAddedByTeam(subscriber) ||
935
+ !subscriber.projectId ||
936
+ !subscriber.statusPageId
937
+ ) {
938
+ return;
939
+ }
940
+
941
+ const contact: StatusPageSubscriberContact | null =
942
+ StatusPageSubscriberUnsubscribe.describeContact(subscriber, {
943
+ maskPhone: false,
944
+ });
945
+
946
+ if (!contact) {
947
+ return;
948
+ }
949
+
950
+ const statusPage: StatusPage | null = await StatusPageService.findOneById({
951
+ id: subscriber.statusPageId,
952
+ select: {
953
+ _id: true,
954
+ name: true,
955
+ pageTitle: true,
956
+ },
957
+ props: {
958
+ isRoot: true,
959
+ ignoreHooks: true,
960
+ },
961
+ });
962
+
963
+ if (!statusPage) {
964
+ return;
965
+ }
966
+
967
+ // Only a teammate's create has a creator; an API key's or a workflow's does not.
968
+ const creator: User | null = subscriber.createdByUserId
969
+ ? await UserService.findOneById({
970
+ id: subscriber.createdByUserId,
971
+ select: {
972
+ _id: true,
973
+ name: true,
974
+ email: true,
975
+ },
976
+ props: {
977
+ isRoot: true,
978
+ },
979
+ })
980
+ : null;
981
+
982
+ const candidates: Array<User> = [
983
+ ...(await StatusPageService.findOwners(subscriber.statusPageId)),
984
+ ...(creator ? [creator] : []),
985
+ ];
986
+
987
+ const recipients: Array<User> = [];
988
+ const seenUserIds: Set<string> = new Set<string>();
989
+
990
+ for (const user of await TeamMemberService.filterUsersToProjectMembers({
991
+ projectId: subscriber.projectId,
992
+ users: candidates,
993
+ })) {
994
+ const userId: string = user.id?.toString().toLowerCase() || "";
995
+
996
+ if (!userId || !user.email || seenUserIds.has(userId)) {
997
+ continue;
998
+ }
999
+
1000
+ seenUserIds.add(userId);
1001
+ recipients.push(user);
1002
+ }
1003
+
1004
+ if (recipients.length === 0) {
1005
+ return;
1006
+ }
1007
+
1008
+ if (!(await this.isFirstUnsubscribeNoticeInWindow(subscriber.id!))) {
1009
+ logger.debug(
1010
+ `The team was told about subscriber ${subscriber.id!.toString()} unsubscribing within the last ${StatusPageSubscriberUnsubscribeNotice.noticeWindowInHours} hours; not telling it again.`,
1011
+ {
1012
+ projectId: subscriber.projectId.toString(),
1013
+ statusPageId: subscriber.statusPageId.toString(),
1014
+ } as LogAttributes,
1015
+ );
1016
+ return;
1017
+ }
1018
+
1019
+ const subscriberListUrl: string = (
1020
+ await StatusPageService.getStatusPageLinkInDashboard(
1021
+ subscriber.projectId,
1022
+ subscriber.statusPageId,
1023
+ )
1024
+ )
1025
+ .addRoute(
1026
+ `/${StatusPageSubscriberUnsubscribeNotice.getSubscriberListRoute(
1027
+ contact.channel,
1028
+ )}`,
1029
+ )
1030
+ .toString();
1031
+
1032
+ const email: StatusPageSubscriberUnsubscribeNoticeEmail =
1033
+ StatusPageSubscriberUnsubscribeNotice.build({
1034
+ statusPageName:
1035
+ statusPage.name || statusPage.pageTitle || "Status Page",
1036
+ contact: contact,
1037
+ source: data.source,
1038
+ unsubscribedAt: data.unsubscribedAt,
1039
+ addedByName: creator?.name?.toString() || creator?.email?.toString(),
1040
+ addedAt: subscriber.createdAt,
1041
+ subscriberListUrl: subscriberListUrl,
1042
+ });
1043
+
1044
+ for (const recipient of recipients) {
1045
+ MailService.sendMail(
1046
+ {
1047
+ toEmail: recipient.email!,
1048
+ templateType: EmailTemplateType.SimpleMessage,
1049
+ vars: {
1050
+ subject: email.subject,
1051
+ message: email.message,
1052
+ },
1053
+ subject: email.subject,
1054
+ isSubjectLiteral: true,
1055
+ },
1056
+ {
1057
+ projectId: subscriber.projectId,
1058
+ userId: recipient.id!,
1059
+ },
1060
+ ).catch((err: Error) => {
1061
+ logger.error(err, {
1062
+ projectId: subscriber.projectId?.toString(),
1063
+ statusPageId: subscriber.statusPageId?.toString(),
1064
+ } as LogAttributes);
1065
+ });
1066
+ }
1067
+ }
1068
+
1069
+ /*
1070
+ * The team is told about a subscriber unsubscribing at most once per
1071
+ * window. The public manage page both re-subscribes a subscriber and
1072
+ * cancels it again, and it is open to anyone who can see the status page
1073
+ * and knows the subscriber's id (it is in the manage link), so without this
1074
+ * a loop of the two would email every owner of the page on every turn.
1075
+ *
1076
+ * One atomic SET NX per subscriber: exactly one notice wins the window
1077
+ * however many cancellations arrive together. If the cache cannot be
1078
+ * reached the notice goes out: telling the team twice is better than not
1079
+ * at all.
1080
+ */
1081
+ private async isFirstUnsubscribeNoticeInWindow(
1082
+ subscriberId: ObjectID,
1083
+ ): Promise<boolean> {
1084
+ try {
1085
+ return await GlobalCache.setStringIfNotExists(
1086
+ StatusPageSubscriberUnsubscribeNotice.noticeCacheNamespace,
1087
+ subscriberId.toString().toLowerCase(),
1088
+ OneUptimeDate.getCurrentDate().toISOString(),
1089
+ {
1090
+ expiresInSeconds:
1091
+ StatusPageSubscriberUnsubscribeNotice.noticeWindowInHours * 60 * 60,
1092
+ },
1093
+ );
1094
+ } catch (err) {
1095
+ logger.warn(
1096
+ `Could not check when the team was last told about subscriber ${subscriberId.toString()} unsubscribing; telling it: ${err instanceof Error ? err.message : String(err)}`,
1097
+ );
1098
+ return true;
1099
+ }
1100
+ }
1101
+
382
1102
  @CaptureSpan()
383
1103
  protected override async onCreateSuccess(
384
1104
  onCreate: OnCreate<Model>,
385
1105
  createdItem: Model,
386
1106
  ): Promise<Model> {
387
- logger.debug("onCreateSuccess called with createdItem:", {
388
- projectId: createdItem.projectId?.toString(),
389
- } as LogAttributes);
390
- logger.debug(createdItem, {
391
- projectId: createdItem.projectId?.toString(),
392
- } as LogAttributes);
1107
+ // Not the item itself: it carries the unsubscribe token.
1108
+ logger.debug(
1109
+ `onCreateSuccess called with createdItem ${createdItem.id?.toString()}.`,
1110
+ {
1111
+ projectId: createdItem.projectId?.toString(),
1112
+ } as LogAttributes,
1113
+ );
393
1114
 
394
1115
  if (!createdItem.statusPageId) {
395
1116
  logger.debug("Status Page ID is missing in createdItem.", {
@@ -413,13 +1134,11 @@ export class Service extends DatabaseService<Model> {
413
1134
  projectId: createdItem.projectId?.toString(),
414
1135
  } as LogAttributes);
415
1136
 
1137
+ // createdItem carries the unsubscribe token onBeforeCreate minted.
416
1138
  const unsubscribeLink: string = this.getUnsubscribeLink(
417
1139
  URL.fromString(statusPageURL),
418
- createdItem.id!,
1140
+ createdItem,
419
1141
  ).toString();
420
- logger.debug(`Unsubscribe Link: ${unsubscribeLink}`, {
421
- projectId: createdItem.projectId?.toString(),
422
- } as LogAttributes);
423
1142
 
424
1143
  if (
425
1144
  createdItem.statusPageId &&
@@ -462,10 +1181,24 @@ export class Service extends DatabaseService<Model> {
462
1181
  { projectId: createdItem.projectId?.toString() } as LogAttributes,
463
1182
  );
464
1183
 
1184
+ /*
1185
+ * On a public status page the SMS keeps the shorter manage link, which
1186
+ * works there without signing in: an SMS is billed by the segment (see
1187
+ * StatusPageSubscriberUnsubscribe.buildSmsLink).
1188
+ */
1189
+ const smsUnsubscribeLink: string =
1190
+ StatusPageSubscriberUnsubscribe.buildSmsLink({
1191
+ isPublicStatusPage: (onCreate.carryForward as StatusPage | undefined)
1192
+ ?.isPublicStatusPage,
1193
+ statusPageUrl: statusPageURL,
1194
+ subscriberId: createdItem.id!,
1195
+ unsubscribeUrl: unsubscribeLink,
1196
+ });
1197
+
465
1198
  SmsService.sendSms(
466
1199
  {
467
1200
  to: createdItem.subscriberPhone,
468
- message: `You have been subscribed to ${statusPageName}. To unsubscribe, click on the link: ${unsubscribeLink}`,
1201
+ message: `You have been subscribed to ${statusPageName}. To unsubscribe, click on the link: ${smsUnsubscribeLink}`,
469
1202
  },
470
1203
  {
471
1204
  projectId: createdItem.projectId,
@@ -528,7 +1261,7 @@ export class Service extends DatabaseService<Model> {
528
1261
  **You have successfully subscribed to receive status updates!**
529
1262
 
530
1263
  🔗 **Status Page:** [${statusPageName}](${statusPageURL})
531
- 📧 **Manage Subscription:** [Update preferences or unsubscribe](${unsubscribeLink})
1264
+ 🔕 **Unsubscribe:** [Stop these notifications](${unsubscribeLink})
532
1265
 
533
1266
  You will receive real-time notifications for:
534
1267
  • Incidents and outages
@@ -611,7 +1344,7 @@ Stay informed about service availability! 🚀`;
611
1344
  **You have successfully subscribed to receive status updates!**
612
1345
 
613
1346
  🔗 **Status Page:** [${statusPageName}](${statusPageURL})
614
- 📧 **Manage Subscription:** [Update preferences or unsubscribe](${unsubscribeLink})
1347
+ 🔕 **Unsubscribe:** [Stop these notifications](${unsubscribeLink})
615
1348
 
616
1349
  You will receive real-time notifications for:
617
1350
  • Incidents and outages
@@ -675,13 +1408,14 @@ Stay informed about service availability! 🚀`;
675
1408
  projectId: true,
676
1409
  subscriptionConfirmationToken: true,
677
1410
  sendYouHaveSubscribedMessage: true,
1411
+ unsubscribeToken: true,
678
1412
  },
679
1413
  props: {
680
1414
  isRoot: true,
681
1415
  ignoreHooks: true,
682
1416
  },
683
1417
  });
684
- logger.debug(`Found Subscriber: ${JSON.stringify(subscriber)}`, {
1418
+ logger.debug(`Found Subscriber: ${subscriber?.id?.toString()}`, {
685
1419
  statusPageSubscriberId: data.subscriberId?.toString(),
686
1420
  } as LogAttributes);
687
1421
 
@@ -693,6 +1427,9 @@ Stay informed about service availability! 🚀`;
693
1427
  return;
694
1428
  }
695
1429
 
1430
+ // Its unsubscribe link needs its token (see ensureUnsubscribeTokens).
1431
+ await this.ensureUnsubscribeTokens([subscriber]);
1432
+
696
1433
  const statusPage: StatusPage | null = await StatusPageService.findOneBy({
697
1434
  query: {
698
1435
  _id: subscriber.statusPageId.toString(),
@@ -777,11 +1514,8 @@ Stay informed about service availability! 🚀`;
777
1514
  ) {
778
1515
  const unsubscribeUrl: string = this.getUnsubscribeLink(
779
1516
  URL.fromString(statusPageURL),
780
- subscriber.id!,
1517
+ subscriber,
781
1518
  ).toString();
782
- logger.debug(`Unsubscribe URL: ${unsubscribeUrl}`, {
783
- statusPageSubscriberId: data.subscriberId?.toString(),
784
- } as LogAttributes);
785
1519
 
786
1520
  const customTemplate: StatusPageSubscriberNotificationTemplate | null =
787
1521
  await StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage(
@@ -806,8 +1540,9 @@ Stay informed about service availability! 🚀`;
806
1540
  * pattern used elsewhere — without custom SMTP we keep the styled
807
1541
  * OneUptime default so emails still look right out-of-the-box).
808
1542
  */
1543
+ // The body is HTML, so the (plain-text) values are escaped into it.
809
1544
  const compiledBody: string =
810
- StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
1545
+ StatusPageSubscriberNotificationTemplateServiceClass.compileEmailBodyTemplate(
811
1546
  customTemplate.templateBody,
812
1547
  templateVariables,
813
1548
  );
@@ -909,13 +1644,14 @@ Stay informed about service availability! 🚀`;
909
1644
  subscriberPhone: true,
910
1645
  projectId: true,
911
1646
  sendYouHaveSubscribedMessage: true,
1647
+ unsubscribeToken: true,
912
1648
  },
913
1649
  props: {
914
1650
  isRoot: true,
915
1651
  ignoreHooks: true,
916
1652
  },
917
1653
  });
918
- logger.debug(`Found Subscriber: ${JSON.stringify(subscriber)}`, {
1654
+ logger.debug(`Found Subscriber: ${subscriber?.id?.toString()}`, {
919
1655
  statusPageSubscriberId: data.subscriberId?.toString(),
920
1656
  } as LogAttributes);
921
1657
 
@@ -927,6 +1663,9 @@ Stay informed about service availability! 🚀`;
927
1663
  return;
928
1664
  }
929
1665
 
1666
+ // Its unsubscribe link needs its token (see ensureUnsubscribeTokens).
1667
+ await this.ensureUnsubscribeTokens([subscriber]);
1668
+
930
1669
  const statusPage: StatusPage | null = await StatusPageService.findOneBy({
931
1670
  query: {
932
1671
  _id: subscriber.statusPageId.toString(),
@@ -997,11 +1736,8 @@ Stay informed about service availability! 🚀`;
997
1736
 
998
1737
  const unsubscribeLink: string = this.getUnsubscribeLink(
999
1738
  URL.fromString(statusPageURL),
1000
- subscriber.id!,
1739
+ subscriber,
1001
1740
  ).toString();
1002
- logger.debug(`Unsubscribe Link: ${unsubscribeLink}`, {
1003
- statusPageSubscriberId: data.subscriberId?.toString(),
1004
- } as LogAttributes);
1005
1741
 
1006
1742
  if (
1007
1743
  subscriber.statusPageId &&
@@ -1029,8 +1765,9 @@ Stay informed about service availability! 🚀`;
1029
1765
  };
1030
1766
 
1031
1767
  if (customTemplate?.templateBody && statusPage.smtpConfig) {
1768
+ // The body is HTML, so the (plain-text) values are escaped into it.
1032
1769
  const compiledBody: string =
1033
- StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
1770
+ StatusPageSubscriberNotificationTemplateServiceClass.compileEmailBodyTemplate(
1034
1771
  customTemplate.templateBody,
1035
1772
  templateVariables,
1036
1773
  );
@@ -1139,10 +1876,26 @@ Stay informed about service availability! 🚀`;
1139
1876
  return confirmSubscriptionLink;
1140
1877
  }
1141
1878
 
1879
+ /*
1880
+ * The confirmed, still-subscribed subscribers of a status page, in _id
1881
+ * order, at most `limit` (LIMIT_MAX by default) of them.
1882
+ *
1883
+ * One read stops at the limit, so a page with more subscribers than that
1884
+ * is read in batches: pass the _id of the last subscriber of one batch as
1885
+ * `afterId` to read the next, until a batch comes back smaller than the
1886
+ * limit (SubscriberNotificationFanOut does this for the subscriber jobs).
1887
+ * The cursor is an _id rather than an offset, so a subscriber who
1888
+ * unsubscribes while a send is part-way through cannot shift the rows
1889
+ * after it into a batch already read, and nobody is skipped.
1890
+ */
1142
1891
  @CaptureSpan()
1143
1892
  public async getSubscribersByStatusPage(
1144
1893
  statusPageId: ObjectID,
1145
1894
  props: DatabaseCommonInteractionProps,
1895
+ options?: {
1896
+ afterId?: ObjectID | undefined;
1897
+ limit?: number | undefined;
1898
+ },
1146
1899
  ): Promise<Array<Model>> {
1147
1900
  logger.debug("getSubscribersByStatusPage called with statusPageId:", {
1148
1901
  statusPageId: statusPageId?.toString(),
@@ -1162,6 +1915,9 @@ Stay informed about service availability! 🚀`;
1162
1915
  statusPageId: statusPageId,
1163
1916
  isUnsubscribed: false,
1164
1917
  isSubscriptionConfirmed: true,
1918
+ ...(options?.afterId
1919
+ ? { _id: QueryHelper.greaterThan(options.afterId) }
1920
+ : {}),
1165
1921
  },
1166
1922
  select: {
1167
1923
  _id: true,
@@ -1174,51 +1930,380 @@ Stay informed about service availability! 🚀`;
1174
1930
  statusPageResources: true,
1175
1931
  isSubscribedToAllEventTypes: true,
1176
1932
  statusPageEventTypes: true,
1933
+ // Every sender puts this subscriber's unsubscribe link in its message.
1934
+ unsubscribeToken: true,
1935
+ /*
1936
+ * Whether the team added this subscriber rather than it signing up
1937
+ * on the status page (see isAddedByTeam).
1938
+ */
1939
+ isAddedByTeam: true,
1940
+ createdByUserId: true,
1941
+ },
1942
+ // A stable order, which the afterId cursor reads on from.
1943
+ sort: {
1944
+ _id: SortOrder.Ascending,
1177
1945
  },
1178
1946
  skip: 0,
1179
- limit: LIMIT_MAX,
1947
+ limit: options?.limit || LIMIT_MAX,
1180
1948
  props: props,
1181
1949
  });
1182
1950
 
1183
- logger.debug("Found subscribers:", {
1184
- statusPageId: statusPageId?.toString(),
1185
- } as LogAttributes);
1186
- logger.debug(subscribers, {
1951
+ await this.ensureUnsubscribeTokens(subscribers);
1952
+
1953
+ /*
1954
+ * The count, not the rows: each row now carries its unsubscribe token,
1955
+ * which is a credential and has no place in a log line.
1956
+ */
1957
+ logger.debug(`Found ${subscribers.length} subscribers.`, {
1187
1958
  statusPageId: statusPageId?.toString(),
1188
1959
  } as LogAttributes);
1189
1960
 
1190
1961
  return subscribers;
1191
1962
  }
1192
1963
 
1964
+ /*
1965
+ * How many subscribers of each status page each channel reaches, for the
1966
+ * audience summary shown before an incident or a public note is sent
1967
+ * (IncidentSubscriberAudience). The same subscribers getSubscribersByStatusPage
1968
+ * sends to - confirmed, not unsubscribed - counted per channel in one
1969
+ * aggregate query rather than read out: it never loads an address.
1970
+ *
1971
+ * A channel counts a subscriber whenever the job would send on it, which is
1972
+ * whenever that column holds a value; a subscription with both an email and
1973
+ * a phone counts once on each. Rows are pinned to the project, whatever ids
1974
+ * the caller passes.
1975
+ *
1976
+ * Keyed by the lower-cased status page id. A page with no active subscriber
1977
+ * has no entry.
1978
+ */
1979
+ @CaptureSpan()
1980
+ public async countActiveSubscribersByChannel(data: {
1981
+ projectId: ObjectID;
1982
+ statusPageIds: Array<ObjectID>;
1983
+ }): Promise<Dictionary<IncidentSubscriberAudienceCounts>> {
1984
+ const counts: Dictionary<IncidentSubscriberAudienceCounts> = {};
1985
+
1986
+ const statusPageIds: Array<string> = [];
1987
+
1988
+ for (const statusPageId of data.statusPageIds) {
1989
+ const id: string = statusPageId.toString().trim().toLowerCase();
1990
+
1991
+ if (id && !statusPageIds.includes(id)) {
1992
+ statusPageIds.push(id);
1993
+ }
1994
+ }
1995
+
1996
+ if (statusPageIds.length === 0) {
1997
+ return counts;
1998
+ }
1999
+
2000
+ const rows: Array<{
2001
+ statusPageId: string;
2002
+ email: string;
2003
+ sms: string;
2004
+ slack: string;
2005
+ microsoftTeams: string;
2006
+ webhook: string;
2007
+ }> = await this.getRepository().manager.query(
2008
+ `SELECT
2009
+ "statusPageId"::text AS "statusPageId",
2010
+ COUNT(*) FILTER (WHERE NULLIF(TRIM("subscriberEmail"), '') IS NOT NULL)::text AS "email",
2011
+ COUNT(*) FILTER (WHERE NULLIF(TRIM("subscriberPhone"), '') IS NOT NULL)::text AS "sms",
2012
+ COUNT(*) FILTER (WHERE NULLIF(TRIM("slackIncomingWebhookUrl"), '') IS NOT NULL)::text AS "slack",
2013
+ COUNT(*) FILTER (WHERE NULLIF(TRIM("microsoftTeamsIncomingWebhookUrl"), '') IS NOT NULL)::text AS "microsoftTeams",
2014
+ COUNT(*) FILTER (WHERE NULLIF(TRIM("subscriberWebhook"), '') IS NOT NULL)::text AS "webhook"
2015
+ FROM "StatusPageSubscriber"
2016
+ WHERE "projectId" = $1
2017
+ AND "statusPageId" = ANY($2::uuid[])
2018
+ AND "isUnsubscribed" = false
2019
+ AND "isSubscriptionConfirmed" = true
2020
+ AND "deletedAt" IS NULL
2021
+ GROUP BY "statusPageId"`,
2022
+ [data.projectId.toString(), statusPageIds],
2023
+ );
2024
+
2025
+ const toCount: (value: string | undefined) => number = (
2026
+ value: string | undefined,
2027
+ ): number => {
2028
+ const count: number = parseInt(value || "0", 10);
2029
+ return Number.isFinite(count) && count > 0 ? count : 0;
2030
+ };
2031
+
2032
+ for (const row of rows) {
2033
+ counts[row.statusPageId.toLowerCase()] = {
2034
+ email: toCount(row.email),
2035
+ sms: toCount(row.sms),
2036
+ slack: toCount(row.slack),
2037
+ microsoftTeams: toCount(row.microsoftTeams),
2038
+ webhook: toCount(row.webhook),
2039
+ };
2040
+ }
2041
+
2042
+ return counts;
2043
+ }
2044
+
2045
+ /*
2046
+ * The link to a subscriber's unsubscribe page, which every notification to
2047
+ * it carries: {statusPageUrl}/unsubscribe/{id}-{token}. It works on public
2048
+ * and private status pages alike and without signing in, and it only ever
2049
+ * asks - the page unsubscribes when its reader confirms (see
2050
+ * Common/Types/StatusPage/StatusPageSubscriberUnsubscribe).
2051
+ *
2052
+ * Pass the subscriber read with its unsubscribeToken: every loader that
2053
+ * feeds a sender selects it (getSubscribersByStatusPage and the subscribe
2054
+ * and manage emails). A subscriber without one still gets a link, to the
2055
+ * page that says the link is out of date, rather than none; that is logged,
2056
+ * because only a subscriber created around the migration that added the
2057
+ * token, or one written with hooks skipped, can lack it.
2058
+ */
1193
2059
  public getUnsubscribeLink(
2060
+ statusPageUrl: URL,
2061
+ subscriber: Pick<Model, "_id" | "id" | "unsubscribeToken">,
2062
+ ): URL {
2063
+ const subscriberId: ObjectID | null = subscriber.id;
2064
+
2065
+ if (!subscriberId) {
2066
+ throw new BadDataException(
2067
+ "A subscriber id is required to build an unsubscribe link.",
2068
+ );
2069
+ }
2070
+
2071
+ if (
2072
+ !StatusPageSubscriberUnsubscribe.isWellFormedToken(
2073
+ subscriber.unsubscribeToken,
2074
+ )
2075
+ ) {
2076
+ logger.error(
2077
+ `Status page subscriber ${subscriberId.toString()} has no unsubscribe token; its unsubscribe link leads to the out-of-date link page.`,
2078
+ {
2079
+ statusPageSubscriberId: subscriberId.toString(),
2080
+ } as LogAttributes,
2081
+ );
2082
+ }
2083
+
2084
+ return StatusPageSubscriberUnsubscribe.buildLink({
2085
+ statusPageUrl: statusPageUrl,
2086
+ subscriberId: subscriberId,
2087
+ unsubscribeToken: subscriber.unsubscribeToken,
2088
+ });
2089
+ }
2090
+
2091
+ /*
2092
+ * The subscriber's Update Subscription page, where it can choose resources
2093
+ * and event types (and unsubscribe). The "manage your subscription" email a
2094
+ * visitor asks for from the Subscribe page links here. On a private status
2095
+ * page it needs a signed-in visitor, like the rest of the page.
2096
+ */
2097
+ public getManageSubscriptionLink(
1194
2098
  statusPageUrl: URL,
1195
2099
  statusPageSubscriberId: ObjectID,
1196
2100
  ): URL {
1197
- logger.debug("getUnsubscribeLink called with statusPageUrl:", {
1198
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1199
- } as LogAttributes);
1200
- logger.debug(statusPageUrl, {
1201
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1202
- } as LogAttributes);
1203
- logger.debug("statusPageSubscriberId:", {
1204
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1205
- } as LogAttributes);
1206
- logger.debug(statusPageSubscriberId, {
1207
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1208
- } as LogAttributes);
2101
+ return StatusPageSubscriberUnsubscribe.buildManageSubscriptionLink({
2102
+ statusPageUrl: statusPageUrl,
2103
+ subscriberId: statusPageSubscriberId,
2104
+ });
2105
+ }
1209
2106
 
1210
- const unsubscribeLink: URL = URL.fromString(
1211
- statusPageUrl.toString(),
1212
- ).addRoute("/update-subscription/" + statusPageSubscriberId.toString());
2107
+ /*
2108
+ * Give every subscriber in the list an unsubscribe token it lacks, in place,
2109
+ * so the links built from them work. Every create mints one, and the
2110
+ * BackfillStatusPageSubscriberUnsubscribeColumns data migration gives one
2111
+ * to every subscriber that existed before; this covers the ones that
2112
+ * migration has not reached yet (it runs after the upgrade, in batches), a
2113
+ * subscriber created in between, and one written with hooks skipped.
2114
+ *
2115
+ * One statement per batch of subscribers, not per subscriber: until the
2116
+ * backfill has run, a whole page's list can be missing its tokens.
2117
+ *
2118
+ * The write only fills an empty column, so two senders racing on the same
2119
+ * subscriber - or a sender and the backfill - cannot hand out two different
2120
+ * tokens: the loser reads back the winner's.
2121
+ */
2122
+ @CaptureSpan()
2123
+ public async ensureUnsubscribeTokens(
2124
+ subscribers: Array<Model>,
2125
+ ): Promise<void> {
2126
+ // The rows that lack one, by id: a list can hold the same subscriber twice.
2127
+ const missing: Map<string, Array<Model>> = new Map<string, Array<Model>>();
1213
2128
 
1214
- logger.debug("Generated Unsubscribe Link:", {
1215
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1216
- } as LogAttributes);
1217
- logger.debug(unsubscribeLink, {
1218
- statusPageSubscriberId: statusPageSubscriberId?.toString(),
1219
- } as LogAttributes);
2129
+ for (const subscriber of subscribers) {
2130
+ if (
2131
+ !subscriber._id ||
2132
+ StatusPageSubscriberUnsubscribe.isWellFormedToken(
2133
+ subscriber.unsubscribeToken,
2134
+ )
2135
+ ) {
2136
+ continue;
2137
+ }
2138
+
2139
+ const id: string = subscriber._id.toString().toLowerCase();
2140
+ missing.set(id, [...(missing.get(id) || []), subscriber]);
2141
+ }
2142
+
2143
+ const ids: Array<string> = Array.from(missing.keys());
2144
+
2145
+ for (
2146
+ let start: number = 0;
2147
+ start < ids.length;
2148
+ start += UNSUBSCRIBE_COLUMNS_BATCH_SIZE
2149
+ ) {
2150
+ const batch: Array<string> = ids.slice(
2151
+ start,
2152
+ start + UNSUBSCRIBE_COLUMNS_BATCH_SIZE,
2153
+ );
2154
+
2155
+ await this.fillMissingUnsubscribeTokens(batch);
1220
2156
 
1221
- return unsubscribeLink;
2157
+ const stored: Array<Model> = await this.findBy({
2158
+ query: {
2159
+ _id: QueryHelper.any(batch),
2160
+ },
2161
+ select: {
2162
+ _id: true,
2163
+ unsubscribeToken: true,
2164
+ },
2165
+ skip: 0,
2166
+ limit: batch.length,
2167
+ props: {
2168
+ isRoot: true,
2169
+ ignoreHooks: true,
2170
+ },
2171
+ });
2172
+
2173
+ for (const row of stored) {
2174
+ if (
2175
+ !row._id ||
2176
+ !StatusPageSubscriberUnsubscribe.isWellFormedToken(
2177
+ row.unsubscribeToken,
2178
+ )
2179
+ ) {
2180
+ continue;
2181
+ }
2182
+
2183
+ for (const subscriber of missing.get(
2184
+ row._id.toString().toLowerCase(),
2185
+ ) || []) {
2186
+ subscriber.unsubscribeToken = row.unsubscribeToken;
2187
+ }
2188
+ }
2189
+ }
2190
+ }
2191
+
2192
+ /*
2193
+ * The BackfillStatusPageSubscriberUnsubscribeColumns data migration: give
2194
+ * every subscriber that existed before the upgrade its unsubscribe token,
2195
+ * and mark the ones with a creator as added by the team (Is Added By
2196
+ * Team). Soft-deleted subscribers are included: one that is restored must
2197
+ * not come back without a token.
2198
+ *
2199
+ * The table is walked in primary key order, a batch of ids at a time, and
2200
+ * each batch is written by primary key in short statements of their own.
2201
+ * So however large the table, no statement holds its rows for long or runs
2202
+ * into the connection's statement timeout, and notifications, sign-ups and
2203
+ * the subscriber lists carry on while it runs - which one UPDATE of every
2204
+ * row, in the migration that added the columns, would have stopped.
2205
+ *
2206
+ * Idempotent, and safe to run twice at once (the data migration runner is
2207
+ * not serialized): every write only fills what is still empty, and
2208
+ * re-checks that under the row lock, so a token a sender has already put
2209
+ * in a message is never replaced.
2210
+ */
2211
+ @CaptureSpan()
2212
+ public async backfillUnsubscribeColumns(options?: {
2213
+ batchSize?: number | undefined;
2214
+ }): Promise<StatusPageSubscriberUnsubscribeBackfillResult> {
2215
+ const batchSize: number = Math.max(
2216
+ 1,
2217
+ Math.floor(options?.batchSize || UNSUBSCRIBE_COLUMNS_BATCH_SIZE),
2218
+ );
2219
+
2220
+ const result: StatusPageSubscriberUnsubscribeBackfillResult = {
2221
+ tokensGiven: 0,
2222
+ markedAddedByTeam: 0,
2223
+ };
2224
+
2225
+ let lastId: string | null = null;
2226
+
2227
+ for (;;) {
2228
+ const rows: Array<{
2229
+ _id: string;
2230
+ needsToken: boolean;
2231
+ needsAddedByTeam: boolean;
2232
+ }> = await this.getRepository().manager.query(
2233
+ `SELECT "_id"::text AS "_id",
2234
+ ("unsubscribeToken" IS NULL) AS "needsToken",
2235
+ ("isAddedByTeam" = false AND "createdByUserId" IS NOT NULL) AS "needsAddedByTeam"
2236
+ FROM "StatusPageSubscriber"
2237
+ WHERE ($2::uuid IS NULL OR "_id" > $2::uuid)
2238
+ ORDER BY "_id" ASC
2239
+ LIMIT $1`,
2240
+ [batchSize, lastId],
2241
+ );
2242
+
2243
+ if (rows.length === 0) {
2244
+ break;
2245
+ }
2246
+
2247
+ const needToken: Array<string> = rows
2248
+ .filter((row: { needsToken: boolean }): boolean => {
2249
+ return row.needsToken === true;
2250
+ })
2251
+ .map((row: { _id: string }): string => {
2252
+ return row._id;
2253
+ });
2254
+
2255
+ const needAddedByTeam: Array<string> = rows
2256
+ .filter((row: { needsAddedByTeam: boolean }): boolean => {
2257
+ return row.needsAddedByTeam === true;
2258
+ })
2259
+ .map((row: { _id: string }): string => {
2260
+ return row._id;
2261
+ });
2262
+
2263
+ result.tokensGiven += await this.fillMissingUnsubscribeTokens(needToken);
2264
+
2265
+ if (needAddedByTeam.length > 0) {
2266
+ result.markedAddedByTeam += affectedRowCount(
2267
+ await this.getRepository().manager.query(
2268
+ `UPDATE "StatusPageSubscriber" SET "isAddedByTeam" = true WHERE "_id" = ANY($1::uuid[]) AND "createdByUserId" IS NOT NULL AND "isAddedByTeam" = false`,
2269
+ [needAddedByTeam],
2270
+ ),
2271
+ );
2272
+ }
2273
+
2274
+ lastId = rows[rows.length - 1]!._id;
2275
+
2276
+ if (rows.length < batchSize) {
2277
+ break;
2278
+ }
2279
+ }
2280
+
2281
+ return result;
2282
+ }
2283
+
2284
+ /*
2285
+ * Mint a token, from Node's CSPRNG like every other, for each of these
2286
+ * subscribers that has none, in one statement. Returns how many were given
2287
+ * one. `"unsubscribeToken" IS NULL` is checked again under each row's lock,
2288
+ * so a token another writer stored first is kept.
2289
+ */
2290
+ private async fillMissingUnsubscribeTokens(
2291
+ subscriberIds: Array<string>,
2292
+ ): Promise<number> {
2293
+ if (subscriberIds.length === 0) {
2294
+ return 0;
2295
+ }
2296
+
2297
+ const tokens: Array<string> = subscriberIds.map((): string => {
2298
+ return StatusPageSubscriberUnsubscribeToken.generate();
2299
+ });
2300
+
2301
+ return affectedRowCount(
2302
+ await this.getRepository().manager.query(
2303
+ `UPDATE "StatusPageSubscriber" AS "subscriber" SET "unsubscribeToken" = "minted"."token" FROM unnest($1::uuid[], $2::text[]) AS "minted"("id", "token") WHERE "subscriber"."_id" = "minted"."id" AND "subscriber"."unsubscribeToken" IS NULL`,
2304
+ [subscriberIds, tokens],
2305
+ ),
2306
+ );
1222
2307
  }
1223
2308
 
1224
2309
  public shouldSendNotification(data: {
@@ -1227,12 +2312,17 @@ Stay informed about service availability! 🚀`;
1227
2312
  statusPage: StatusPage;
1228
2313
  eventType: StatusPageEventType;
1229
2314
  }): boolean {
1230
- logger.debug("shouldSendNotification called with data:", {
1231
- statusPageId: data.statusPage?.id?.toString(),
1232
- } as LogAttributes);
1233
- logger.debug(data, {
1234
- statusPageId: data.statusPage?.id?.toString(),
1235
- } as LogAttributes);
2315
+ /*
2316
+ * Ids only: the subscriber carries its unsubscribe token, which is a
2317
+ * credential (it cancels the subscription, private pages included), and
2318
+ * every subscriber job calls this for every subscriber.
2319
+ */
2320
+ logger.debug(
2321
+ `shouldSendNotification called for subscriber ${data.subscriber?._id?.toString() || ""} (${data.eventType}, ${data.statusPageResources?.length || 0} resource(s)).`,
2322
+ {
2323
+ statusPageId: data.statusPage?.id?.toString(),
2324
+ } as LogAttributes,
2325
+ );
1236
2326
 
1237
2327
  let shouldSendNotification: boolean = true; // default to true.
1238
2328
 
@@ -1410,6 +2500,13 @@ Stay informed about service availability! 🚀`;
1410
2500
  * every episode notification from being sent.
1411
2501
  */
1412
2502
  showEpisodesOnStatusPage: true,
2503
+ /*
2504
+ * IncidentStatusPageScope decides from this which incidents reach a
2505
+ * page, and treats a page without it as showing only incidents
2506
+ * limited to it - so leaving it out would silence every unscoped
2507
+ * incident's notifications.
2508
+ */
2509
+ onlyShowScopedIncidents: true,
1413
2510
  },
1414
2511
  });
1415
2512