@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,689 @@
1
+ import ChannelVerification, {
2
+ ChannelVerificationOutcome,
3
+ ChannelVerificationResult,
4
+ MAX_VERIFICATION_ATTEMPTS,
5
+ RESEND_COOLDOWN_SECONDS,
6
+ VERIFICATION_CODE_EXPIRY_MINUTES,
7
+ VerifiableChannelFields,
8
+ } from "../../../Server/Utils/ChannelVerification";
9
+ import VerificationCode from "../../../Server/Utils/VerificationCode";
10
+ import DatabaseService from "../../../Server/Services/DatabaseService";
11
+ import ObjectID from "../../../Types/ObjectID";
12
+ import UserEmail from "../../../Models/DatabaseModels/UserEmail";
13
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
14
+
15
+ jest.mock("../../../Server/Utils/Logger", () => {
16
+ return {
17
+ __esModule: true,
18
+ default: {
19
+ debug: jest.fn(),
20
+ info: jest.fn(),
21
+ warn: jest.fn(),
22
+ error: jest.fn(),
23
+ },
24
+ getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
25
+ };
26
+ });
27
+
28
+ /*
29
+ * GHSA-5cr8-vph4-3hrf — the four controls that replace "compare the column".
30
+ *
31
+ * These run against a fake row store rather than mocks-that-assert, because
32
+ * the interesting claims are about STATE: that an attempt is consumed even
33
+ * when the request racing it also consumed one, that crossing the limit burns
34
+ * the stored challenge rather than merely refusing the request in front of it,
35
+ * and that a used code stops working. A test that only checked which methods
36
+ * were called would pass against a version that did none of those things.
37
+ */
38
+
39
+ const ITEM_ID: ObjectID = new ObjectID("4c1e0c8e-1111-4111-8111-111111111111");
40
+ const OWNER_ID: ObjectID = new ObjectID("4c1e0c8e-2222-4222-8222-222222222222");
41
+ const OTHER_USER_ID: ObjectID = new ObjectID(
42
+ "4c1e0c8e-3333-4333-8333-333333333333",
43
+ );
44
+ const PROJECT_ID: ObjectID = new ObjectID(
45
+ "4c1e0c8e-4444-4444-8444-444444444444",
46
+ );
47
+
48
+ interface StoredRow extends VerifiableChannelFields {
49
+ _id?: string | undefined;
50
+ }
51
+
52
+ /*
53
+ * A stand-in for one channel service. findOneById/updateOneById read and
54
+ * write the row; the increment really increments, so concurrency can be
55
+ * exercised rather than asserted about.
56
+ */
57
+ class FakeChannelService {
58
+ public row: StoredRow | null = null;
59
+ public updateCalls: Array<Record<string, unknown>> = [];
60
+ public incrementCalls: number = 0;
61
+
62
+ public async findOneById(): Promise<StoredRow | null> {
63
+ if (!this.row) {
64
+ return null;
65
+ }
66
+
67
+ /* A copy, so the caller cannot accidentally mutate the store. */
68
+ return { ...this.row };
69
+ }
70
+
71
+ public async updateOneById(data: {
72
+ id: ObjectID;
73
+ data: Record<string, unknown>;
74
+ }): Promise<number> {
75
+ this.updateCalls.push(data.data);
76
+
77
+ if (this.row) {
78
+ this.row = { ...this.row, ...(data.data as StoredRow) };
79
+ }
80
+
81
+ return 1;
82
+ }
83
+
84
+ public async atomicIncrementColumnValueByOneAndGetValue(data: {
85
+ columnName: string;
86
+ }): Promise<number> {
87
+ this.incrementCalls++;
88
+
89
+ if (!this.row) {
90
+ throw new Error("row not found");
91
+ }
92
+
93
+ const next: number =
94
+ ((this.row as unknown as Record<string, number>)[data.columnName] || 0) +
95
+ 1;
96
+
97
+ (this.row as unknown as Record<string, number>)[data.columnName] = next;
98
+
99
+ return next;
100
+ }
101
+
102
+ public getModel(): { tableName: string } {
103
+ return { tableName: "UserEmail" };
104
+ }
105
+ }
106
+
107
+ type ServiceUnderTest = DatabaseService<UserEmail>;
108
+
109
+ const asService: (fake: FakeChannelService) => ServiceUnderTest = (
110
+ fake: FakeChannelService,
111
+ ) => {
112
+ return fake as unknown as ServiceUnderTest;
113
+ };
114
+
115
+ const buildLiveRow: (overrides?: Partial<StoredRow>) => StoredRow = (
116
+ overrides: Partial<StoredRow> = {},
117
+ ) => {
118
+ return {
119
+ _id: ITEM_ID.toString(),
120
+ userId: OWNER_ID,
121
+ projectId: PROJECT_ID,
122
+ isVerified: false,
123
+ verificationCode: VerificationCode.hashCode({
124
+ code: "123456",
125
+ channelId: ITEM_ID,
126
+ }),
127
+ verificationCodeExpiresAt: ChannelVerification.getExpiresAt(),
128
+ verificationFailedAttempts: 0,
129
+ verificationCodeSentAt: new Date(),
130
+ ...overrides,
131
+ };
132
+ };
133
+
134
+ const verify: (
135
+ fake: FakeChannelService,
136
+ code: string,
137
+ userId?: ObjectID,
138
+ ) => Promise<ChannelVerificationResult> = (
139
+ fake: FakeChannelService,
140
+ code: string,
141
+ userId?: ObjectID,
142
+ ) => {
143
+ return ChannelVerification.verifyCode({
144
+ service: asService(fake),
145
+ itemId: ITEM_ID,
146
+ userId: userId || OWNER_ID,
147
+ code,
148
+ });
149
+ };
150
+
151
+ describe("ChannelVerification", () => {
152
+ let service: FakeChannelService;
153
+
154
+ beforeEach(() => {
155
+ service = new FakeChannelService();
156
+ service.row = buildLiveRow();
157
+ });
158
+
159
+ describe("issueCode", () => {
160
+ it("stores a digest and never the code", () => {
161
+ const issued: {
162
+ plainCode: string;
163
+ fields: VerifiableChannelFields;
164
+ } = ChannelVerification.issueCode({ channelId: ITEM_ID });
165
+
166
+ expect(issued.plainCode).toMatch(/^[0-9]{6}$/);
167
+ expect(issued.fields.verificationCode).not.toBe(issued.plainCode);
168
+ expect(issued.fields.verificationCode).toMatch(/^[0-9a-f]{64}$/);
169
+ });
170
+
171
+ it("stores a digest that the plaintext verifies against", () => {
172
+ const issued: {
173
+ plainCode: string;
174
+ fields: VerifiableChannelFields;
175
+ } = ChannelVerification.issueCode({ channelId: ITEM_ID });
176
+
177
+ expect(
178
+ VerificationCode.isHashEqual(
179
+ VerificationCode.hashCode({
180
+ code: issued.plainCode,
181
+ channelId: ITEM_ID,
182
+ }),
183
+ issued.fields.verificationCode as string,
184
+ ),
185
+ ).toBe(true);
186
+ });
187
+
188
+ it("sets an expiry, records the send, and resets the attempt counter", () => {
189
+ const now: Date = new Date("2026-03-01T12:00:00.000Z");
190
+
191
+ const issued: {
192
+ plainCode: string;
193
+ fields: VerifiableChannelFields;
194
+ } = ChannelVerification.issueCode({ channelId: ITEM_ID, now });
195
+
196
+ expect(issued.fields.verificationCodeExpiresAt).toEqual(
197
+ new Date(now.getTime() + VERIFICATION_CODE_EXPIRY_MINUTES * 60 * 1000),
198
+ );
199
+ expect(issued.fields.verificationCodeSentAt).toEqual(now);
200
+ expect(issued.fields.verificationFailedAttempts).toBe(0);
201
+ });
202
+
203
+ /*
204
+ * A resend has to hand the attacker a NEW target, not the same one back.
205
+ * Reissuing the same code would make the attempt counter worthless: burn
206
+ * five, resend, resume against the same value.
207
+ */
208
+ it("issues a different code each time", () => {
209
+ const codes: Set<string> = new Set<string>();
210
+
211
+ for (let i: number = 0; i < 200; i++) {
212
+ codes.add(
213
+ ChannelVerification.issueCode({ channelId: ITEM_ID }).plainCode,
214
+ );
215
+ }
216
+
217
+ expect(codes.size).toBeGreaterThan(190);
218
+ });
219
+ });
220
+
221
+ describe("issueCodeOnItem", () => {
222
+ it("writes the digest, expiry, sent time and a zeroed counter to the row", async () => {
223
+ const plainCode: string = await ChannelVerification.issueCodeOnItem({
224
+ service: asService(service),
225
+ itemId: ITEM_ID,
226
+ });
227
+
228
+ const written: Record<string, unknown> = service.updateCalls[0] as Record<
229
+ string,
230
+ unknown
231
+ >;
232
+
233
+ expect(Object.keys(written).sort()).toEqual([
234
+ "verificationCode",
235
+ "verificationCodeExpiresAt",
236
+ "verificationCodeSentAt",
237
+ "verificationFailedAttempts",
238
+ ]);
239
+ expect(written["verificationCode"]).not.toBe(plainCode);
240
+ expect(written["verificationFailedAttempts"]).toBe(0);
241
+ });
242
+
243
+ it("returns a code that then verifies against the stored row", async () => {
244
+ const plainCode: string = await ChannelVerification.issueCodeOnItem({
245
+ service: asService(service),
246
+ itemId: ITEM_ID,
247
+ });
248
+
249
+ const result: ChannelVerificationResult = await verify(
250
+ service,
251
+ plainCode,
252
+ );
253
+
254
+ expect(result.outcome).toBe(ChannelVerificationOutcome.Verified);
255
+ });
256
+ });
257
+
258
+ describe("isCodeExpired", () => {
259
+ it("treats a future expiry as live", () => {
260
+ expect(
261
+ ChannelVerification.isCodeExpired({
262
+ expiresAt: new Date(Date.now() + 60000),
263
+ }),
264
+ ).toBe(false);
265
+ });
266
+
267
+ it("treats a past expiry as expired", () => {
268
+ expect(
269
+ ChannelVerification.isCodeExpired({
270
+ expiresAt: new Date(Date.now() - 1),
271
+ }),
272
+ ).toBe(true);
273
+ });
274
+
275
+ it("treats the exact expiry instant as expired", () => {
276
+ const now: Date = new Date("2026-03-01T12:00:00.000Z");
277
+
278
+ expect(ChannelVerification.isCodeExpired({ expiresAt: now, now })).toBe(
279
+ true,
280
+ );
281
+ });
282
+
283
+ /*
284
+ * Rows written before this column existed carry a plaintext code and no
285
+ * expiry. Reading a missing expiry as "never expires" would leave exactly
286
+ * the vulnerable state in place on every one of them.
287
+ */
288
+ it("treats a missing expiry as expired, not as never-expiring", () => {
289
+ expect(ChannelVerification.isCodeExpired({})).toBe(true);
290
+ expect(ChannelVerification.isCodeExpired({ expiresAt: null })).toBe(true);
291
+ expect(ChannelVerification.isCodeExpired({ expiresAt: undefined })).toBe(
292
+ true,
293
+ );
294
+ });
295
+ });
296
+
297
+ describe("getResendRetryAfterSeconds", () => {
298
+ it("allows the first send", () => {
299
+ expect(ChannelVerification.getResendRetryAfterSeconds({})).toBe(0);
300
+ });
301
+
302
+ it("allows a send once the cooldown has elapsed", () => {
303
+ expect(
304
+ ChannelVerification.getResendRetryAfterSeconds({
305
+ lastSentAt: new Date(
306
+ Date.now() - RESEND_COOLDOWN_SECONDS * 1000 - 1000,
307
+ ),
308
+ }),
309
+ ).toBe(0);
310
+ });
311
+
312
+ it("refuses a send inside the cooldown and says how long to wait", () => {
313
+ const now: Date = new Date("2026-03-01T12:00:30.000Z");
314
+ const lastSentAt: Date = new Date("2026-03-01T12:00:00.000Z");
315
+
316
+ expect(
317
+ ChannelVerification.getResendRetryAfterSeconds({ lastSentAt, now }),
318
+ ).toBe(RESEND_COOLDOWN_SECONDS - 30);
319
+ });
320
+
321
+ /*
322
+ * A timestamp in the future means a clock moved, not that the caller has
323
+ * earned a free send — reading it as elapsed time would hand out an
324
+ * unlimited resend budget to anybody who could nudge one.
325
+ */
326
+ it("treats a future send time as a full cooldown", () => {
327
+ expect(
328
+ ChannelVerification.getResendRetryAfterSeconds({
329
+ lastSentAt: new Date(Date.now() + 60_000),
330
+ }),
331
+ ).toBe(RESEND_COOLDOWN_SECONDS);
332
+ });
333
+ });
334
+
335
+ describe("verifyCode", () => {
336
+ it("accepts the right code and marks the row verified", async () => {
337
+ const result: ChannelVerificationResult = await verify(service, "123456");
338
+
339
+ expect(result.outcome).toBe(ChannelVerificationOutcome.Verified);
340
+ expect(result.userId?.toString()).toBe(OWNER_ID.toString());
341
+ expect(result.projectId?.toString()).toBe(PROJECT_ID.toString());
342
+ expect(service.row?.isVerified).toBe(true);
343
+ });
344
+
345
+ it("refuses a wrong code", async () => {
346
+ const result: ChannelVerificationResult = await verify(service, "654321");
347
+
348
+ expect(result.outcome).toBe(ChannelVerificationOutcome.IncorrectCode);
349
+ expect(service.row?.isVerified).toBeFalsy();
350
+ });
351
+
352
+ it("refuses a missing row", async () => {
353
+ service.row = null;
354
+
355
+ expect((await verify(service, "123456")).outcome).toBe(
356
+ ChannelVerificationOutcome.NotFound,
357
+ );
358
+ });
359
+
360
+ /*
361
+ * The row can legitimately hold somebody else's address — that is how
362
+ * "add my phone" works — so ownership is the check that keeps one user
363
+ * from verifying another user's channel.
364
+ */
365
+ it("refuses a caller who does not own the row", async () => {
366
+ const result: ChannelVerificationResult = await verify(
367
+ service,
368
+ "123456",
369
+ OTHER_USER_ID,
370
+ );
371
+
372
+ expect(result.outcome).toBe(ChannelVerificationOutcome.NotOwner);
373
+ expect(service.row?.isVerified).toBeFalsy();
374
+ });
375
+
376
+ it("does not spend an attempt when the caller is not the owner", async () => {
377
+ await verify(service, "654321", OTHER_USER_ID);
378
+
379
+ expect(service.incrementCalls).toBe(0);
380
+ expect(service.row?.verificationFailedAttempts).toBe(0);
381
+ });
382
+
383
+ it("refuses a row that is already verified", async () => {
384
+ service.row = buildLiveRow({ isVerified: true });
385
+
386
+ expect((await verify(service, "123456")).outcome).toBe(
387
+ ChannelVerificationOutcome.AlreadyVerified,
388
+ );
389
+ });
390
+
391
+ describe("expiry", () => {
392
+ it("refuses a code past its expiry, even the correct one", async () => {
393
+ service.row = buildLiveRow({
394
+ verificationCodeExpiresAt: new Date(Date.now() - 1000),
395
+ });
396
+
397
+ const result: ChannelVerificationResult = await verify(
398
+ service,
399
+ "123456",
400
+ );
401
+
402
+ expect(result.outcome).toBe(ChannelVerificationOutcome.Expired);
403
+ expect(service.row?.isVerified).toBeFalsy();
404
+ });
405
+
406
+ it("refuses a row that has no expiry recorded at all", async () => {
407
+ service.row = buildLiveRow({ verificationCodeExpiresAt: undefined });
408
+
409
+ expect((await verify(service, "123456")).outcome).toBe(
410
+ ChannelVerificationOutcome.Expired,
411
+ );
412
+ });
413
+
414
+ it("does not spend an attempt on an expired code", async () => {
415
+ service.row = buildLiveRow({
416
+ verificationCodeExpiresAt: new Date(Date.now() - 1000),
417
+ });
418
+
419
+ await verify(service, "654321");
420
+
421
+ expect(service.incrementCalls).toBe(0);
422
+ });
423
+ });
424
+
425
+ describe("attempt limit", () => {
426
+ it("allows exactly MAX_VERIFICATION_ATTEMPTS wrong guesses", async () => {
427
+ for (let i: number = 1; i <= MAX_VERIFICATION_ATTEMPTS; i++) {
428
+ const result: ChannelVerificationResult = await verify(
429
+ service,
430
+ "000000",
431
+ );
432
+
433
+ expect(result.outcome).toBe(ChannelVerificationOutcome.IncorrectCode);
434
+ }
435
+
436
+ expect((await verify(service, "000000")).outcome).toBe(
437
+ ChannelVerificationOutcome.TooManyAttempts,
438
+ );
439
+ });
440
+
441
+ /*
442
+ * The heart of the advisory: a bounded number of guesses per code, so
443
+ * walking a 10^6 space at request speed is not available.
444
+ */
445
+ it("refuses the correct code once the limit is crossed", async () => {
446
+ for (let i: number = 0; i < MAX_VERIFICATION_ATTEMPTS; i++) {
447
+ await verify(service, "000000");
448
+ }
449
+
450
+ const result: ChannelVerificationResult = await verify(
451
+ service,
452
+ "123456",
453
+ );
454
+
455
+ expect(result.outcome).toBe(ChannelVerificationOutcome.TooManyAttempts);
456
+ expect(service.row?.isVerified).toBeFalsy();
457
+ });
458
+
459
+ /*
460
+ * Rotation, not merely refusal. A lockout that leaves the challenge
461
+ * intact is a pause the attacker waits out with the same target still
462
+ * behind it.
463
+ */
464
+ it("burns the stored challenge when the limit is crossed", async () => {
465
+ const originalDigest: string | undefined =
466
+ service.row?.verificationCode;
467
+
468
+ for (let i: number = 0; i < MAX_VERIFICATION_ATTEMPTS + 1; i++) {
469
+ await verify(service, "000000");
470
+ }
471
+
472
+ expect(service.row?.verificationCode).not.toBe(originalDigest);
473
+ expect(service.row?.verificationCodeExpiresAt).toBeNull();
474
+ });
475
+
476
+ it("rewrites the challenge once, not on every request after the limit", async () => {
477
+ for (let i: number = 0; i < MAX_VERIFICATION_ATTEMPTS + 5; i++) {
478
+ await verify(service, "000000");
479
+ }
480
+
481
+ /* One write on the crossing, and none for the four requests after it. */
482
+ expect(service.updateCalls).toHaveLength(1);
483
+ });
484
+
485
+ /*
486
+ * Concurrency. A read-modify-write counter lets N simultaneous requests
487
+ * all observe the same pre-increment value and all decide they are
488
+ * inside the limit — which is precisely the shape of a brute-force
489
+ * loop. Every attempt must consume its own slot.
490
+ */
491
+ it("consumes a slot per attempt even when attempts run concurrently", async () => {
492
+ const attempts: number = MAX_VERIFICATION_ATTEMPTS + 10;
493
+
494
+ const results: Array<ChannelVerificationResult> = await Promise.all(
495
+ Array.from({ length: attempts }, () => {
496
+ return verify(service, "000000");
497
+ }),
498
+ );
499
+
500
+ const lockedOut: number = results.filter(
501
+ (result: ChannelVerificationResult) => {
502
+ return (
503
+ result.outcome === ChannelVerificationOutcome.TooManyAttempts
504
+ );
505
+ },
506
+ ).length;
507
+
508
+ expect(lockedOut).toBe(attempts - MAX_VERIFICATION_ATTEMPTS);
509
+ expect(service.row?.verificationFailedAttempts).toBe(attempts);
510
+ });
511
+
512
+ it("counts an attempt before it reveals whether the code was right", async () => {
513
+ await verify(service, "000000");
514
+
515
+ expect(service.row?.verificationFailedAttempts).toBe(1);
516
+ });
517
+
518
+ it("gives a fresh attempt budget only by issuing a fresh code", async () => {
519
+ for (let i: number = 0; i < MAX_VERIFICATION_ATTEMPTS + 1; i++) {
520
+ await verify(service, "000000");
521
+ }
522
+
523
+ const plainCode: string = await ChannelVerification.issueCodeOnItem({
524
+ service: asService(service),
525
+ itemId: ITEM_ID,
526
+ });
527
+
528
+ expect(service.row?.verificationFailedAttempts).toBe(0);
529
+ expect((await verify(service, plainCode)).outcome).toBe(
530
+ ChannelVerificationOutcome.Verified,
531
+ );
532
+ });
533
+ });
534
+
535
+ describe("single use", () => {
536
+ it("clears the challenge when a code succeeds", async () => {
537
+ const originalDigest: string | undefined =
538
+ service.row?.verificationCode;
539
+
540
+ await verify(service, "123456");
541
+
542
+ expect(service.row?.verificationCode).not.toBe(originalDigest);
543
+ expect(service.row?.verificationCodeExpiresAt).toBeNull();
544
+ expect(service.row?.verificationFailedAttempts).toBe(0);
545
+ });
546
+
547
+ it("refuses the same code a second time", async () => {
548
+ expect((await verify(service, "123456")).outcome).toBe(
549
+ ChannelVerificationOutcome.Verified,
550
+ );
551
+
552
+ expect((await verify(service, "123456")).outcome).toBe(
553
+ ChannelVerificationOutcome.AlreadyVerified,
554
+ );
555
+ });
556
+ });
557
+
558
+ describe("comparison", () => {
559
+ /*
560
+ * The stored value is a digest now, so a caller who somehow read the
561
+ * column cannot replay it as the code. This is the test that fails if
562
+ * anyone reintroduces `item.verificationCode === req.body.code`.
563
+ */
564
+ it("does not accept the stored digest as if it were the code", async () => {
565
+ const storedDigest: string = service.row?.verificationCode as string;
566
+
567
+ expect((await verify(service, storedDigest)).outcome).toBe(
568
+ ChannelVerificationOutcome.IncorrectCode,
569
+ );
570
+ });
571
+
572
+ it("does not accept a code issued for a different row", async () => {
573
+ const otherRowId: ObjectID = new ObjectID(
574
+ "4c1e0c8e-5555-4555-8555-555555555555",
575
+ );
576
+
577
+ service.row = buildLiveRow({
578
+ verificationCode: VerificationCode.hashCode({
579
+ code: "123456",
580
+ channelId: otherRowId,
581
+ }),
582
+ });
583
+
584
+ expect((await verify(service, "123456")).outcome).toBe(
585
+ ChannelVerificationOutcome.IncorrectCode,
586
+ );
587
+ });
588
+
589
+ it("compares codes as strings, so a numeric body still works", async () => {
590
+ const result: ChannelVerificationResult =
591
+ await ChannelVerification.verifyCode({
592
+ service: asService(service),
593
+ itemId: ITEM_ID,
594
+ userId: OWNER_ID,
595
+ code: 123456 as unknown as string,
596
+ });
597
+
598
+ expect(result.outcome).toBe(ChannelVerificationOutcome.Verified);
599
+ });
600
+
601
+ it("refuses an empty stored digest rather than matching an empty code", async () => {
602
+ service.row = buildLiveRow({ verificationCode: "" });
603
+
604
+ expect((await verify(service, "")).outcome).toBe(
605
+ ChannelVerificationOutcome.IncorrectCode,
606
+ );
607
+ });
608
+ });
609
+ });
610
+
611
+ describe("getFailureException", () => {
612
+ it("says nothing about whether a wrong code was close", () => {
613
+ expect(
614
+ ChannelVerification.getFailureException(
615
+ ChannelVerificationOutcome.IncorrectCode,
616
+ ).message,
617
+ ).toBe("Invalid code");
618
+ });
619
+
620
+ it("tells a user with an expired or exhausted code to ask for a new one", () => {
621
+ expect(
622
+ ChannelVerification.getFailureException(
623
+ ChannelVerificationOutcome.Expired,
624
+ ).message,
625
+ ).toContain("request a new code");
626
+
627
+ expect(
628
+ ChannelVerification.getFailureException(
629
+ ChannelVerificationOutcome.TooManyAttempts,
630
+ ).message,
631
+ ).toContain("request a new code");
632
+ });
633
+
634
+ it("keeps the original wording for the two pre-existing failures", () => {
635
+ expect(
636
+ ChannelVerification.getFailureException(
637
+ ChannelVerificationOutcome.NotFound,
638
+ ).message,
639
+ ).toBe("Item not found");
640
+
641
+ expect(
642
+ ChannelVerification.getFailureException(
643
+ ChannelVerificationOutcome.NotOwner,
644
+ ).message,
645
+ ).toBe("Invalid user ID");
646
+ });
647
+ });
648
+
649
+ /*
650
+ * The end-to-end shape of the attack in the advisory, run against the new
651
+ * code: create a row for somebody else's address and walk the code space.
652
+ */
653
+ describe("brute force", () => {
654
+ it("cannot be walked to a verification within one issued code", async () => {
655
+ const plainCode: string = await ChannelVerification.issueCodeOnItem({
656
+ service: asService(service),
657
+ itemId: ITEM_ID,
658
+ });
659
+
660
+ let verified: boolean = false;
661
+
662
+ for (let guess: number = 0; guess < 1000; guess++) {
663
+ const candidate: string = guess.toString().padStart(6, "0");
664
+
665
+ if (candidate === plainCode) {
666
+ continue;
667
+ }
668
+
669
+ const result: ChannelVerificationResult = await verify(
670
+ service,
671
+ candidate,
672
+ );
673
+
674
+ if (result.outcome === ChannelVerificationOutcome.Verified) {
675
+ verified = true;
676
+ break;
677
+ }
678
+ }
679
+
680
+ expect(verified).toBe(false);
681
+ expect(service.row?.isVerified).toBeFalsy();
682
+
683
+ /* And the real code is dead too, so the attacker gained nothing. */
684
+ expect((await verify(service, plainCode)).outcome).toBe(
685
+ ChannelVerificationOutcome.TooManyAttempts,
686
+ );
687
+ });
688
+ });
689
+ });