@oneuptime/common 14.0.7 → 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 (440) 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/StatusPage.ts +55 -0
  6. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  7. package/Server/API/IncidentAPI.ts +162 -0
  8. package/Server/API/StatusPageAPI.ts +487 -171
  9. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  18. package/Server/Infrastructure/Semaphore.ts +10 -0
  19. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  20. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  21. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  22. package/Server/Services/DatabaseService.ts +93 -10
  23. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  24. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  25. package/Server/Services/IncidentEpisodeService.ts +15 -0
  26. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  27. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  28. package/Server/Services/IncidentService.ts +1535 -65
  29. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  30. package/Server/Services/IncidentTemplateService.ts +109 -0
  31. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  32. package/Server/Services/ScheduledMaintenanceService.ts +68 -18
  33. package/Server/Services/SmsService.ts +10 -0
  34. package/Server/Services/StatusPageService.ts +65 -8
  35. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  36. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  37. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  38. package/Server/Types/Markdown.ts +153 -6
  39. package/Server/Types/Workflow/Components/Email.ts +61 -2
  40. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  41. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  42. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  43. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  44. package/Server/Utils/SSRFProtection.ts +76 -16
  45. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  46. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  47. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  48. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  49. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  50. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  51. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  52. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  53. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  54. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  55. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  56. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  57. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  58. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  59. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  60. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  61. package/Server/Utils/StatusPageResource.ts +51 -20
  62. package/Server/Utils/VM/VMAPI.ts +1 -0
  63. package/Server/Utils/VM/VMRunner.ts +10 -0
  64. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  65. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  66. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  67. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  68. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  69. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  70. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  71. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  72. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  73. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  74. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  75. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
  76. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  77. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  78. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  79. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  80. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  81. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  82. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  83. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  84. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  85. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  86. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  87. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  88. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  89. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  90. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  91. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  92. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  93. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  94. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  95. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  96. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  97. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  98. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  99. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  100. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  101. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  102. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  103. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  104. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  105. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  106. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  107. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  108. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  109. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  110. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  111. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  112. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  113. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  114. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  115. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  116. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  117. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  118. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  119. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  120. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  121. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  122. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  123. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  124. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  125. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  126. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  127. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  128. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  129. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  130. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  131. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  132. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  133. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  134. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  135. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  136. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  137. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  138. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  139. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  140. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  141. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  142. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  143. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  144. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  145. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  146. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  147. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  148. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  149. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  150. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  151. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  152. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  153. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  154. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  155. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  156. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  157. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  158. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  159. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  160. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  161. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  162. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  163. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  164. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  165. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  166. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  167. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  168. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  169. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  170. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  171. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  172. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  173. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  174. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  175. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  176. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  177. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  178. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  179. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  180. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  181. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  182. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  183. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  184. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  185. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  186. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  187. package/Tests/Types/SafeHtml.test.ts +102 -0
  188. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  189. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  190. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  191. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  192. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  193. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  194. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  195. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  196. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  197. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  198. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  199. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  200. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  201. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  202. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  203. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  204. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  205. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  206. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  207. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  208. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  209. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  210. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  211. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  212. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  213. package/Types/CustomField/CustomFieldType.ts +14 -0
  214. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  215. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  216. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  217. package/Types/SafeHtml.ts +93 -0
  218. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  219. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  220. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  221. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  222. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  223. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  224. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  225. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  226. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  227. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  228. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  229. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  230. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  231. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  232. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  233. package/UI/Components/Forms/ModelForm.tsx +70 -8
  234. package/UI/Components/Forms/Validation.ts +1 -1
  235. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  236. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  237. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  238. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  239. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  240. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  241. package/Utils/API.ts +247 -2
  242. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  243. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  244. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  245. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  246. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  247. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  248. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  249. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  250. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  251. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  252. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  253. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  254. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  255. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  256. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  257. package/build/dist/Server/API/IncidentAPI.js +100 -1
  258. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  259. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  260. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  261. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  262. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  263. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  264. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  265. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  266. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  267. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  268. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  269. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  270. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  271. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  272. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  273. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  274. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  275. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  276. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  277. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  278. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  279. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  280. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  281. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  282. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  283. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  284. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  285. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  286. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  287. package/build/dist/Server/Services/DatabaseService.js +56 -7
  288. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  289. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  290. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  291. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  292. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  293. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  294. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  295. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  296. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  297. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  298. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  299. package/build/dist/Server/Services/IncidentService.js +1052 -0
  300. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  301. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  302. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  303. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  304. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  305. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  306. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  307. package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
  308. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  309. package/build/dist/Server/Services/SmsService.js +2 -22
  310. package/build/dist/Server/Services/SmsService.js.map +1 -1
  311. package/build/dist/Server/Services/StatusPageService.js +47 -9
  312. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  313. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  314. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  315. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  316. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  317. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  318. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  319. package/build/dist/Server/Types/Markdown.js +110 -6
  320. package/build/dist/Server/Types/Markdown.js.map +1 -1
  321. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  322. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  323. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  324. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  325. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  326. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  327. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  328. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  329. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  330. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  331. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  332. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  333. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  334. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  335. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  336. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  337. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  338. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  339. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  340. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  341. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  342. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  343. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  344. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  345. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  346. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  348. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  349. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  350. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  351. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  352. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  353. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  354. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  355. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  356. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  357. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  358. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  359. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  360. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  361. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  362. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  363. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  364. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  365. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  366. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  367. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  368. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  369. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  370. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  371. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  372. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  373. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  374. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  375. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  376. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  377. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  378. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  379. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  380. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  381. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  382. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  383. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  384. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  385. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  386. package/build/dist/Types/SafeHtml.js +84 -0
  387. package/build/dist/Types/SafeHtml.js.map +1 -0
  388. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  389. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  390. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  391. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  392. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  393. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  394. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  395. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  396. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  397. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  398. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  399. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  400. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  401. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  402. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  403. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  404. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  405. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  406. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  407. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  408. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  409. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  410. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  411. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  412. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  413. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  414. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  415. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  416. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  417. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  418. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  419. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  420. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  421. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  422. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  423. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  424. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  425. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  426. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  427. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  428. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  429. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  430. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  431. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  432. package/build/dist/Utils/API.js +177 -1
  433. package/build/dist/Utils/API.js.map +1 -1
  434. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  435. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  436. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  437. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  438. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  439. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  440. package/package.json +1 -1
