@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,238 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * The limit in front of "Preview notification" (POST /preview on the
12
+ * subscriber notification preview router). A preview sends nothing, but it
13
+ * renders an email for every status page the caller can read, and any
14
+ * audience role may call it. What matters is that it is per user, that it
15
+ * really counts, that a caller who keeps trying stays refused for the
16
+ * window, and that it FAILS OPEN when Redis is unreachable: a preview is not
17
+ * worth taking away from everyone during a Redis outage.
18
+ */
19
+
20
+ jest.mock("../../../Server/Infrastructure/Redis", () => {
21
+ return {
22
+ __esModule: true,
23
+ default: {
24
+ getClient: jest.fn(),
25
+ isConnected: jest.fn(),
26
+ },
27
+ };
28
+ });
29
+
30
+ jest.mock("../../../Server/Utils/Logger", () => {
31
+ return {
32
+ __esModule: true,
33
+ default: {
34
+ debug: jest.fn(),
35
+ info: jest.fn(),
36
+ warn: jest.fn(),
37
+ error: jest.fn(),
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock("../../../Server/Utils/Response", () => {
43
+ return {
44
+ __esModule: true,
45
+ default: {
46
+ sendErrorResponse: jest.fn(),
47
+ },
48
+ };
49
+ });
50
+
51
+ import Redis from "../../../Server/Infrastructure/Redis";
52
+ import Response from "../../../Server/Utils/Response";
53
+ import SubscriberNotificationPreviewRateLimit, {
54
+ SubscriberNotificationPreviewRateLimitConfig,
55
+ SubscriberNotificationPreviewRateLimitDecision,
56
+ SubscriberNotificationPreviewRateLimitOutcome,
57
+ } from "../../../Server/Middleware/SubscriberNotificationPreviewRateLimit";
58
+ import TooManyRequestsException from "../../../Types/Exception/TooManyRequestsException";
59
+ import ObjectID from "../../../Types/ObjectID";
60
+ import {
61
+ ExpressRequest,
62
+ ExpressResponse,
63
+ NextFunction,
64
+ } from "../../../Server/Utils/Express";
65
+
66
+ type MockedFn = ReturnType<typeof jest.fn>;
67
+
68
+ const USER_A: string = "1a2b3c4d-3333-4333-8333-333333333333";
69
+ const USER_B: string = "1a2b3c4d-4444-4444-8444-444444444444";
70
+
71
+ // A Redis counter store: INCR really increments, EXPIRE records the TTL.
72
+ class FakeRedisClient {
73
+ public counters: Map<string, number> = new Map();
74
+ public expires: Map<string, number> = new Map();
75
+ public failIncr: Error | null = null;
76
+
77
+ public async incr(key: string): Promise<number> {
78
+ if (this.failIncr) {
79
+ throw this.failIncr;
80
+ }
81
+
82
+ const next: number = (this.counters.get(key) || 0) + 1;
83
+ this.counters.set(key, next);
84
+ return next;
85
+ }
86
+
87
+ public async expire(key: string, ttlSeconds: number): Promise<number> {
88
+ this.expires.set(key, ttlSeconds);
89
+ return 1;
90
+ }
91
+ }
92
+
93
+ let client: FakeRedisClient;
94
+
95
+ function request(userId: string | undefined): ExpressRequest {
96
+ return {
97
+ userAuthorization: userId ? { userId: new ObjectID(userId) } : undefined,
98
+ headers: {},
99
+ } as unknown as ExpressRequest;
100
+ }
101
+
102
+ interface MiddlewareCall {
103
+ nextCalled: boolean;
104
+ error: unknown;
105
+ retryAfter: string | undefined;
106
+ }
107
+
108
+ async function callMiddleware(
109
+ userId: string | undefined,
110
+ ): Promise<MiddlewareCall> {
111
+ (Response.sendErrorResponse as unknown as MockedFn).mockClear();
112
+
113
+ const headers: Record<string, string> = {};
114
+ const res: ExpressResponse = {
115
+ setHeader: (name: string, value: string): void => {
116
+ headers[name] = value;
117
+ },
118
+ } as unknown as ExpressResponse;
119
+ const next: MockedFn = jest.fn();
120
+
121
+ await SubscriberNotificationPreviewRateLimit.getMiddleware()(
122
+ request(userId),
123
+ res,
124
+ next as unknown as NextFunction,
125
+ );
126
+
127
+ const errorCall: Array<unknown> | undefined = (
128
+ Response.sendErrorResponse as unknown as MockedFn
129
+ ).mock.calls[0];
130
+
131
+ return {
132
+ nextCalled: next.mock.calls.length > 0,
133
+ error: errorCall ? errorCall[2] : undefined,
134
+ retryAfter: headers["Retry-After"],
135
+ };
136
+ }
137
+
138
+ beforeEach(() => {
139
+ // Mid-window, so no test straddles two windows.
140
+ jest
141
+ .spyOn(Date, "now")
142
+ .mockReturnValue(Date.UTC(2026, 8, 27, 10, 7, 30) as never);
143
+
144
+ client = new FakeRedisClient();
145
+ (Redis.getClient as unknown as MockedFn).mockReturnValue(client);
146
+ (Redis.isConnected as unknown as MockedFn).mockReturnValue(true);
147
+ });
148
+
149
+ afterEach(() => {
150
+ jest.restoreAllMocks();
151
+ jest.clearAllMocks();
152
+ });
153
+
154
+ describe("SubscriberNotificationPreviewRateLimit", () => {
155
+ const config: SubscriberNotificationPreviewRateLimitConfig =
156
+ SubscriberNotificationPreviewRateLimit.getConfig();
157
+
158
+ it("allows sixty previews every ten minutes per user", () => {
159
+ expect(config).toEqual({ windowSeconds: 10 * 60, perUserLimit: 60 });
160
+ });
161
+
162
+ it("lets a user preview up to the limit, then refuses with 429 and Retry-After", async () => {
163
+ for (let i: number = 0; i < config.perUserLimit; i++) {
164
+ const call: MiddlewareCall = await callMiddleware(USER_A);
165
+ expect(call.nextCalled).toBe(true);
166
+ expect(call.error).toBeUndefined();
167
+ }
168
+
169
+ const refused: MiddlewareCall = await callMiddleware(USER_A);
170
+
171
+ expect(refused.nextCalled).toBe(false);
172
+ expect(refused.error).toBeInstanceOf(TooManyRequestsException);
173
+ expect(Number(refused.retryAfter)).toBeGreaterThan(0);
174
+ expect(Number(refused.retryAfter)).toBeLessThanOrEqual(
175
+ config.windowSeconds,
176
+ );
177
+ });
178
+
179
+ it("counts per user: one user's previews do not use up another's", async () => {
180
+ for (let i: number = 0; i <= config.perUserLimit; i++) {
181
+ await callMiddleware(USER_A);
182
+ }
183
+
184
+ expect((await callMiddleware(USER_A)).nextCalled).toBe(false);
185
+ expect((await callMiddleware(USER_B)).nextCalled).toBe(true);
186
+ });
187
+
188
+ it("keeps counting refused requests, so hammering does not earn a fresh allowance", async () => {
189
+ for (let i: number = 0; i < config.perUserLimit + 5; i++) {
190
+ await callMiddleware(USER_A);
191
+ }
192
+
193
+ const keys: Array<string> = Array.from(client.counters.keys());
194
+ expect(keys).toHaveLength(1);
195
+ expect(client.counters.get(keys[0]!)).toBe(config.perUserLimit + 5);
196
+ });
197
+
198
+ it("does not share its counter with the test email's", async () => {
199
+ await callMiddleware(USER_A);
200
+
201
+ const keys: Array<string> = Array.from(client.counters.keys());
202
+ expect(keys[0]).toContain("subnotifpreview:");
203
+ expect(keys[0]).not.toContain("subnotiftest:");
204
+ });
205
+
206
+ it("sets the expiry once, when the counter is created, outliving its window", async () => {
207
+ await callMiddleware(USER_A);
208
+ await callMiddleware(USER_A);
209
+
210
+ const keys: Array<string> = Array.from(client.expires.keys());
211
+ expect(keys).toHaveLength(1);
212
+ expect(keys[0]).toContain(USER_A);
213
+ expect(client.expires.get(keys[0]!)).toBe(config.windowSeconds * 2);
214
+ });
215
+
216
+ it("fails open when Redis is not connected: the preview is served", async () => {
217
+ (Redis.isConnected as unknown as MockedFn).mockReturnValue(false);
218
+
219
+ const call: MiddlewareCall = await callMiddleware(USER_A);
220
+
221
+ expect(call.nextCalled).toBe(true);
222
+ expect(call.error).toBeUndefined();
223
+ });
224
+
225
+ it("fails open when the counter errors", async () => {
226
+ client.failIncr = new Error("READONLY");
227
+
228
+ const decision: SubscriberNotificationPreviewRateLimitDecision =
229
+ await SubscriberNotificationPreviewRateLimit.consume({
230
+ userKey: USER_A,
231
+ });
232
+
233
+ expect(decision.outcome).toBe(
234
+ SubscriberNotificationPreviewRateLimitOutcome.CounterUnavailable,
235
+ );
236
+ expect((await callMiddleware(USER_A)).nextCalled).toBe(true);
237
+ });
238
+ });
@@ -0,0 +1,245 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * The limit in front of "Send test to me" (POST /send-test on the subscriber
12
+ * notification preview router). Every allowed request is a real email, and
13
+ * this counter is the only thing bounding how many, so what matters is that
14
+ * it is per user (one inbox), that it really counts, that a caller who keeps
15
+ * trying stays refused for the window, and that it FAILS CLOSED when Redis is
16
+ * unreachable.
17
+ */
18
+
19
+ jest.mock("../../../Server/Infrastructure/Redis", () => {
20
+ return {
21
+ __esModule: true,
22
+ default: {
23
+ getClient: jest.fn(),
24
+ isConnected: jest.fn(),
25
+ },
26
+ };
27
+ });
28
+
29
+ jest.mock("../../../Server/Utils/Logger", () => {
30
+ return {
31
+ __esModule: true,
32
+ default: {
33
+ debug: jest.fn(),
34
+ info: jest.fn(),
35
+ warn: jest.fn(),
36
+ error: jest.fn(),
37
+ },
38
+ };
39
+ });
40
+
41
+ jest.mock("../../../Server/Utils/Response", () => {
42
+ return {
43
+ __esModule: true,
44
+ default: {
45
+ sendErrorResponse: jest.fn(),
46
+ },
47
+ };
48
+ });
49
+
50
+ import Redis from "../../../Server/Infrastructure/Redis";
51
+ import Response from "../../../Server/Utils/Response";
52
+ import SubscriberNotificationTestSendRateLimit, {
53
+ SubscriberNotificationTestSendRateLimitConfig,
54
+ SubscriberNotificationTestSendRateLimitDecision,
55
+ SubscriberNotificationTestSendRateLimitOutcome,
56
+ } from "../../../Server/Middleware/SubscriberNotificationTestSendRateLimit";
57
+ import ServiceUnavailableException from "../../../Types/Exception/ServiceUnavailableException";
58
+ import TooManyRequestsException from "../../../Types/Exception/TooManyRequestsException";
59
+ import ObjectID from "../../../Types/ObjectID";
60
+ import {
61
+ ExpressRequest,
62
+ ExpressResponse,
63
+ NextFunction,
64
+ } from "../../../Server/Utils/Express";
65
+
66
+ type MockedFn = ReturnType<typeof jest.fn>;
67
+
68
+ const USER_A: string = "1a2b3c4d-3333-4333-8333-333333333333";
69
+ const USER_B: string = "1a2b3c4d-4444-4444-8444-444444444444";
70
+
71
+ // A Redis counter store: INCR really increments, EXPIRE records the TTL.
72
+ class FakeRedisClient {
73
+ public counters: Map<string, number> = new Map();
74
+ public expires: Map<string, number> = new Map();
75
+ public failIncr: Error | null = null;
76
+
77
+ public async incr(key: string): Promise<number> {
78
+ if (this.failIncr) {
79
+ throw this.failIncr;
80
+ }
81
+
82
+ const next: number = (this.counters.get(key) || 0) + 1;
83
+ this.counters.set(key, next);
84
+ return next;
85
+ }
86
+
87
+ public async expire(key: string, ttlSeconds: number): Promise<number> {
88
+ this.expires.set(key, ttlSeconds);
89
+ return 1;
90
+ }
91
+ }
92
+
93
+ let client: FakeRedisClient;
94
+
95
+ function request(userId: string | undefined): ExpressRequest {
96
+ return {
97
+ userAuthorization: userId ? { userId: new ObjectID(userId) } : undefined,
98
+ headers: {},
99
+ } as unknown as ExpressRequest;
100
+ }
101
+
102
+ interface MiddlewareCall {
103
+ nextCalled: boolean;
104
+ error: unknown;
105
+ retryAfter: string | undefined;
106
+ }
107
+
108
+ async function callMiddleware(
109
+ userId: string | undefined,
110
+ ): Promise<MiddlewareCall> {
111
+ (Response.sendErrorResponse as unknown as MockedFn).mockClear();
112
+
113
+ const headers: Record<string, string> = {};
114
+ const res: ExpressResponse = {
115
+ setHeader: (name: string, value: string): void => {
116
+ headers[name] = value;
117
+ },
118
+ } as unknown as ExpressResponse;
119
+ const next: MockedFn = jest.fn();
120
+
121
+ await SubscriberNotificationTestSendRateLimit.getMiddleware()(
122
+ request(userId),
123
+ res,
124
+ next as unknown as NextFunction,
125
+ );
126
+
127
+ const errorCall: Array<unknown> | undefined = (
128
+ Response.sendErrorResponse as unknown as MockedFn
129
+ ).mock.calls[0];
130
+
131
+ return {
132
+ nextCalled: next.mock.calls.length > 0,
133
+ error: errorCall ? errorCall[2] : undefined,
134
+ retryAfter: headers["Retry-After"],
135
+ };
136
+ }
137
+
138
+ beforeEach(() => {
139
+ // Mid-window, so no test straddles two windows.
140
+ jest
141
+ .spyOn(Date, "now")
142
+ .mockReturnValue(Date.UTC(2026, 8, 27, 10, 7, 30) as never);
143
+
144
+ client = new FakeRedisClient();
145
+ (Redis.getClient as unknown as MockedFn).mockReturnValue(client);
146
+ (Redis.isConnected as unknown as MockedFn).mockReturnValue(true);
147
+ });
148
+
149
+ afterEach(() => {
150
+ jest.restoreAllMocks();
151
+ jest.clearAllMocks();
152
+ });
153
+
154
+ describe("SubscriberNotificationTestSendRateLimit", () => {
155
+ const config: SubscriberNotificationTestSendRateLimitConfig =
156
+ SubscriberNotificationTestSendRateLimit.getConfig();
157
+
158
+ it("allows ten test emails a quarter of an hour per user", () => {
159
+ expect(config).toEqual({ windowSeconds: 15 * 60, perUserLimit: 10 });
160
+ });
161
+
162
+ it("lets a user send up to the limit, then refuses with 429 and Retry-After", async () => {
163
+ for (let i: number = 0; i < config.perUserLimit; i++) {
164
+ const call: MiddlewareCall = await callMiddleware(USER_A);
165
+ expect(call.nextCalled).toBe(true);
166
+ expect(call.error).toBeUndefined();
167
+ }
168
+
169
+ const refused: MiddlewareCall = await callMiddleware(USER_A);
170
+
171
+ expect(refused.nextCalled).toBe(false);
172
+ expect(refused.error).toBeInstanceOf(TooManyRequestsException);
173
+ expect(Number(refused.retryAfter)).toBeGreaterThan(0);
174
+ expect(Number(refused.retryAfter)).toBeLessThanOrEqual(
175
+ config.windowSeconds,
176
+ );
177
+ });
178
+
179
+ it("counts per user: one user's tests do not use up another's", async () => {
180
+ for (let i: number = 0; i <= config.perUserLimit; i++) {
181
+ await callMiddleware(USER_A);
182
+ }
183
+
184
+ expect((await callMiddleware(USER_A)).nextCalled).toBe(false);
185
+ expect((await callMiddleware(USER_B)).nextCalled).toBe(true);
186
+ });
187
+
188
+ it("keeps counting refused requests, so hammering does not earn a fresh allowance", async () => {
189
+ for (let i: number = 0; i < config.perUserLimit + 5; i++) {
190
+ await callMiddleware(USER_A);
191
+ }
192
+
193
+ const keys: Array<string> = Array.from(client.counters.keys());
194
+ expect(keys).toHaveLength(1);
195
+ expect(client.counters.get(keys[0]!)).toBe(config.perUserLimit + 5);
196
+ });
197
+
198
+ it("sets the expiry once, when the counter is created, outliving its window", async () => {
199
+ await callMiddleware(USER_A);
200
+ await callMiddleware(USER_A);
201
+
202
+ const keys: Array<string> = Array.from(client.expires.keys());
203
+ expect(keys).toHaveLength(1);
204
+ expect(keys[0]).toContain(USER_A);
205
+ expect(client.expires.get(keys[0]!)).toBe(config.windowSeconds * 2);
206
+ });
207
+
208
+ it("fails closed when Redis is not connected: 503, and no email", async () => {
209
+ (Redis.isConnected as unknown as MockedFn).mockReturnValue(false);
210
+
211
+ const call: MiddlewareCall = await callMiddleware(USER_A);
212
+
213
+ expect(call.nextCalled).toBe(false);
214
+ expect(call.error).toBeInstanceOf(ServiceUnavailableException);
215
+ });
216
+
217
+ it("fails closed when the counter errors", async () => {
218
+ client.failIncr = new Error("READONLY");
219
+
220
+ const decision: SubscriberNotificationTestSendRateLimitDecision =
221
+ await SubscriberNotificationTestSendRateLimit.consume({
222
+ userKey: USER_A,
223
+ });
224
+
225
+ expect(decision.outcome).toBe(
226
+ SubscriberNotificationTestSendRateLimitOutcome.CounterUnavailable,
227
+ );
228
+ expect((await callMiddleware(USER_A)).error).toBeInstanceOf(
229
+ ServiceUnavailableException,
230
+ );
231
+ });
232
+
233
+ it("bills a request with no user to one shared bucket", async () => {
234
+ expect(
235
+ SubscriberNotificationTestSendRateLimit.resolveUserKey(
236
+ request(undefined),
237
+ ),
238
+ ).toBe("anonymous");
239
+ expect(
240
+ SubscriberNotificationTestSendRateLimit.resolveUserKey(
241
+ request(USER_A.toUpperCase()),
242
+ ),
243
+ ).toBe(USER_A);
244
+ });
245
+ });
@@ -0,0 +1,152 @@
1
+ import { AddStatusPageSubscriberUnsubscribeToken1795600000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken";
2
+ import { AddStatusPageSubscriberIsAddedByTeam1795700000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam";
3
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
4
+ import { QueryRunner } from "typeorm";
5
+ import { describe, expect, test } from "@jest/globals";
6
+
7
+ /*
8
+ * The migrations behind unsubscribing without signing in: they add
9
+ * StatusPageSubscriber.unsubscribedAt, .unsubscribeToken and .isAddedByTeam.
10
+ *
11
+ * Only the columns, and none of them rewrites the table: a migration runs in
12
+ * one transaction, holding ADD COLUMN's exclusive lock on StatusPageSubscriber
13
+ * until it commits, and under the connection's statement timeout. So filling
14
+ * the columns for existing subscribers is the
15
+ * BackfillStatusPageSubscriberUnsubscribeColumns data migration's job, in
16
+ * batches (StatusPageSubscriberUnsubscribePostgres runs it against a real
17
+ * Postgres).
18
+ *
19
+ * Pure SQL-contract assertions against a fake QueryRunner.
20
+ */
21
+
22
+ type MakeQueryRunnerResult = {
23
+ runner: QueryRunner;
24
+ statements: Array<string>;
25
+ };
26
+
27
+ const makeQueryRunner: () => MakeQueryRunnerResult =
28
+ (): MakeQueryRunnerResult => {
29
+ const statements: Array<string> = [];
30
+
31
+ const query: (...args: Array<unknown>) => Promise<undefined> = (
32
+ ...args: Array<unknown>
33
+ ): Promise<undefined> => {
34
+ statements.push(String(args[0]));
35
+ return Promise.resolve(undefined);
36
+ };
37
+
38
+ return {
39
+ runner: { query } as unknown as QueryRunner,
40
+ statements,
41
+ };
42
+ };
43
+
44
+ describe("AddStatusPageSubscriberUnsubscribeToken1795600000000", () => {
45
+ test("adds the two columns, nullable, and nothing else", async () => {
46
+ const { runner, statements } = makeQueryRunner();
47
+
48
+ await new AddStatusPageSubscriberUnsubscribeToken1795600000000().up(runner);
49
+
50
+ expect(statements).toEqual([
51
+ `ALTER TABLE "StatusPageSubscriber" ADD "unsubscribedAt" TIMESTAMP WITH TIME ZONE`,
52
+ `ALTER TABLE "StatusPageSubscriber" ADD "unsubscribeToken" character varying(100)`,
53
+ ]);
54
+ });
55
+
56
+ test("never updates rows inside the migration's transaction", async () => {
57
+ const { runner, statements } = makeQueryRunner();
58
+
59
+ await new AddStatusPageSubscriberUnsubscribeToken1795600000000().up(runner);
60
+
61
+ for (const statement of statements) {
62
+ expect(statement.startsWith("ALTER TABLE")).toBe(true);
63
+ // A nullable column with no default is added without a table rewrite.
64
+ expect(statement).not.toContain("DEFAULT");
65
+ expect(statement).not.toContain("NOT NULL");
66
+ }
67
+ });
68
+
69
+ test("the column is wide enough for a 64-character token", async () => {
70
+ const { runner, statements } = makeQueryRunner();
71
+
72
+ await new AddStatusPageSubscriberUnsubscribeToken1795600000000().up(runner);
73
+
74
+ const width: RegExpMatchArray | null = statements[1]!.match(
75
+ /character varying\((\d+)\)/,
76
+ );
77
+
78
+ expect(Number(width![1])).toBeGreaterThanOrEqual(64);
79
+ });
80
+
81
+ test("down() drops exactly what up() added", async () => {
82
+ const { runner, statements } = makeQueryRunner();
83
+
84
+ await new AddStatusPageSubscriberUnsubscribeToken1795600000000().down(
85
+ runner,
86
+ );
87
+
88
+ expect(statements).toEqual([
89
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "unsubscribeToken"`,
90
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "unsubscribedAt"`,
91
+ ]);
92
+ });
93
+
94
+ test("is registered, after the migrations it builds on, and named for its class", () => {
95
+ const names: Array<string> = SchemaMigrations.map(
96
+ (migration: unknown): string => {
97
+ return (migration as { name: string }).name;
98
+ },
99
+ );
100
+
101
+ const index: number = names.indexOf(
102
+ "AddStatusPageSubscriberUnsubscribeToken1795600000000",
103
+ );
104
+
105
+ expect(index).toBeGreaterThan(-1);
106
+ expect(index).toBeGreaterThan(
107
+ names.indexOf("AddIncidentTemplateStatusPageScopeFlag1795500000000"),
108
+ );
109
+ expect(
110
+ new AddStatusPageSubscriberUnsubscribeToken1795600000000().name,
111
+ ).toBe("AddStatusPageSubscriberUnsubscribeToken1795600000000");
112
+ });
113
+ });
114
+
115
+ describe("AddStatusPageSubscriberIsAddedByTeam1795700000000", () => {
116
+ test("adds the column NOT NULL with a constant default, which Postgres does without a rewrite", async () => {
117
+ const { runner, statements } = makeQueryRunner();
118
+
119
+ await new AddStatusPageSubscriberIsAddedByTeam1795700000000().up(runner);
120
+
121
+ expect(statements).toEqual([
122
+ `ALTER TABLE "StatusPageSubscriber" ADD "isAddedByTeam" boolean NOT NULL DEFAULT false`,
123
+ ]);
124
+ });
125
+
126
+ test("down() drops exactly what up() added", async () => {
127
+ const { runner, statements } = makeQueryRunner();
128
+
129
+ await new AddStatusPageSubscriberIsAddedByTeam1795700000000().down(runner);
130
+
131
+ expect(statements).toEqual([
132
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isAddedByTeam"`,
133
+ ]);
134
+ });
135
+
136
+ test("is registered right after the token migration, and named for its class", () => {
137
+ const names: Array<string> = SchemaMigrations.map(
138
+ (migration: unknown): string => {
139
+ return (migration as { name: string }).name;
140
+ },
141
+ );
142
+
143
+ expect(
144
+ names.indexOf("AddStatusPageSubscriberIsAddedByTeam1795700000000"),
145
+ ).toBe(
146
+ names.indexOf("AddStatusPageSubscriberUnsubscribeToken1795600000000") + 1,
147
+ );
148
+ expect(new AddStatusPageSubscriberIsAddedByTeam1795700000000().name).toBe(
149
+ "AddStatusPageSubscriberIsAddedByTeam1795700000000",
150
+ );
151
+ });
152
+ });