@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,322 @@
1
+ import EmailVerificationToken from "../../../Models/DatabaseModels/EmailVerificationToken";
2
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
3
+ import EmailVerificationTokenService from "../../../Server/Services/EmailVerificationTokenService";
4
+ import UserRegistrationToken, {
5
+ REGISTRATION_TOKEN_EXPIRY_IN_DAYS,
6
+ } from "../../../Server/Utils/UserRegistrationToken";
7
+ import Hostname from "../../../Types/API/Hostname";
8
+ import Protocol from "../../../Types/API/Protocol";
9
+ import URL from "../../../Types/API/URL";
10
+ import OneUptimeDate from "../../../Types/Date";
11
+ import Email from "../../../Types/Email";
12
+ import ObjectID from "../../../Types/ObjectID";
13
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
14
+
15
+ /*
16
+ * The gate on claiming an invited account (GHSA-qg84-6hrg-mr5g).
17
+ *
18
+ * An invited user's row exists, with no password, before they have ever
19
+ * authenticated. Whoever can spend one of these tokens gets to put a password
20
+ * on that row and walk in, so the interesting cases here are all the ways a
21
+ * token must NOT be spendable: not by a different address, not after it has
22
+ * expired, and not twice.
23
+ */
24
+
25
+ const USER_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
26
+ const TOKEN_ROW_ID: ObjectID = new ObjectID(
27
+ "22222222-2222-4222-8222-222222222222",
28
+ );
29
+ const TOKEN: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
30
+
31
+ const INVITED_EMAIL: Email = new Email("alice@company.com");
32
+ const OTHER_EMAIL: Email = new Email("mallory@company.com");
33
+
34
+ let createSpy: jest.SpyInstance;
35
+ let findOneBySpy: jest.SpyInstance;
36
+ let deleteOneBySpy: jest.SpyInstance;
37
+
38
+ /*
39
+ * A live, valid token row for INVITED_EMAIL.
40
+ *
41
+ * Overrides are loosely typed so a test can put a column back to undefined --
42
+ * a half-written row read out of the database. exactOptionalPropertyTypes
43
+ * forbids expressing that through Partial<EmailVerificationToken>, and those
44
+ * are exactly the rows consumeRegistrationToken has to refuse.
45
+ */
46
+ function tokenRow(
47
+ overrides: Record<string, unknown> = {},
48
+ ): EmailVerificationToken {
49
+ const row: EmailVerificationToken = new EmailVerificationToken(TOKEN_ROW_ID);
50
+ row._id = TOKEN_ROW_ID.toString();
51
+ row.email = INVITED_EMAIL;
52
+ row.expires = OneUptimeDate.getSomeDaysAfter(1);
53
+
54
+ return Object.assign(row, overrides);
55
+ }
56
+
57
+ beforeEach(() => {
58
+ createSpy = jest
59
+ .spyOn(EmailVerificationTokenService, "create")
60
+ .mockResolvedValue(new EmailVerificationToken());
61
+
62
+ findOneBySpy = jest
63
+ .spyOn(EmailVerificationTokenService, "findOneBy")
64
+ .mockResolvedValue(tokenRow());
65
+
66
+ deleteOneBySpy = jest
67
+ .spyOn(EmailVerificationTokenService, "deleteOneBy")
68
+ .mockResolvedValue(1);
69
+
70
+ jest
71
+ .spyOn(DatabaseConfig, "getHost")
72
+ .mockResolvedValue(new Hostname("oneuptime.test"));
73
+ jest
74
+ .spyOn(DatabaseConfig, "getHttpProtocol")
75
+ .mockResolvedValue(Protocol.HTTPS);
76
+ });
77
+
78
+ afterEach(() => {
79
+ jest.restoreAllMocks();
80
+ });
81
+
82
+ describe("UserRegistrationToken.generateRegistrationToken", () => {
83
+ test("binds the token to the user and the address it will be mailed to", async () => {
84
+ const token: ObjectID =
85
+ await UserRegistrationToken.generateRegistrationToken({
86
+ userId: USER_ID,
87
+ email: INVITED_EMAIL,
88
+ });
89
+
90
+ const created: EmailVerificationToken = createSpy.mock.calls[0]![0][
91
+ "data"
92
+ ] as EmailVerificationToken;
93
+
94
+ expect(created.userId!.toString()).toBe(USER_ID.toString());
95
+ expect(created.email!.toString()).toBe(INVITED_EMAIL.toString());
96
+ expect(created.token!.toString()).toBe(token.toString());
97
+ });
98
+
99
+ test("mints a fresh unguessable token each time", async () => {
100
+ const first: ObjectID =
101
+ await UserRegistrationToken.generateRegistrationToken({
102
+ userId: USER_ID,
103
+ email: INVITED_EMAIL,
104
+ });
105
+ const second: ObjectID =
106
+ await UserRegistrationToken.generateRegistrationToken({
107
+ userId: USER_ID,
108
+ email: INVITED_EMAIL,
109
+ });
110
+
111
+ expect(first.toString()).not.toBe(second.toString());
112
+ expect(ObjectID.isValidUUID(first.toString())).toBe(true);
113
+ });
114
+
115
+ test("expires, and outlives a plain verification link", async () => {
116
+ /*
117
+ * Invitations get read when the person gets round to them, so these last
118
+ * days rather than the 24 hours a verification link gets. They still have
119
+ * to expire -- an invitation mail sitting in an old inbox forever is a
120
+ * standing key to the account.
121
+ */
122
+ await UserRegistrationToken.generateRegistrationToken({
123
+ userId: USER_ID,
124
+ email: INVITED_EMAIL,
125
+ });
126
+
127
+ const created: EmailVerificationToken = createSpy.mock.calls[0]![0][
128
+ "data"
129
+ ] as EmailVerificationToken;
130
+
131
+ /*
132
+ * The source stamps expires = now + N days, then a few milliseconds later
133
+ * this test reads the clock again to measure the gap. Measuring in whole
134
+ * days would truncate that sub-day drift down to N-1 (moment's diff rounds
135
+ * toward zero), which is a timing race that fails under CI load. Measure the
136
+ * raw gap in milliseconds and assert it lands in the (N-1, N]-day band: at
137
+ * most the configured N days, and strictly more than N-1. That one-day lower
138
+ * tolerance absorbs both the execution time between the two clock reads and
139
+ * a DST hour (moment's calendar add is timezone-aware), while still pinning
140
+ * the lifetime to exactly N days — it cannot be confused with N-1 or N+1.
141
+ */
142
+ const oneDayMs: number = 24 * 60 * 60 * 1000;
143
+ const msOut: number =
144
+ created.expires!.getTime() - OneUptimeDate.getCurrentDate().getTime();
145
+ const expectedMs: number = REGISTRATION_TOKEN_EXPIRY_IN_DAYS * oneDayMs;
146
+
147
+ expect(msOut).toBeLessThanOrEqual(expectedMs);
148
+ expect(msOut).toBeGreaterThan(expectedMs - oneDayMs);
149
+
150
+ // It lasts days, not the 24 hours a plain verification link gets.
151
+ expect(REGISTRATION_TOKEN_EXPIRY_IN_DAYS).toBeGreaterThan(1);
152
+ });
153
+ });
154
+
155
+ describe("UserRegistrationToken.getRegistrationLink", () => {
156
+ test("points at the register page and carries both the address and the token", async () => {
157
+ const link: URL = await UserRegistrationToken.getRegistrationLink({
158
+ email: INVITED_EMAIL,
159
+ token: TOKEN,
160
+ });
161
+
162
+ const asString: string = link.toString();
163
+
164
+ expect(asString).toContain("/accounts/register");
165
+ expect(asString).toContain(encodeURIComponent(INVITED_EMAIL.toString()));
166
+ expect(asString).toContain(TOKEN.toString());
167
+ });
168
+
169
+ test("generateRegistrationLink mints a token and puts that same token on the link", async () => {
170
+ const link: URL = await UserRegistrationToken.generateRegistrationLink({
171
+ userId: USER_ID,
172
+ email: INVITED_EMAIL,
173
+ });
174
+
175
+ const created: EmailVerificationToken = createSpy.mock.calls[0]![0][
176
+ "data"
177
+ ] as EmailVerificationToken;
178
+
179
+ expect(link.toString()).toContain(created.token!.toString());
180
+ });
181
+ });
182
+
183
+ describe("UserRegistrationToken.consumeRegistrationToken", () => {
184
+ test("accepts a live token minted for this address", async () => {
185
+ const accepted: boolean =
186
+ await UserRegistrationToken.consumeRegistrationToken({
187
+ token: TOKEN,
188
+ email: INVITED_EMAIL,
189
+ });
190
+
191
+ expect(accepted).toBe(true);
192
+ });
193
+
194
+ test("spends it, so it cannot be replayed", async () => {
195
+ await UserRegistrationToken.consumeRegistrationToken({
196
+ token: TOKEN,
197
+ email: INVITED_EMAIL,
198
+ });
199
+
200
+ expect(deleteOneBySpy).toHaveBeenCalledTimes(1);
201
+
202
+ const query: Record<string, any> = deleteOneBySpy.mock.calls[0]![0][
203
+ "query"
204
+ ] as Record<string, any>;
205
+
206
+ expect(query["_id"].toString()).toBe(TOKEN_ROW_ID.toString());
207
+ });
208
+
209
+ test("refuses a token that does not exist", async () => {
210
+ findOneBySpy.mockResolvedValue(null);
211
+
212
+ const accepted: boolean =
213
+ await UserRegistrationToken.consumeRegistrationToken({
214
+ token: TOKEN,
215
+ email: INVITED_EMAIL,
216
+ });
217
+
218
+ expect(accepted).toBe(false);
219
+ expect(deleteOneBySpy).not.toHaveBeenCalled();
220
+ });
221
+
222
+ test("refuses a token minted for somebody else's address", async () => {
223
+ /*
224
+ * The cross-invitation case. Mallory has a real invitation of her own, so
225
+ * she holds a real live token -- it just is not for Alice's address, and
226
+ * without this check it would open Alice's unclaimed account.
227
+ */
228
+ const accepted: boolean =
229
+ await UserRegistrationToken.consumeRegistrationToken({
230
+ token: TOKEN,
231
+ email: OTHER_EMAIL,
232
+ });
233
+
234
+ expect(accepted).toBe(false);
235
+ expect(deleteOneBySpy).not.toHaveBeenCalled();
236
+ });
237
+
238
+ test("matches the address case-insensitively", async () => {
239
+ /*
240
+ * Email lowercases on construction, so an invitation sent to Alice@ and a
241
+ * form filled in as alice@ are the same mailbox and must not be refused.
242
+ */
243
+ const accepted: boolean =
244
+ await UserRegistrationToken.consumeRegistrationToken({
245
+ token: TOKEN,
246
+ email: new Email("ALICE@company.com"),
247
+ });
248
+
249
+ expect(accepted).toBe(true);
250
+ });
251
+
252
+ test("refuses an expired token", async () => {
253
+ findOneBySpy.mockResolvedValue(
254
+ tokenRow({ expires: OneUptimeDate.getSomeDaysAgo(1) }),
255
+ );
256
+
257
+ const accepted: boolean =
258
+ await UserRegistrationToken.consumeRegistrationToken({
259
+ token: TOKEN,
260
+ email: INVITED_EMAIL,
261
+ });
262
+
263
+ expect(accepted).toBe(false);
264
+ expect(deleteOneBySpy).not.toHaveBeenCalled();
265
+ });
266
+
267
+ test("refuses a row with no expiry rather than treating it as immortal", async () => {
268
+ findOneBySpy.mockResolvedValue(tokenRow({ expires: undefined }));
269
+
270
+ const accepted: boolean =
271
+ await UserRegistrationToken.consumeRegistrationToken({
272
+ token: TOKEN,
273
+ email: INVITED_EMAIL,
274
+ });
275
+
276
+ expect(accepted).toBe(false);
277
+ expect(deleteOneBySpy).not.toHaveBeenCalled();
278
+ });
279
+
280
+ test("refuses a row with no address rather than treating it as a match", async () => {
281
+ findOneBySpy.mockResolvedValue(tokenRow({ email: undefined }));
282
+
283
+ const accepted: boolean =
284
+ await UserRegistrationToken.consumeRegistrationToken({
285
+ token: TOKEN,
286
+ email: INVITED_EMAIL,
287
+ });
288
+
289
+ expect(accepted).toBe(false);
290
+ expect(deleteOneBySpy).not.toHaveBeenCalled();
291
+ });
292
+
293
+ test("loses the race gracefully when another request spent it first", async () => {
294
+ /*
295
+ * Two requests can both read a live token before either deletes it. The
296
+ * delete is the tiebreak: whoever removes the row won, and the one that
297
+ * removed nothing is told no.
298
+ */
299
+ deleteOneBySpy.mockResolvedValue(0);
300
+
301
+ const accepted: boolean =
302
+ await UserRegistrationToken.consumeRegistrationToken({
303
+ token: TOKEN,
304
+ email: INVITED_EMAIL,
305
+ });
306
+
307
+ expect(accepted).toBe(false);
308
+ });
309
+
310
+ test("looks the token up by the token itself", async () => {
311
+ await UserRegistrationToken.consumeRegistrationToken({
312
+ token: TOKEN,
313
+ email: INVITED_EMAIL,
314
+ });
315
+
316
+ const query: Record<string, any> = findOneBySpy.mock.calls[0]![0][
317
+ "query"
318
+ ] as Record<string, any>;
319
+
320
+ expect(query["token"].toString()).toBe(TOKEN.toString());
321
+ });
322
+ });
@@ -0,0 +1,296 @@
1
+ import VerificationCode, {
2
+ VERIFICATION_CODE_LENGTH,
3
+ } from "../../../Server/Utils/VerificationCode";
4
+ import ObjectID from "../../../Types/ObjectID";
5
+ import { describe, expect, it } from "@jest/globals";
6
+ import crypto from "crypto";
7
+
8
+ /*
9
+ * GHSA-5cr8-vph4-3hrf.
10
+ *
11
+ * The cryptography under notification-channel verification: a code drawn from
12
+ * a real CSPRNG, stored only as a keyed digest that is domain-separated by the
13
+ * row it belongs to, and compared in constant time.
14
+ */
15
+ describe("VerificationCode", () => {
16
+ describe("generate", () => {
17
+ it("produces a six digit code by default", () => {
18
+ const code: string = VerificationCode.generate();
19
+
20
+ expect(code).toHaveLength(VERIFICATION_CODE_LENGTH);
21
+ expect(code).toMatch(/^[0-9]{6}$/);
22
+ });
23
+
24
+ it("honours an explicit length", () => {
25
+ expect(VerificationCode.generate(8)).toMatch(/^[0-9]{8}$/);
26
+ expect(VerificationCode.generate(4)).toMatch(/^[0-9]{4}$/);
27
+ });
28
+
29
+ /*
30
+ * The whole point of this module existing separately from
31
+ * Common/Types/Text: Text has to tolerate a runtime without a CSPRNG
32
+ * because it is bundled into the browser, and falls back to Math.random
33
+ * there. A code minted on the server must never take that path.
34
+ */
35
+ it("never falls back to Math.random", () => {
36
+ const mathRandomSpy: jest.SpyInstance = jest.spyOn(Math, "random");
37
+
38
+ try {
39
+ for (let i: number = 0; i < 200; i++) {
40
+ VerificationCode.generate();
41
+ }
42
+
43
+ expect(mathRandomSpy).not.toHaveBeenCalled();
44
+ } finally {
45
+ mathRandomSpy.mockRestore();
46
+ }
47
+ });
48
+
49
+ it("draws from crypto.randomInt", () => {
50
+ const randomIntSpy: jest.SpyInstance = jest.spyOn(crypto, "randomInt");
51
+
52
+ try {
53
+ VerificationCode.generate(6);
54
+
55
+ expect(randomIntSpy).toHaveBeenCalledTimes(6);
56
+ expect(randomIntSpy).toHaveBeenCalledWith(0, 10);
57
+ } finally {
58
+ randomIntSpy.mockRestore();
59
+ }
60
+ });
61
+
62
+ it("keeps leading zeros, so the code space really is 10^6", () => {
63
+ let sawLeadingZero: boolean = false;
64
+
65
+ for (let i: number = 0; i < 500; i++) {
66
+ if (VerificationCode.generate().startsWith("0")) {
67
+ sawLeadingZero = true;
68
+ break;
69
+ }
70
+ }
71
+
72
+ expect(sawLeadingZero).toBe(true);
73
+ });
74
+
75
+ it("spreads across the space rather than repeating itself", () => {
76
+ const seen: Set<string> = new Set<string>();
77
+
78
+ for (let i: number = 0; i < 500; i++) {
79
+ seen.add(VerificationCode.generate());
80
+ }
81
+
82
+ /*
83
+ * 500 draws from 10^6 values: the birthday bound puts the expected
84
+ * number of collisions at well under one, so anything below 490 means
85
+ * the generator is not spreading.
86
+ */
87
+ expect(seen.size).toBeGreaterThan(490);
88
+ });
89
+
90
+ it("uses every digit in every position", () => {
91
+ const perPosition: Array<Set<string>> = [
92
+ new Set<string>(),
93
+ new Set<string>(),
94
+ new Set<string>(),
95
+ new Set<string>(),
96
+ new Set<string>(),
97
+ new Set<string>(),
98
+ ];
99
+
100
+ for (let i: number = 0; i < 2000; i++) {
101
+ const code: string = VerificationCode.generate();
102
+
103
+ for (let position: number = 0; position < 6; position++) {
104
+ (perPosition[position] as Set<string>).add(code.charAt(position));
105
+ }
106
+ }
107
+
108
+ for (const position of perPosition) {
109
+ expect(position.size).toBe(10);
110
+ }
111
+ });
112
+ });
113
+
114
+ describe("hashCode", () => {
115
+ const channelId: ObjectID = new ObjectID(
116
+ "8f7c1a1a-4bb0-4f0d-9f01-1c0d0d0d0d01",
117
+ );
118
+
119
+ it("returns a 64 character hex digest", () => {
120
+ expect(VerificationCode.hashCode({ code: "123456", channelId })).toMatch(
121
+ /^[0-9a-f]{64}$/,
122
+ );
123
+ });
124
+
125
+ it("is deterministic for the same code and row", () => {
126
+ expect(VerificationCode.hashCode({ code: "123456", channelId })).toBe(
127
+ VerificationCode.hashCode({ code: "123456", channelId }),
128
+ );
129
+ });
130
+
131
+ it("never returns the code itself", () => {
132
+ const digest: string = VerificationCode.hashCode({
133
+ code: "123456",
134
+ channelId,
135
+ });
136
+
137
+ expect(digest).not.toBe("123456");
138
+ expect(digest).not.toContain("123456");
139
+ });
140
+
141
+ it("changes completely when a single digit changes", () => {
142
+ const a: string = VerificationCode.hashCode({
143
+ code: "123456",
144
+ channelId,
145
+ });
146
+ const b: string = VerificationCode.hashCode({
147
+ code: "123457",
148
+ channelId,
149
+ });
150
+
151
+ expect(a).not.toBe(b);
152
+ });
153
+
154
+ /*
155
+ * Domain separation. Without the row id in the message, one precomputed
156
+ * table of 10^6 digests would invert every row in the table at once, and
157
+ * two rows that happened to draw the same code would be visibly equal to
158
+ * anybody holding a database dump.
159
+ */
160
+ it("gives the same code different digests on different rows", () => {
161
+ const otherChannelId: ObjectID = new ObjectID(
162
+ "8f7c1a1a-4bb0-4f0d-9f01-1c0d0d0d0d02",
163
+ );
164
+
165
+ expect(VerificationCode.hashCode({ code: "123456", channelId })).not.toBe(
166
+ VerificationCode.hashCode({
167
+ code: "123456",
168
+ channelId: otherChannelId,
169
+ }),
170
+ );
171
+ });
172
+
173
+ /*
174
+ * Length-prefixing. Without it, ("ab", "cde") and ("abc", "de") would
175
+ * concatenate to the same message — the id and code fields could be slid
176
+ * past each other to forge a digest for a different pair.
177
+ */
178
+ it("cannot be confused by moving a character between the id and the code", () => {
179
+ const first: string = VerificationCode.hashCode({
180
+ code: "3456",
181
+ channelId: new ObjectID("ab12"),
182
+ });
183
+ const second: string = VerificationCode.hashCode({
184
+ code: "23456",
185
+ channelId: new ObjectID("ab1"),
186
+ });
187
+
188
+ expect(first).not.toBe(second);
189
+ });
190
+
191
+ it("is keyed, not a bare digest of its inputs", () => {
192
+ const digest: string = VerificationCode.hashCode({
193
+ code: "123456",
194
+ channelId,
195
+ });
196
+
197
+ const unkeyed: string = crypto
198
+ .createHash("sha256")
199
+ .update(`${channelId.toString()}123456`)
200
+ .digest("hex");
201
+
202
+ expect(digest).not.toBe(unkeyed);
203
+ });
204
+ });
205
+
206
+ describe("isHashEqual", () => {
207
+ it("accepts identical digests", () => {
208
+ const digest: string = VerificationCode.hashCode({
209
+ code: "123456",
210
+ channelId: new ObjectID("row-1"),
211
+ });
212
+
213
+ expect(VerificationCode.isHashEqual(digest, digest)).toBe(true);
214
+ });
215
+
216
+ it("rejects different digests", () => {
217
+ const channelId: ObjectID = new ObjectID("row-1");
218
+
219
+ expect(
220
+ VerificationCode.isHashEqual(
221
+ VerificationCode.hashCode({ code: "123456", channelId }),
222
+ VerificationCode.hashCode({ code: "654321", channelId }),
223
+ ),
224
+ ).toBe(false);
225
+ });
226
+
227
+ it("rejects empty and mismatched-length values without throwing", () => {
228
+ /*
229
+ * timingSafeEqual throws on differing lengths, so the guard in front of
230
+ * it is load-bearing rather than cosmetic.
231
+ */
232
+ expect(VerificationCode.isHashEqual("", "")).toBe(false);
233
+ expect(VerificationCode.isHashEqual("abc", "")).toBe(false);
234
+ expect(VerificationCode.isHashEqual("", "abc")).toBe(false);
235
+ expect(VerificationCode.isHashEqual("abc", "abcd")).toBe(false);
236
+ });
237
+
238
+ it("compares with a constant-time primitive rather than ===", () => {
239
+ const timingSafeEqualSpy: jest.SpyInstance = jest.spyOn(
240
+ crypto,
241
+ "timingSafeEqual",
242
+ );
243
+
244
+ try {
245
+ const digest: string = VerificationCode.hashCode({
246
+ code: "123456",
247
+ channelId: new ObjectID("row-1"),
248
+ });
249
+
250
+ VerificationCode.isHashEqual(digest, digest);
251
+
252
+ expect(timingSafeEqualSpy).toHaveBeenCalled();
253
+ } finally {
254
+ timingSafeEqualSpy.mockRestore();
255
+ }
256
+ });
257
+ });
258
+
259
+ describe("generateUnusableHash", () => {
260
+ it("looks like a digest, so a stored value gives nothing away", () => {
261
+ expect(VerificationCode.generateUnusableHash()).toMatch(/^[0-9a-f]{64}$/);
262
+ });
263
+
264
+ it("is different every time", () => {
265
+ const seen: Set<string> = new Set<string>();
266
+
267
+ for (let i: number = 0; i < 100; i++) {
268
+ seen.add(VerificationCode.generateUnusableHash());
269
+ }
270
+
271
+ expect(seen.size).toBe(100);
272
+ });
273
+
274
+ /*
275
+ * The property that makes it a safe "no live code" marker: no six-digit
276
+ * code hashes to it. Exhausting 10^6 codes per assertion is not something
277
+ * a unit test can do, so this samples — the real guarantee is that the
278
+ * value comes from a 256-bit space the 10^6 digests cannot cover.
279
+ */
280
+ it("is not the digest of any code that happens to be tried against it", () => {
281
+ const channelId: ObjectID = new ObjectID("row-1");
282
+ const unusable: string = VerificationCode.generateUnusableHash();
283
+
284
+ for (let i: number = 0; i < 2000; i++) {
285
+ const code: string = i.toString().padStart(6, "0");
286
+
287
+ expect(
288
+ VerificationCode.isHashEqual(
289
+ VerificationCode.hashCode({ code, channelId }),
290
+ unusable,
291
+ ),
292
+ ).toBe(false);
293
+ }
294
+ });
295
+ });
296
+ });