@oneuptime/common 12.0.13 → 12.0.14

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 (376) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  2. package/Models/DatabaseModels/UserCall.ts +94 -3
  3. package/Models/DatabaseModels/UserEmail.ts +94 -3
  4. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  5. package/Models/DatabaseModels/UserPush.ts +37 -0
  6. package/Models/DatabaseModels/UserSMS.ts +94 -3
  7. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  8. package/Server/API/BaseAPI.ts +79 -14
  9. package/Server/API/UserCallAPI.ts +53 -45
  10. package/Server/API/UserEmailAPI.ts +53 -44
  11. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  12. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  13. package/Server/API/UserPushAPI.ts +126 -2
  14. package/Server/API/UserSmsAPI.ts +53 -44
  15. package/Server/API/UserWhatsAppAPI.ts +53 -50
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  21. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  22. package/Server/Services/DatabaseService.ts +101 -0
  23. package/Server/Services/LogAggregationService.ts +5 -0
  24. package/Server/Services/PushNotificationService.ts +127 -12
  25. package/Server/Services/TeamMemberService.ts +53 -13
  26. package/Server/Services/UserCallService.ts +55 -24
  27. package/Server/Services/UserEmailService.ts +83 -17
  28. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  29. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  30. package/Server/Services/UserNotificationRuleService.ts +26 -0
  31. package/Server/Services/UserPushService.ts +89 -0
  32. package/Server/Services/UserSmsService.ts +50 -17
  33. package/Server/Services/UserWhatsAppService.ts +48 -15
  34. package/Server/Types/Database/QueryHelper.ts +30 -0
  35. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  36. package/Server/Utils/ChannelVerification.ts +510 -0
  37. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
  38. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  39. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  40. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  41. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  42. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  43. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  44. package/Server/Utils/TotpAuth.ts +137 -9
  45. package/Server/Utils/UserRegistrationToken.ts +182 -0
  46. package/Server/Utils/VerificationCode.ts +134 -0
  47. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  48. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  49. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  50. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  51. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  52. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  53. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  54. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  55. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  56. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  57. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  58. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  59. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  60. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  61. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
  62. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  63. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  64. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  65. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  66. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  67. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  68. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  69. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  70. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  71. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  72. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  73. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  74. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  75. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  76. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  77. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  78. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  79. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  80. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  81. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  82. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  83. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  84. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  85. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  86. package/Tests/Server/Utils/Browser.test.ts +162 -0
  87. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  88. package/Tests/Server/Utils/Express.test.ts +371 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  91. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  92. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  93. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  94. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  95. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  96. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  97. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  98. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  99. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  100. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  101. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  102. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  103. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  104. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  105. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  106. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  107. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  108. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  109. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  110. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  111. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  112. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  113. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  114. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  115. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  116. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  117. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  118. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  119. package/Tests/UI/Components/Button.test.tsx +175 -1
  120. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  121. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  122. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  123. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  124. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  125. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  126. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  127. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  128. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  129. package/Tests/UI/Components/List.test.tsx +9 -2
  130. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  131. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  132. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  133. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  134. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  135. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  136. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  137. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  138. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  139. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  140. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  141. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  142. package/Tests/UI/Components/Toast.test.tsx +130 -7
  143. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  144. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  145. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  146. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  147. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  148. package/Tests/Utils/API.test.ts +16 -2
  149. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  150. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  151. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  152. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  153. package/Types/Email/EmailTemplateType.ts +1 -0
  154. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  155. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  156. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  157. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  158. package/Types/Permission.ts +35 -0
  159. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  160. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  161. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  162. package/Types/Service/ServiceLanguage.ts +144 -0
  163. package/Types/Text.ts +111 -19
  164. package/UI/Components/Button/Button.tsx +27 -14
  165. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  166. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  167. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  168. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  169. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  170. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  171. package/UI/Components/CommandPalette/Types.ts +49 -0
  172. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  173. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  174. package/UI/Components/Feed/FeedItem.tsx +1 -1
  175. package/UI/Components/List/List.tsx +37 -5
  176. package/UI/Components/List/ListSkeleton.tsx +54 -0
  177. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  178. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  179. package/UI/Components/Modal/Modal.tsx +2 -2
  180. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  181. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  182. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  183. package/UI/Components/Navbar/NavBar.tsx +14 -1
  184. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  185. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  186. package/UI/Components/SideOver/SideOver.tsx +37 -2
  187. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  188. package/UI/Components/Table/Table.tsx +74 -44
  189. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  190. package/UI/Components/Tabs/Tab.tsx +1 -1
  191. package/UI/Components/Toast/Toast.tsx +118 -53
  192. package/UI/Components/Toast/ToastInit.tsx +82 -32
  193. package/UI/Styles/Theme.css +40 -0
  194. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  195. package/UI/Utils/ModelListCache.ts +188 -0
  196. package/UI/Utils/Project.ts +29 -0
  197. package/Utils/API.ts +6 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  201. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  203. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  205. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  207. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  208. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  209. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  211. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  212. package/build/dist/Server/API/BaseAPI.js +69 -13
  213. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  214. package/build/dist/Server/API/UserCallAPI.js +42 -38
  215. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  216. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  217. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  218. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  219. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  220. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  221. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  222. package/build/dist/Server/API/UserPushAPI.js +95 -2
  223. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  224. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  225. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  226. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  227. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  233. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  234. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  236. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  237. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  239. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  240. package/build/dist/Server/Services/DatabaseService.js +74 -0
  241. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  242. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  243. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  244. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  245. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  246. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  247. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  248. package/build/dist/Server/Services/UserCallService.js +54 -22
  249. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  250. package/build/dist/Server/Services/UserEmailService.js +78 -17
  251. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  252. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  253. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  254. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  255. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  256. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  257. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  258. package/build/dist/Server/Services/UserPushService.js +77 -0
  259. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  260. package/build/dist/Server/Services/UserSmsService.js +50 -17
  261. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  262. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  263. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  264. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  265. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  266. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  267. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  268. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  269. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  270. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
  271. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  272. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  273. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  274. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  275. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  276. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  277. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  278. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  279. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  280. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  281. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  282. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  283. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  284. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  285. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  286. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  287. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  288. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  289. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  290. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  291. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  292. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  297. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  298. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  299. package/build/dist/Types/Permission.js +33 -0
  300. package/build/dist/Types/Permission.js.map +1 -1
  301. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  302. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  303. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  304. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  305. package/build/dist/Types/Text.js +82 -17
  306. package/build/dist/Types/Text.js.map +1 -1
  307. package/build/dist/UI/Components/Button/Button.js +22 -14
  308. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  309. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  310. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  311. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  312. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  313. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  314. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  315. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  316. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  317. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  318. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  319. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  320. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  321. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  322. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  323. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  324. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  325. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  326. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  327. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  328. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  329. package/build/dist/UI/Components/List/List.js +19 -5
  330. package/build/dist/UI/Components/List/List.js.map +1 -1
  331. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  332. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  333. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  334. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  335. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  336. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  337. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  338. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  339. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  340. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  341. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  342. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  343. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  344. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  345. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  346. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  347. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  348. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  349. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  350. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  351. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  352. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  353. package/build/dist/UI/Components/Table/Table.js +35 -24
  354. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  355. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  356. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  357. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  358. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  359. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  360. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  361. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  362. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  363. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  364. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  365. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  366. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  367. package/build/dist/UI/Utils/Project.js +24 -1
  368. package/build/dist/UI/Utils/Project.js.map +1 -1
  369. package/build/dist/Utils/API.js +6 -1
  370. package/build/dist/Utils/API.js.map +1 -1
  371. package/jest.config.json +1 -1
  372. package/package.json +2 -3
  373. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  374. package/Tests/__mocks__/otpauth.js +0 -30
  375. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  376. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