@@ -0,0 +1,275 @@
1
+ import Semaphore, {
2
+ SemaphoreLockTimeoutError,
3
+ SemaphorePermit,
4
+ } from "../../../../Server/Infrastructure/Semaphore";
5
+ import SubscriberNotificationRunLimit from "../../../../Server/Utils/StatusPage/SubscriberNotificationRunLimit";
6
+ import SubscriberNotificationTiming from "../../../../Server/Utils/StatusPage/SubscriberNotificationTiming";
7
+ import logger from "../../../../Server/Utils/Logger";
8
+ import { JSONObject } from "../../../../Types/JSON";
9
+ import {
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ jest,
15
+ test,
16
+ } from "@jest/globals";
17
+ import type { SpyInstance } from "jest-mock";
18
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
19
+
20
+ /*
21
+ * The subscriber jobs run every minute on the shared Worker queue and await
22
+ * their sends, so their runs overlap; without a bound, slow sends during an
23
+ * outage could hold most of the Worker consumer's slots and starve on-call
24
+ * escalation and every other cron. A run takes one of a few permits per job
25
+ * first (SubscriberNotificationRunLimit), and returns at once when they are
26
+ * all taken.
27
+ */
28
+
29
+ const JOB_NAME: string = "IncidentPublicNote:SendNotificationToSubscribers";
30
+
31
+ // The nine subscriber jobs, and the Worker consumer's default concurrency.
32
+ const SUBSCRIBER_JOB_COUNT: number = 9;
33
+ const DEFAULT_WORKER_CONCURRENCY: number = 100;
34
+
35
+ let acquire: SpyInstance<typeof Semaphore.acquirePermit>;
36
+ let release: SpyInstance<typeof Semaphore.releasePermit>;
37
+ let permit: SemaphorePermit;
38
+
39
+ beforeEach(() => {
40
+ permit = { identifier: "permit-1" } as unknown as SemaphorePermit;
41
+
42
+ acquire = jest
43
+ .spyOn(Semaphore, "acquirePermit")
44
+ .mockResolvedValue(permit as never);
45
+ release = jest
46
+ .spyOn(Semaphore, "releasePermit")
47
+ .mockResolvedValue(undefined as never);
48
+
49
+ jest.spyOn(logger, "warn").mockImplementation((() => {}) as never);
50
+ jest.spyOn(logger, "debug").mockImplementation((() => {}) as never);
51
+ });
52
+
53
+ afterEach(() => {
54
+ jest.restoreAllMocks();
55
+ });
56
+
57
+ describe("SubscriberNotificationRunLimit", () => {
58
+ test("keeps the nine jobs well inside the Worker consumer's slots", () => {
59
+ expect(
60
+ SubscriberNotificationRunLimit.MAX_CONCURRENT_RUNS_PER_JOB *
61
+ SUBSCRIBER_JOB_COUNT,
62
+ ).toBeLessThanOrEqual(DEFAULT_WORKER_CONCURRENCY / 2);
63
+ expect(
64
+ SubscriberNotificationRunLimit.MAX_CONCURRENT_RUNS_PER_JOB,
65
+ ).toBeGreaterThanOrEqual(2);
66
+ });
67
+
68
+ test("a dead worker's permit lapses long before the job timeout", () => {
69
+ expect(SubscriberNotificationRunLimit.PERMIT_TTL_IN_MS).toBeLessThan(
70
+ SubscriberNotificationTiming.JOB_TIMEOUT_IN_MS / 5,
71
+ );
72
+ });
73
+
74
+ test("runs while holding one of the job's permits, and gives it back after", async () => {
75
+ const order: Array<string> = [];
76
+ acquire.mockImplementation((async () => {
77
+ order.push("acquire");
78
+ return permit;
79
+ }) as never);
80
+ release.mockImplementation((async () => {
81
+ order.push("release");
82
+ }) as never);
83
+
84
+ await SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {
85
+ order.push("run");
86
+ })();
87
+
88
+ expect(order).toEqual(["acquire", "run", "release"]);
89
+ expect(release).toHaveBeenCalledWith(permit);
90
+
91
+ const options: JSONObject = acquire.mock.calls[0]![0] as JSONObject;
92
+ expect(options).toEqual(
93
+ expect.objectContaining({
94
+ key: JOB_NAME,
95
+ namespace: SubscriberNotificationRunLimit.NAMESPACE,
96
+ limit: SubscriberNotificationRunLimit.MAX_CONCURRENT_RUNS_PER_JOB,
97
+ lockTimeout: SubscriberNotificationRunLimit.PERMIT_TTL_IN_MS,
98
+ acquireAttemptsLimit: 1,
99
+ }),
100
+ );
101
+ });
102
+
103
+ test("each job has its own permits", async () => {
104
+ await SubscriberNotificationRunLimit.limit("Job:A", async () => {})();
105
+ await SubscriberNotificationRunLimit.limit("Job:B", async () => {})();
106
+
107
+ expect(
108
+ acquire.mock.calls.map((call: Array<unknown>): unknown => {
109
+ return (call[0] as JSONObject)["key"];
110
+ }),
111
+ ).toEqual(["Job:A", "Job:B"]);
112
+ });
113
+
114
+ test("when every permit is taken, the run does not run: its notifications wait for the next", async () => {
115
+ acquire.mockRejectedValue(
116
+ new SemaphoreLockTimeoutError("Acquire semaphore timeout") as never,
117
+ );
118
+ const run: MockFunction = getJestMockFunction();
119
+
120
+ await SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {
121
+ run();
122
+ })();
123
+
124
+ expect(run).not.toHaveBeenCalled();
125
+ expect(release).not.toHaveBeenCalled();
126
+ });
127
+
128
+ test("gives the permit back when the run throws, and lets the error through", async () => {
129
+ await expect(
130
+ SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {
131
+ throw new Error("database hiccup");
132
+ })(),
133
+ ).rejects.toThrow("database hiccup");
134
+
135
+ expect(release).toHaveBeenCalledWith(permit);
136
+ });
137
+
138
+ test("runs without a permit when Redis cannot be reached for one", async () => {
139
+ acquire.mockRejectedValue(
140
+ new Error("Redis client is not connected") as never,
141
+ );
142
+ const run: MockFunction = getJestMockFunction();
143
+
144
+ await SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {
145
+ run();
146
+ })();
147
+
148
+ expect(run).toHaveBeenCalledTimes(1);
149
+ expect(release).not.toHaveBeenCalled();
150
+ expect(logger.warn).toHaveBeenCalled();
151
+ });
152
+
153
+ test("a permit that cannot be given back does not fail the run: it lapses", async () => {
154
+ release.mockRejectedValue(new Error("connection reset") as never);
155
+
156
+ await expect(
157
+ SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {})(),
158
+ ).resolves.toBeUndefined();
159
+ });
160
+
161
+ test("a permit lost while sending is logged, not thrown from a timer", async () => {
162
+ await SubscriberNotificationRunLimit.limit(JOB_NAME, async () => {})();
163
+
164
+ const onLockLost: (err: Error) => void = (
165
+ acquire.mock.calls[0]![0] as { onLockLost: (err: Error) => void }
166
+ ).onLockLost;
167
+
168
+ expect(() => {
169
+ onLockLost(new Error("Lost semaphore for key"));
170
+ }).not.toThrow();
171
+ expect(logger.warn).toHaveBeenCalledWith(
172
+ expect.stringContaining("lost its run permit"),
173
+ );
174
+ });
175
+ });
176
+
177
+ /*
178
+ * The run permits are shared by every project on the server, and a run sends
179
+ * one notification at a time. So one project's notifications also take one
180
+ * of a few per-project slots each: a tenant whose sends are slow - its own
181
+ * mail server hangs, or it posts a burst of notes on a large page - holds at
182
+ * most that many runs of a job, and the rest keep sending everyone else's.
183
+ */
184
+ describe("SubscriberNotificationRunLimit.takeProjectSlot", () => {
185
+ const PROJECT_ID: string = "10000000-0000-4000-8000-00000000000A";
186
+
187
+ test("a project gets fewer slots than the job has runs", () => {
188
+ expect(
189
+ SubscriberNotificationRunLimit.MAX_CONCURRENT_SENDS_PER_PROJECT,
190
+ ).toBeGreaterThanOrEqual(1);
191
+ expect(
192
+ SubscriberNotificationRunLimit.MAX_CONCURRENT_SENDS_PER_PROJECT,
193
+ ).toBeLessThan(SubscriberNotificationRunLimit.MAX_CONCURRENT_RUNS_PER_JOB);
194
+ });
195
+
196
+ test("takes one of the project's slots for this job, and gives it back", async () => {
197
+ const slot: Awaited<
198
+ ReturnType<typeof SubscriberNotificationRunLimit.takeProjectSlot>
199
+ > = await SubscriberNotificationRunLimit.takeProjectSlot({
200
+ jobName: JOB_NAME,
201
+ projectId: PROJECT_ID,
202
+ });
203
+
204
+ expect(slot).not.toBeNull();
205
+ expect(acquire.mock.calls[0]![0]).toEqual(
206
+ expect.objectContaining({
207
+ // Per job and project; the project id as it is stored.
208
+ key: `${JOB_NAME}:${PROJECT_ID.toLowerCase()}`,
209
+ namespace: SubscriberNotificationRunLimit.PROJECT_NAMESPACE,
210
+ limit: SubscriberNotificationRunLimit.MAX_CONCURRENT_SENDS_PER_PROJECT,
211
+ lockTimeout: SubscriberNotificationRunLimit.PERMIT_TTL_IN_MS,
212
+ acquireAttemptsLimit: 1,
213
+ }),
214
+ );
215
+
216
+ await slot!.release();
217
+ expect(release).toHaveBeenCalledWith(permit);
218
+ });
219
+
220
+ test("a project that already has its share being sent gets none: its notification waits", async () => {
221
+ acquire.mockRejectedValue(
222
+ new SemaphoreLockTimeoutError("Acquire semaphore timeout") as never,
223
+ );
224
+
225
+ await expect(
226
+ SubscriberNotificationRunLimit.takeProjectSlot({
227
+ jobName: JOB_NAME,
228
+ projectId: PROJECT_ID,
229
+ }),
230
+ ).resolves.toBeNull();
231
+ });
232
+
233
+ test("sends without a slot when Redis cannot be reached, as a run does without its permit", async () => {
234
+ acquire.mockRejectedValue(
235
+ new Error("Redis client is not connected") as never,
236
+ );
237
+
238
+ const slot: Awaited<
239
+ ReturnType<typeof SubscriberNotificationRunLimit.takeProjectSlot>
240
+ > = await SubscriberNotificationRunLimit.takeProjectSlot({
241
+ jobName: JOB_NAME,
242
+ projectId: PROJECT_ID,
243
+ });
244
+
245
+ expect(slot).not.toBeNull();
246
+ await slot!.release();
247
+ expect(release).not.toHaveBeenCalled();
248
+ expect(logger.warn).toHaveBeenCalled();
249
+ });
250
+
251
+ test("a notification without a project is not bounded by one", async () => {
252
+ const slot: Awaited<
253
+ ReturnType<typeof SubscriberNotificationRunLimit.takeProjectSlot>
254
+ > = await SubscriberNotificationRunLimit.takeProjectSlot({
255
+ jobName: JOB_NAME,
256
+ projectId: undefined,
257
+ });
258
+
259
+ expect(slot).not.toBeNull();
260
+ expect(acquire).not.toHaveBeenCalled();
261
+ });
262
+
263
+ test("a slot that cannot be given back does not throw: it lapses", async () => {
264
+ release.mockRejectedValue(new Error("connection reset") as never);
265
+
266
+ const slot: Awaited<
267
+ ReturnType<typeof SubscriberNotificationRunLimit.takeProjectSlot>
268
+ > = await SubscriberNotificationRunLimit.takeProjectSlot({
269
+ jobName: JOB_NAME,
270
+ projectId: PROJECT_ID,
271
+ });
272
+
273
+ await expect(slot!.release()).resolves.toBeUndefined();
274
+ });
275
+ });
@@ -0,0 +1,131 @@
1
+ import SubscriberNotificationTiming, {
2
+ SubscriberNotificationRunClock,
3
+ SubscriberNotificationSendWindow,
4
+ } from "../../../../Server/Utils/StatusPage/SubscriberNotificationTiming";
5
+ import LIMIT_MAX from "../../../../Types/Database/LimitMax";
6
+ import OneUptimeDate from "../../../../Types/Date";
7
+ import { describe, expect, test } from "@jest/globals";
8
+
9
+ /*
10
+ * The numbers that bound a subscriber send, and the arithmetic that ties
11
+ * them together (see SubscriberNotificationTiming). These tests pin the
12
+ * relations, not just the values: a change to one number that breaks
13
+ * another's reason fails here rather than in production, as a send that
14
+ * outlives its job or a live send swept as interrupted.
15
+ */
16
+
17
+ const MINUTE: number = OneUptimeDate.convertMinutesToMilliseconds(1);
18
+
19
+ describe("SubscriberNotificationTiming", () => {
20
+ test("a bounded number of messages in flight", () => {
21
+ expect(SubscriberNotificationTiming.SEND_CONCURRENCY).toBe(20);
22
+ });
23
+
24
+ test("reads a status page's subscribers LIMIT_MAX at a time: the cap one read used to stop at", () => {
25
+ expect(SubscriberNotificationTiming.SUBSCRIBERS_PER_READ).toBe(LIMIT_MAX);
26
+ });
27
+
28
+ test("waits for a message longer than any sender's own retries", () => {
29
+ // SMTP: three attempts with a 60 second connect timeout, 1 + 2 seconds of backoff and jitter.
30
+ const smtpRetryLadder: number = 3 * 60_000 + 1_000 + 2_000 + 2 * 1_000;
31
+ // Slack and webhooks: four attempts, backing off 2, 4 and 8 seconds.
32
+ const webhookRetryLadder: number = 2_000 + 4_000 + 8_000;
33
+
34
+ expect(SubscriberNotificationTiming.SEND_TIMEOUT_IN_MS).toBeGreaterThan(
35
+ smtpRetryLadder,
36
+ );
37
+ expect(SubscriberNotificationTiming.SEND_TIMEOUT_IN_MS).toBeGreaterThan(
38
+ webhookRetryLadder,
39
+ );
40
+ });
41
+
42
+ test("the send window fits one full read of a page at a few seconds a message", () => {
43
+ const rounds: number =
44
+ SubscriberNotificationTiming.SUBSCRIBERS_PER_READ /
45
+ SubscriberNotificationTiming.SEND_CONCURRENCY;
46
+
47
+ // 10,000 subscribers at 20 in flight: 500 rounds of 2.4 seconds.
48
+ expect(rounds).toBe(500);
49
+ expect(
50
+ SubscriberNotificationTiming.SEND_WINDOW_IN_MS / rounds,
51
+ ).toBeGreaterThanOrEqual(2_000);
52
+ });
53
+
54
+ test("a notification at most takes its window, the last answers it waits for, and its settle", () => {
55
+ expect(SubscriberNotificationTiming.NOTIFICATION_MAX_IN_MS).toBe(
56
+ SubscriberNotificationTiming.SEND_WINDOW_IN_MS +
57
+ SubscriberNotificationTiming.SEND_TIMEOUT_IN_MS +
58
+ SubscriberNotificationTiming.SETTLE_ALLOWANCE_IN_MS,
59
+ );
60
+ expect(SubscriberNotificationTiming.NOTIFICATION_MAX_IN_MS).toBe(
61
+ 25 * MINUTE,
62
+ );
63
+ });
64
+
65
+ test("a run claims only what can finish within the job timeout, and there is time to claim", () => {
66
+ expect(SubscriberNotificationTiming.JOB_TIMEOUT_IN_MS).toBe(30 * MINUTE);
67
+ expect(
68
+ SubscriberNotificationTiming.LATEST_CLAIM_IN_MS +
69
+ SubscriberNotificationTiming.NOTIFICATION_MAX_IN_MS,
70
+ ).toBe(SubscriberNotificationTiming.JOB_TIMEOUT_IN_MS);
71
+ expect(SubscriberNotificationTiming.LATEST_CLAIM_IN_MS).toBeGreaterThan(
72
+ MINUTE,
73
+ );
74
+ });
75
+
76
+ test("the sweeper waits well past anything a live send can take", () => {
77
+ expect(SubscriberNotificationTiming.STUCK_AFTER_IN_MS).toBe(40 * MINUTE);
78
+ expect(
79
+ SubscriberNotificationTiming.STUCK_AFTER_IN_MS -
80
+ SubscriberNotificationTiming.JOB_TIMEOUT_IN_MS,
81
+ ).toBeGreaterThanOrEqual(5 * MINUTE);
82
+ });
83
+
84
+ describe("the run clock", () => {
85
+ test("claims until the latest claim, and not after", () => {
86
+ const clock: SubscriberNotificationRunClock =
87
+ SubscriberNotificationTiming.startRun(1_000_000);
88
+
89
+ expect(clock.canClaimAnotherNotification(1_000_000)).toBe(true);
90
+ expect(
91
+ clock.canClaimAnotherNotification(
92
+ 1_000_000 + SubscriberNotificationTiming.LATEST_CLAIM_IN_MS,
93
+ ),
94
+ ).toBe(true);
95
+ expect(
96
+ clock.canClaimAnotherNotification(
97
+ 1_000_000 + SubscriberNotificationTiming.LATEST_CLAIM_IN_MS + 1,
98
+ ),
99
+ ).toBe(false);
100
+ });
101
+
102
+ test("reads the time now by default", () => {
103
+ expect(
104
+ SubscriberNotificationTiming.startRun().canClaimAnotherNotification(),
105
+ ).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe("the send window", () => {
110
+ test("is open for SEND_WINDOW_IN_MS after the claim", () => {
111
+ const window: SubscriberNotificationSendWindow =
112
+ SubscriberNotificationTiming.startSendWindow(5_000);
113
+
114
+ expect(window.closesAt).toBe(
115
+ 5_000 + SubscriberNotificationTiming.SEND_WINDOW_IN_MS,
116
+ );
117
+ expect(window.isOpen(5_000)).toBe(true);
118
+ expect(window.isOpen(window.closesAt - 1)).toBe(true);
119
+ expect(window.isOpen(window.closesAt)).toBe(false);
120
+ });
121
+
122
+ test("reads the time now by default", () => {
123
+ expect(SubscriberNotificationTiming.startSendWindow().isOpen()).toBe(
124
+ true,
125
+ );
126
+ expect(
127
+ new SubscriberNotificationSendWindow(Date.now() - 1).isOpen(),
128
+ ).toBe(false);
129
+ });
130
+ });
131
+ });
@@ -135,6 +135,104 @@ describe("StatusPageResourceUtil", () => {
135
135
  });
136
136
  });
137
137
 
138
+ /*
139
+ * The HTML list goes into the raw-HTML slot of the default subscriber
140
+ * emails, and into custom email templates as HTML, so every name in it is
141
+ * escaped. Project members name resources and groups; a name holding a
142
+ * link or a script must read as those characters, not become markup.
143
+ */
144
+ describe("getResourcesGroupedByGroupName escapes names", () => {
145
+ const LINK: string = '<a href="https://evil.example/login">Checkout</a>';
146
+ const LINK_ESCAPED: string =
147
+ "&lt;a href=&quot;https://evil.example/login&quot;&gt;Checkout&lt;/a&gt;";
148
+ const SCRIPT: string = "<script>alert('x')</script>";
149
+ const SCRIPT_ESCAPED: string =
150
+ "&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;";
151
+
152
+ it("escapes an ungrouped display name", () => {
153
+ const result: string =
154
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([
155
+ createResource(LINK),
156
+ createResource("Payments & Billing"),
157
+ ]);
158
+
159
+ expect(result).toBe(`${LINK_ESCAPED}, Payments &amp; Billing`);
160
+ expect(result).not.toContain("<a");
161
+ });
162
+
163
+ it("escapes a group name and the names in it", () => {
164
+ const result: string =
165
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([
166
+ createResourceWithGroup(SCRIPT, `EU "West"`),
167
+ createResourceWithGroup("API", `EU "West"`),
168
+ createResourceWithGroup("Web", SCRIPT),
169
+ ]);
170
+
171
+ expect(result).toBe(
172
+ `EU &quot;West&quot;: ${SCRIPT_ESCAPED}, API<br/>${SCRIPT_ESCAPED}: Web`,
173
+ );
174
+ expect(result).not.toContain("<script");
175
+ });
176
+
177
+ it("escapes an ungrouped name listed after the groups", () => {
178
+ const result: string =
179
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([
180
+ createResourceWithGroup("API", "EU"),
181
+ createResource(`O'Brien's <b>service</b>`),
182
+ ]);
183
+
184
+ expect(result).toBe(
185
+ "EU: API<br/>O&#39;Brien&#39;s &lt;b&gt;service&lt;/b&gt;",
186
+ );
187
+ });
188
+
189
+ it("keeps its own <br/> between groups as markup", () => {
190
+ const result: string =
191
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([
192
+ createResourceWithGroup("A", "G1"),
193
+ createResourceWithGroup("B", "G2"),
194
+ ]);
195
+
196
+ expect(result).toBe("G1: A<br/>G2: B");
197
+ });
198
+
199
+ it("merges two groups only when their names are the same as written", () => {
200
+ const result: string =
201
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([
202
+ createResourceWithGroup("A", "R&D"),
203
+ createResourceWithGroup("B", "R&amp;D"),
204
+ createResourceWithGroup("C", "R&D"),
205
+ ]);
206
+
207
+ expect(result).toBe("R&amp;D: A, C<br/>R&amp;amp;D: B");
208
+ });
209
+
210
+ it("escapes the default value too", () => {
211
+ expect(
212
+ StatusPageResourceUtil.getResourcesGroupedByGroupName([], "<none>"),
213
+ ).toBe("&lt;none&gt;");
214
+ });
215
+
216
+ it("leaves the plain-text list as written, for channels that do not render HTML", () => {
217
+ const resources: Array<StatusPageResource> = [
218
+ createResourceWithGroup(SCRIPT, `EU "West"`),
219
+ createResource(LINK),
220
+ ];
221
+
222
+ expect(
223
+ StatusPageResourceUtil.getResourcesGroupedByGroupNameAsPlainText(
224
+ resources,
225
+ ),
226
+ ).toBe(`EU "West": ${SCRIPT}; ${LINK}`);
227
+ expect(
228
+ StatusPageResourceUtil.getResourcesGroupedByGroupNameAsPlainText(
229
+ [],
230
+ "<none>",
231
+ ),
232
+ ).toBe("<none>");
233
+ });
234
+ });
235
+
138
236
  describe("getResourcesGroupedByGroupNameAsPlainText", () => {
139
237
  it("should separate groups with semicolons instead of <br/>", () => {
140
238
  const resources: Array<StatusPageResource> = [
@@ -81,6 +81,23 @@ describe("VMRunner — private network policy plumbing", () => {
81
81
  );
82
82
  });
83
83
 
84
+ test("the caller decides how much a DNS-decided refusal says", () => {
85
+ /*
86
+ * The probe passes false; the workflow caller passes nothing and gets the
87
+ * deployment default. Dropping it here would hand every script the
88
+ * detailed refusal on a shared probe.
89
+ */
90
+ expect(runnerSource).toMatch(
91
+ /includeResolutionDetailInError:\s*\n?\s*options\.includeResolutionDetailInError/,
92
+ );
93
+ expect(runnerSource).toMatch(
94
+ /includeResolutionDetailInError\?:\s*boolean \| undefined;/,
95
+ );
96
+ expect(apiSource).toMatch(
97
+ /includeResolutionDetailInError\?:\s*boolean \| undefined;/,
98
+ );
99
+ });
100
+
84
101
  test("there is exactly one call to the guard", () => {
85
102
  /*
86
103
  * A second, unguarded request path inside the runner would be invisible to
@@ -16,6 +16,7 @@ import http from "http";
16
16
  import https from "https";
17
17
  import { AddressInfo } from "net";
18
18
  import { gzipSync } from "zlib";
19
+ import { startEachTestOnSelfHostedEgressPolicy } from "../EgressPolicyEnvironment";
19
20
 
20
21
  /*
21
22
  * Common's jsdom resolver selects Axios's browser bundle. VMRunner is a
@@ -46,6 +47,7 @@ jest.setTimeout(60000);
46
47
  interface SandboxPrivateNetworkOptions {
47
48
  allowPrivateNetworkRequests?: boolean | undefined;
48
49
  privateNetworkAccessIsAllowed?: boolean | undefined;
50
+ includeResolutionDetailInError?: boolean | undefined;
49
51
  }
50
52
 
51
53
  interface RunningLoopbackServer {
@@ -675,6 +677,79 @@ describe("VMRunner sandbox axios bridge — private network policy", () => {
675
677
  );
676
678
  });
677
679
 
680
+ /*
681
+ * A script sees the guard's refusal verbatim. If "no such name" and "that name
682
+ * is internal" read differently, a script is a DNS oracle for the network the
683
+ * sandbox runs in: the API server's on SaaS, a shared probe's for the custom
684
+ * code monitor.
685
+ */
686
+ describe("VMRunner sandbox axios bridge — DNS refusal detail", () => {
687
+ startEachTestOnSelfHostedEgressPolicy();
688
+
689
+ const internalNameRequest: string = `
690
+ try {
691
+ await axios.get('http://redis:6379/');
692
+ return 'request reached transport';
693
+ } catch (error) {
694
+ return 'request failed: ' + error.message;
695
+ }
696
+ `;
697
+
698
+ async function refusalsForMissingAndInternalName(
699
+ policy: SandboxPrivateNetworkOptions,
700
+ ): Promise<{ missing: string; internal: string }> {
701
+ const lookupSpy: ValidationLookupSpy = jest.spyOn(
702
+ dns.promises,
703
+ "lookup",
704
+ ) as unknown as ValidationLookupSpy;
705
+
706
+ lookupSpy.mockRejectedValue(new Error("getaddrinfo ENOTFOUND redis"));
707
+ const missing: string = await errorFromSandbox(internalNameRequest, policy);
708
+
709
+ lookupSpy.mockResolvedValue([{ address: "10.96.0.9", family: 4 }]);
710
+ const internal: string = await errorFromSandbox(
711
+ internalNameRequest,
712
+ policy,
713
+ );
714
+
715
+ return { missing, internal };
716
+ }
717
+
718
+ test("the probe's opt-out makes a missing and an internal name indistinguishable", async () => {
719
+ const { missing, internal } = await refusalsForMissingAndInternalName({
720
+ includeResolutionDetailInError: false,
721
+ });
722
+
723
+ expect(missing).toContain("Request URL could not be reached.");
724
+ expect(internal).toBe(missing);
725
+ expect(internal).not.toContain("10.96.0.9");
726
+ });
727
+
728
+ test("on SaaS a workflow script gets the same single refusal by default", async () => {
729
+ process.env["BILLING_ENABLED"] = "true";
730
+
731
+ const { missing, internal } = await refusalsForMissingAndInternalName({
732
+ allowPrivateNetworkRequests: true,
733
+ });
734
+
735
+ expect(missing).toContain("Request URL could not be reached.");
736
+ expect(internal).toBe(missing);
737
+ });
738
+
739
+ test("a self-hosted workflow script still gets the detailed refusal", async () => {
740
+ const { missing, internal } = await refusalsForMissingAndInternalName({
741
+ allowPrivateNetworkRequests: true,
742
+ });
743
+
744
+ expect(missing).toContain(
745
+ "Request URL hostname could not be resolved via DNS.",
746
+ );
747
+ expect(internal).toContain(
748
+ "Request URL resolves to a private network address and is not allowed.",
749
+ );
750
+ });
751
+ });
752
+
678
753
  describe("VMRunner sandbox axios bridge — bounded host I/O", () => {
679
754
  const allowLoopbackForWorkflow: SandboxPrivateNetworkOptions = {
680
755
  allowPrivateNetworkRequests: true,