@@ -0,0 +1,586 @@
1
+ import Redis, { ClientType } from "../Infrastructure/Redis";
2
+ import {
3
+ ExpressRequest,
4
+ ExpressResponse,
5
+ NextFunction,
6
+ OneUptimeRequest,
7
+ } from "../Utils/Express";
8
+ import logger from "../Utils/Logger";
9
+ import resolveTrustedClientIp, { ClientIpRequestLike } from "../Utils/ClientIp";
10
+ import Response from "../Utils/Response";
11
+ import ObjectID from "../../Types/ObjectID";
12
+ import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
13
+
14
+ /*
15
+ * Request limiting for the notification-channel verification routes:
16
+ * /user-email/verify, /user-sms/verify, /user-call/verify,
17
+ * /user-whats-app/verify, /user-incoming-call-number/verify and each of their
18
+ * resend siblings.
19
+ *
20
+ * WHAT THIS IS FOR, GIVEN THE DATABASE ALREADY COUNTS ATTEMPTS
21
+ *
22
+ * The durable controls in ChannelVerification bound guessing against ONE
23
+ * issued code: five attempts, then the code is burned. This middleware bounds
24
+ * the things a per-row counter cannot see.
25
+ *
26
+ * ROW ROTATION. Nothing stops a caller creating a hundred channel rows —
27
+ * that is the ordinary "add my phone" flow — and each new row is a fresh
28
+ * code with a fresh attempt budget. Five guesses per row multiplied by
29
+ * however many rows the caller cares to create is not bounded by anything
30
+ * on the row. The per-user and per-address counters are.
31
+ *
32
+ * MESSAGE VOLUME. Every issued code is a real email, SMS, voice call or
33
+ * WhatsApp message to somebody who did not ask for it, billed to the
34
+ * project. The resend bucket bounds that independently of whether any
35
+ * individual row is inside its own cooldown.
36
+ *
37
+ * COST BEFORE THE CHECK. A verify request costs a session lookup and a row
38
+ * read before the attempt counter is even consulted. This runs first.
39
+ *
40
+ * THIS FAILS OPEN, DELIBERATELY
41
+ *
42
+ * If Redis is unreachable, requests are allowed through with a throttled
43
+ * warning. That is the opposite of the choice PublicDashboardRateLimit makes
44
+ * for /master-password, and the difference is that there the counter IS the
45
+ * only thing bounding guesses, whereas here the expiry, the attempt counter,
46
+ * the rotation and the resend cooldown all live in Postgres and are all still
47
+ * in force. Failing closed would mean nobody can verify a new phone number
48
+ * during a Redis blip, in exchange for a control that is at that moment
49
+ * redundant with four others.
50
+ */
51
+
52
+ export enum VerificationCodeRateLimitBucket {
53
+ /* Submitting a code. The guessing surface. */
54
+ Verify = "verify",
55
+
56
+ /* Asking for a new code. The spending-somebody-else's-attention surface. */
57
+ Resend = "resend",
58
+ }
59
+
60
+ export enum VerificationCodeRateLimitScope {
61
+ /* One channel row, for one user. */
62
+ Item = "item",
63
+
64
+ /* One signed-in user, across every row they own. Survives row rotation. */
65
+ User = "user",
66
+
67
+ /* One client address, across every account. Survives account rotation. */
68
+ Ip = "ip",
69
+ }
70
+
71
+ export enum VerificationCodeRateLimitOutcome {
72
+ Allowed = "allowed",
73
+ RateLimited = "rate-limited",
74
+
75
+ /* Redis is unreachable, so no limit can be honoured either way. */
76
+ CounterUnavailable = "counter-unavailable",
77
+ }
78
+
79
+ export interface VerificationCodeRateLimitDecision {
80
+ outcome: VerificationCodeRateLimitOutcome;
81
+ retryAfterSeconds?: number | undefined;
82
+
83
+ /* Which counter rejected, for logs. Unset unless rejected. */
84
+ scope?: VerificationCodeRateLimitScope | undefined;
85
+
86
+ /*
87
+ * True only on the request that first crossed the line in this window, so a
88
+ * flood produces one log line per key per window rather than one per
89
+ * request.
90
+ */
91
+ isFirstRejectionInWindow?: boolean | undefined;
92
+ }
93
+
94
+ const parsePositiveIntFromEnv: (envKey: string, fallback: number) => number = (
95
+ envKey: string,
96
+ fallback: number,
97
+ ): number => {
98
+ const rawValue: string | undefined = process.env[envKey];
99
+
100
+ if (!rawValue) {
101
+ return fallback;
102
+ }
103
+
104
+ const parsedValue: number = parseInt(rawValue, 10);
105
+
106
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
107
+ return fallback;
108
+ }
109
+
110
+ return parsedValue;
111
+ };
112
+
113
+ interface BucketConfig {
114
+ windowSeconds: number;
115
+ perItemLimit: number;
116
+ perUserLimit: number;
117
+ perIpLimit: number;
118
+ }
119
+
120
+ /*
121
+ * Verify budget, over a window matching the code's own lifetime.
122
+ *
123
+ * Per item is 10 against a per-code attempt limit of 5, so it never fires
124
+ * before the durable counter does for a single honest code — it is there to
125
+ * bound the requests that arrive after the row is already locked out.
126
+ *
127
+ * Per user is the one that closes row rotation: 50 submissions per quarter
128
+ * hour across every channel row the account owns. A person adding a phone
129
+ * number and mistyping the code a few times is nowhere near it; a script
130
+ * creating rows to farm attempt budgets hits it after ten rows.
131
+ *
132
+ * Per address is higher than per user because a corporate NAT can put a whole
133
+ * office behind one address, and several colleagues onboarding at once is a
134
+ * real thing that must not be refused.
135
+ */
136
+ const VERIFY_BUCKET: BucketConfig = {
137
+ windowSeconds: parsePositiveIntFromEnv(
138
+ "NOTIFICATION_VERIFICATION_RATE_LIMIT_WINDOW_SECONDS",
139
+ 15 * 60,
140
+ ),
141
+ perItemLimit: parsePositiveIntFromEnv(
142
+ "NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_ITEM_PER_WINDOW",
143
+ 10,
144
+ ),
145
+ perUserLimit: parsePositiveIntFromEnv(
146
+ "NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_USER_PER_WINDOW",
147
+ 50,
148
+ ),
149
+ perIpLimit: parsePositiveIntFromEnv(
150
+ "NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_IP_PER_WINDOW",
151
+ 150,
152
+ ),
153
+ };
154
+
155
+ /*
156
+ * Resend budget. Tighter than verify, because each one of these is a message
157
+ * that arrives on somebody's phone and a line on somebody's bill. The
158
+ * per-item figure sits just above what the 60-second per-row cooldown allows
159
+ * in the window, so an honest user pressing "resend" whenever the cooldown
160
+ * lets them is never refused by this.
161
+ */
162
+ const RESEND_BUCKET: BucketConfig = {
163
+ windowSeconds: parsePositiveIntFromEnv(
164
+ "NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_WINDOW_SECONDS",
165
+ 15 * 60,
166
+ ),
167
+ perItemLimit: parsePositiveIntFromEnv(
168
+ "NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_ITEM_PER_WINDOW",
169
+ 5,
170
+ ),
171
+ perUserLimit: parsePositiveIntFromEnv(
172
+ "NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_USER_PER_WINDOW",
173
+ 15,
174
+ ),
175
+ perIpLimit: parsePositiveIntFromEnv(
176
+ "NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_IP_PER_WINDOW",
177
+ 45,
178
+ ),
179
+ };
180
+
181
+ const KEY_PREFIX: string = "notifverify:rl:";
182
+
183
+ /*
184
+ * Counter keys outlive their window by one full window, so a request landing
185
+ * on a boundary cannot read a key that was reclaimed mid-window.
186
+ */
187
+ const TTL_MULTIPLIER: number = 2;
188
+
189
+ /* Bounds a Redis key built from caller-supplied values. */
190
+ const MAX_KEY_SEGMENT_LENGTH: number = 64;
191
+
192
+ /* How often the "counter unavailable" condition may be logged, per bucket. */
193
+ const COUNTER_UNAVAILABLE_LOG_INTERVAL_MS: number = 60 * 1000;
194
+
195
+ export default class VerificationCodeRateLimit {
196
+ /*
197
+ * The client address to bill this request to.
198
+ *
199
+ * Uses the shared ClientIp helper, which reads X-Forwarded-For from the
200
+ * trusted right-hand end. Express.getClientIp takes the LEFTMOST entry,
201
+ * which the caller can set themselves — for a rate limiter that is fatal,
202
+ * because a fresh spoofed value per request is a fresh bucket per request
203
+ * and therefore no limit at all.
204
+ */
205
+ public static resolveClientIp(req: ExpressRequest): string {
206
+ const clientIp: string | undefined = resolveTrustedClientIp(
207
+ req as unknown as ClientIpRequestLike,
208
+ );
209
+
210
+ if (clientIp) {
211
+ return VerificationCodeRateLimit.sanitizeKeySegment(clientIp);
212
+ }
213
+
214
+ /*
215
+ * Everything with no resolvable address shares one bucket. That is the
216
+ * conservative direction: an unidentifiable caller should not be handed a
217
+ * private allowance.
218
+ */
219
+ return "unknown";
220
+ }
221
+
222
+ /*
223
+ * The signed-in user this request belongs to.
224
+ *
225
+ * These routes all sit behind UserMiddleware, so in practice this is always
226
+ * present. If it somehow is not, everything anonymous shares one bucket
227
+ * rather than each getting its own.
228
+ */
229
+ public static resolveUserKey(req: ExpressRequest): string {
230
+ const userId: ObjectID | undefined = (req as OneUptimeRequest)
231
+ ?.userAuthorization?.userId;
232
+
233
+ if (!userId) {
234
+ return "anonymous";
235
+ }
236
+
237
+ return VerificationCodeRateLimit.sanitizeKeySegment(userId.toString());
238
+ }
239
+
240
+ /*
241
+ * The channel row this request is about.
242
+ *
243
+ * Anything that is not a valid id collapses into one shared "invalid"
244
+ * bucket. That bounds Redis memory against a caller feeding junk ids, and
245
+ * it is the right grouping anyway: those requests cannot name a real row,
246
+ * so one small shared allowance between all of them is what we want.
247
+ */
248
+ public static resolveItemKey(req: ExpressRequest): string {
249
+ const raw: unknown = (req.body as Record<string, unknown> | undefined)?.[
250
+ "itemId"
251
+ ];
252
+
253
+ if (typeof raw !== "string" || raw.trim().length === 0) {
254
+ return "none";
255
+ }
256
+
257
+ const value: string = raw.trim();
258
+
259
+ if (!ObjectID.isValidUUID(value)) {
260
+ return "invalid";
261
+ }
262
+
263
+ return value.toLowerCase();
264
+ }
265
+
266
+ private static sanitizeKeySegment(value: string): string {
267
+ return (
268
+ value
269
+ .slice(0, MAX_KEY_SEGMENT_LENGTH)
270
+ /*
271
+ * Redis keys are binary safe, but a predictable charset keeps
272
+ * operational tooling (SCAN patterns, dashboards) sane.
273
+ */
274
+ .replace(/[^a-zA-Z0-9._:%\-[\]]/g, "_")
275
+ );
276
+ }
277
+
278
+ public static getBucketConfig(
279
+ bucket: VerificationCodeRateLimitBucket,
280
+ ): BucketConfig {
281
+ return bucket === VerificationCodeRateLimitBucket.Resend
282
+ ? RESEND_BUCKET
283
+ : VERIFY_BUCKET;
284
+ }
285
+
286
+ /*
287
+ * Increment all three counters for this request and decide.
288
+ *
289
+ * All three INCRs go out in one pipeline so the common path is a single
290
+ * round trip; the EXPIRE follow-ups only happen on the request that created
291
+ * a key.
292
+ *
293
+ * Every counter is incremented on every request, INCLUDING a rejected one.
294
+ * That is standard fixed-window behaviour and it is deliberate: a caller
295
+ * who keeps hammering keeps their window pinned rather than being handed a
296
+ * fresh allowance for free.
297
+ */
298
+ public static async consume(data: {
299
+ itemKey: string;
300
+ userKey: string;
301
+ clientIp: string;
302
+ bucket: VerificationCodeRateLimitBucket;
303
+ }): Promise<VerificationCodeRateLimitDecision> {
304
+ const client: ClientType | null = Redis.getClient();
305
+
306
+ if (!client || !Redis.isConnected()) {
307
+ return { outcome: VerificationCodeRateLimitOutcome.CounterUnavailable };
308
+ }
309
+
310
+ const config: BucketConfig = VerificationCodeRateLimit.getBucketConfig(
311
+ data.bucket,
312
+ );
313
+
314
+ const windowMs: number = config.windowSeconds * 1000;
315
+ const windowIndex: number = Math.floor(Date.now() / windowMs);
316
+
317
+ /*
318
+ * The item counter is keyed on item AND user. Two users cannot own the
319
+ * same row, so this changes nothing about correctness — but it means a
320
+ * caller cannot deny a row's real owner their allowance by burning it
321
+ * against an id they do not own.
322
+ */
323
+ const itemCounterKey: string = `${KEY_PREFIX}${data.bucket}:t:${data.userKey}:${data.itemKey}:${windowIndex}`;
324
+ const userCounterKey: string = `${KEY_PREFIX}${data.bucket}:u:${data.userKey}:${windowIndex}`;
325
+ const ipCounterKey: string = `${KEY_PREFIX}${data.bucket}:i:${data.clientIp}:${windowIndex}`;
326
+
327
+ try {
328
+ const pipelineResults: Array<[Error | null, unknown]> | null =
329
+ (await client
330
+ .pipeline()
331
+ .incr(itemCounterKey)
332
+ .incr(userCounterKey)
333
+ .incr(ipCounterKey)
334
+ .exec()) as Array<[Error | null, unknown]> | null;
335
+
336
+ if (!pipelineResults || pipelineResults.length < 3) {
337
+ throw new Error("Rate limit pipeline returned no result");
338
+ }
339
+
340
+ const itemCount: number = VerificationCodeRateLimit.readCounterResult(
341
+ pipelineResults[0],
342
+ );
343
+ const userCount: number = VerificationCodeRateLimit.readCounterResult(
344
+ pipelineResults[1],
345
+ );
346
+ const ipCount: number = VerificationCodeRateLimit.readCounterResult(
347
+ pipelineResults[2],
348
+ );
349
+
350
+ /*
351
+ * Set the expiry only on the write that created the key. Re-issuing
352
+ * EXPIRE on every increment would slide the window forward for as long
353
+ * as the load continues, so a caller who tripped the limit once could
354
+ * never recover.
355
+ */
356
+ const ttlSeconds: number = config.windowSeconds * TTL_MULTIPLIER;
357
+ const keysToExpire: Array<string> = [];
358
+
359
+ if (itemCount === 1) {
360
+ keysToExpire.push(itemCounterKey);
361
+ }
362
+
363
+ if (userCount === 1) {
364
+ keysToExpire.push(userCounterKey);
365
+ }
366
+
367
+ if (ipCount === 1) {
368
+ keysToExpire.push(ipCounterKey);
369
+ }
370
+
371
+ if (keysToExpire.length > 0) {
372
+ const expirePipeline: ReturnType<ClientType["pipeline"]> =
373
+ client.pipeline();
374
+
375
+ for (const key of keysToExpire) {
376
+ expirePipeline.expire(key, ttlSeconds);
377
+ }
378
+
379
+ await expirePipeline.exec();
380
+ }
381
+
382
+ const retryAfterSeconds: number =
383
+ VerificationCodeRateLimit.getSecondsUntilWindowEnd(
384
+ config.windowSeconds,
385
+ );
386
+
387
+ /*
388
+ * Reported most specific first, because that is the most useful thing
389
+ * to see in a log line when more than one is over.
390
+ */
391
+ if (itemCount > config.perItemLimit) {
392
+ return {
393
+ outcome: VerificationCodeRateLimitOutcome.RateLimited,
394
+ retryAfterSeconds,
395
+ scope: VerificationCodeRateLimitScope.Item,
396
+ isFirstRejectionInWindow: itemCount === config.perItemLimit + 1,
397
+ };
398
+ }
399
+
400
+ if (userCount > config.perUserLimit) {
401
+ return {
402
+ outcome: VerificationCodeRateLimitOutcome.RateLimited,
403
+ retryAfterSeconds,
404
+ scope: VerificationCodeRateLimitScope.User,
405
+ isFirstRejectionInWindow: userCount === config.perUserLimit + 1,
406
+ };
407
+ }
408
+
409
+ if (ipCount > config.perIpLimit) {
410
+ return {
411
+ outcome: VerificationCodeRateLimitOutcome.RateLimited,
412
+ retryAfterSeconds,
413
+ scope: VerificationCodeRateLimitScope.Ip,
414
+ isFirstRejectionInWindow: ipCount === config.perIpLimit + 1,
415
+ };
416
+ }
417
+
418
+ return { outcome: VerificationCodeRateLimitOutcome.Allowed };
419
+ } catch (err) {
420
+ /*
421
+ * Throttled: whatever broke Redis is unlikely to have broken it for one
422
+ * request only, and a per-request log line buries the incident it is
423
+ * reporting.
424
+ */
425
+ if (VerificationCodeRateLimit.shouldLogCounterUnavailable(data.bucket)) {
426
+ logger.warn(
427
+ `VerificationCodeRateLimit: counter failed for ${data.bucket} request from user ${data.userKey}`,
428
+ );
429
+ logger.warn(err);
430
+ }
431
+
432
+ return { outcome: VerificationCodeRateLimitOutcome.CounterUnavailable };
433
+ }
434
+ }
435
+
436
+ private static readCounterResult(
437
+ result: [Error | null, unknown] | undefined,
438
+ ): number {
439
+ if (!result) {
440
+ throw new Error("Rate limit counter returned no result");
441
+ }
442
+
443
+ const [error, value] = result;
444
+
445
+ if (error) {
446
+ throw error;
447
+ }
448
+
449
+ if (typeof value !== "number") {
450
+ throw new Error("Rate limit counter returned a non-numeric value");
451
+ }
452
+
453
+ return value;
454
+ }
455
+
456
+ /*
457
+ * Remaining seconds in the current fixed window, so every rejected caller
458
+ * is told to come back when the window actually rolls rather than all
459
+ * backing off by the same constant and re-colliding.
460
+ */
461
+ private static getSecondsUntilWindowEnd(windowSeconds: number): number {
462
+ const windowMs: number = windowSeconds * 1000;
463
+ const msIntoWindow: number = Date.now() % windowMs;
464
+
465
+ return Math.max(1, Math.ceil((windowMs - msIntoWindow) / 1000));
466
+ }
467
+
468
+ /*
469
+ * Express middleware. Register AFTER UserMiddleware — the user counter is
470
+ * the one that closes row rotation, and it needs the session — but before
471
+ * the route handler, so a flood is refused before it costs a row read.
472
+ */
473
+ public static getMiddleware(
474
+ bucket: VerificationCodeRateLimitBucket,
475
+ ): (
476
+ req: ExpressRequest,
477
+ res: ExpressResponse,
478
+ next: NextFunction,
479
+ ) => Promise<void> {
480
+ return async (
481
+ req: ExpressRequest,
482
+ res: ExpressResponse,
483
+ next: NextFunction,
484
+ ): Promise<void> => {
485
+ const itemKey: string = VerificationCodeRateLimit.resolveItemKey(req);
486
+ const userKey: string = VerificationCodeRateLimit.resolveUserKey(req);
487
+ const clientIp: string = VerificationCodeRateLimit.resolveClientIp(req);
488
+
489
+ const decision: VerificationCodeRateLimitDecision =
490
+ await VerificationCodeRateLimit.consume({
491
+ itemKey,
492
+ userKey,
493
+ clientIp,
494
+ bucket,
495
+ });
496
+
497
+ if (decision.outcome === VerificationCodeRateLimitOutcome.RateLimited) {
498
+ if (decision.retryAfterSeconds) {
499
+ VerificationCodeRateLimit.setRetryAfterHeader(
500
+ res,
501
+ decision.retryAfterSeconds,
502
+ );
503
+ }
504
+
505
+ if (decision.isFirstRejectionInWindow) {
506
+ logger.warn(
507
+ `VerificationCodeRateLimit: rejected ${bucket} request for item ${itemKey} from user ${userKey} at ${clientIp} (${decision.scope} limit)`,
508
+ );
509
+ }
510
+
511
+ return Response.sendErrorResponse(
512
+ req,
513
+ res,
514
+ new TooManyRequestsException(
515
+ bucket === VerificationCodeRateLimitBucket.Resend
516
+ ? "Too many verification codes requested. Please try again later."
517
+ : "Too many verification attempts. Please try again later.",
518
+ ),
519
+ );
520
+ }
521
+
522
+ if (
523
+ decision.outcome === VerificationCodeRateLimitOutcome.CounterUnavailable
524
+ ) {
525
+ /*
526
+ * Fails open. See the note at the top of this file: the expiry, the
527
+ * attempt counter, the rotation and the resend cooldown are all in
528
+ * Postgres and all still in force, so allowing the request through
529
+ * costs a bounded amount of extra attempt budget rather than an
530
+ * unlimited guessing oracle.
531
+ */
532
+ if (VerificationCodeRateLimit.shouldLogCounterUnavailable(bucket)) {
533
+ logger.warn(
534
+ `VerificationCodeRateLimit: rate limit counter unavailable, allowing ${bucket} requests unthrottled`,
535
+ );
536
+ }
537
+ }
538
+
539
+ return next();
540
+ };
541
+ }
542
+
543
+ /*
544
+ * A Redis outage means EVERY request takes the unavailable path, so an
545
+ * unguarded log line there is one per request for as long as the outage
546
+ * lasts. One line per bucket per interval makes the condition visible
547
+ * without burying everything else.
548
+ */
549
+ private static shouldLogCounterUnavailable(
550
+ bucket: VerificationCodeRateLimitBucket,
551
+ ): boolean {
552
+ const now: number = Date.now();
553
+ const lastLoggedAt: number =
554
+ VerificationCodeRateLimit.counterUnavailableLastLoggedAt.get(bucket) || 0;
555
+
556
+ if (now - lastLoggedAt < COUNTER_UNAVAILABLE_LOG_INTERVAL_MS) {
557
+ return false;
558
+ }
559
+
560
+ VerificationCodeRateLimit.counterUnavailableLastLoggedAt.set(bucket, now);
561
+
562
+ return true;
563
+ }
564
+
565
+ private static counterUnavailableLastLoggedAt: Map<
566
+ VerificationCodeRateLimitBucket,
567
+ number
568
+ > = new Map();
569
+
570
+ private static setRetryAfterHeader(
571
+ res: ExpressResponse,
572
+ retryAfterSeconds: number,
573
+ ): void {
574
+ const setHeader: unknown = (res as unknown as Record<string, unknown>)[
575
+ "setHeader"
576
+ ];
577
+
578
+ if (typeof setHeader === "function") {
579
+ (setHeader as (name: string, value: string) => void).call(
580
+ res,
581
+ "Retry-After",
582
+ String(retryAfterSeconds),
583
+ );
584
+ }
585
+ }
586
+ }