@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,948 @@
1
+ import UserNotificationMethodAdminService, {
2
+ AdminNotificationMethodView,
3
+ } from "../../../Server/Services/UserNotificationMethodAdminService";
4
+ import OnCallReadinessService, {
5
+ IDENTIFIER_MASK,
6
+ ReadinessMethodType,
7
+ } from "../../../Server/Services/OnCallReadinessService";
8
+ import MailService from "../../../Server/Services/MailService";
9
+ import ProjectService from "../../../Server/Services/ProjectService";
10
+ import TeamMemberService from "../../../Server/Services/TeamMemberService";
11
+ import UserCallService from "../../../Server/Services/UserCallService";
12
+ import UserEmailService from "../../../Server/Services/UserEmailService";
13
+ import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
14
+ import UserPushService from "../../../Server/Services/UserPushService";
15
+ import UserService from "../../../Server/Services/UserService";
16
+ import UserSmsService from "../../../Server/Services/UserSmsService";
17
+ import UserTelegramService from "../../../Server/Services/UserTelegramService";
18
+ import UserWebhookService from "../../../Server/Services/UserWebhookService";
19
+ import UserWhatsAppService from "../../../Server/Services/UserWhatsAppService";
20
+ import Project from "../../../Models/DatabaseModels/Project";
21
+ import User from "../../../Models/DatabaseModels/User";
22
+ import UserCall from "../../../Models/DatabaseModels/UserCall";
23
+ import UserEmail from "../../../Models/DatabaseModels/UserEmail";
24
+ import UserPush from "../../../Models/DatabaseModels/UserPush";
25
+ import UserSMS from "../../../Models/DatabaseModels/UserSMS";
26
+ import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
27
+ import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
28
+ import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
29
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
30
+ import Email from "../../../Types/Email";
31
+ import BadDataException from "../../../Types/Exception/BadDataException";
32
+ import Name from "../../../Types/Name";
33
+ import ObjectID from "../../../Types/ObjectID";
34
+ import Phone from "../../../Types/Phone";
35
+ import { beforeEach, describe, expect, test } from "@jest/globals";
36
+
37
+ /*
38
+ * The service that lets a project administrator set up somebody else's
39
+ * notification methods.
40
+ *
41
+ * Everything here is tested against the REAL masking, not a stub of it. That is
42
+ * the whole point of testing this layer separately from the router: the claim
43
+ * is that a raw phone number sitting in the database cannot come back out of
44
+ * this service, and the only way to test that claim is to put a raw phone
45
+ * number in one end and read what comes out of the other. A masking test that
46
+ * stubbed the masker proves nothing.
47
+ *
48
+ * The properties this file exists to pin, in the order they matter:
49
+ *
50
+ * 1. AN ADMIN-ADDED METHOD IS NOT LIVE. The row is written with the TARGET's
51
+ * user id and with isVerified untouched, which is what makes the method
52
+ * service send a verification code to the address itself. Nothing in this
53
+ * service can mark a method verified, and neither can the administrator
54
+ * afterwards — the verify endpoints compare the row's owner against the
55
+ * signed-in caller. If a future edit sets isVerified, or writes the
56
+ * ACTOR's user id instead of the target's, an administrator's own phone
57
+ * becomes a live delivery address on a colleague's account. Both are
58
+ * asserted on the row that is actually handed to the create.
59
+ *
60
+ * 2. NOTHING RAW COMES BACK. Every view this service returns goes through one
61
+ * function, and these tests scan the whole returned structure for the raw
62
+ * values they planted rather than checking a field by name.
63
+ *
64
+ * 3. THE OWNER IS ALWAYS TOLD. An admin adding a colleague's work phone and
65
+ * an attacker adding a number of their own are the same request from the
66
+ * server's side. The only party who always knows which it was is the
67
+ * account holder, so the mail is not decoration — it is the control.
68
+ *
69
+ * 4. MEMBERSHIP IS A CLAIM ABOUT A PROJECT. Holding an administrative
70
+ * permission somewhere is not a licence to write for a user id anywhere.
71
+ *
72
+ * 5. THE THREE UNADDABLE CHANNELS STAY UNADDABLE. Push has no value to type,
73
+ * Telegram needs the account holder to talk to the bot, and a webhook
74
+ * would be live the instant it was written because UserWebhook has no
75
+ * verification at all.
76
+ */
77
+
78
+ const RAW_EMAIL: string = "jane.ops@example.com";
79
+ const RAW_PHONE: string = "+14155554821";
80
+ const RAW_TELEGRAM_HANDLE: string = "@janeops_oncall";
81
+ const RAW_WEBHOOK_NAME: string = "payments-hook";
82
+ const RAW_DEVICE_NAME: string = "Jane's iPhone";
83
+
84
+ const ALL_RAW_VALUES: Array<string> = [
85
+ RAW_EMAIL,
86
+ RAW_PHONE,
87
+ RAW_TELEGRAM_HANDLE,
88
+ RAW_WEBHOOK_NAME,
89
+ RAW_DEVICE_NAME,
90
+ ];
91
+
92
+ let projectId: ObjectID;
93
+ let targetUserId: ObjectID;
94
+ let actorUserId: ObjectID;
95
+ let emailMethodId: ObjectID;
96
+ let smsMethodId: ObjectID;
97
+
98
+ let teamMemberFindOneBy: jest.SpyInstance;
99
+ let mailSpy: jest.SpyInstance;
100
+ let clearCacheSpy: jest.SpyInstance;
101
+
102
+ let emailFindBy: jest.SpyInstance;
103
+ let smsFindBy: jest.SpyInstance;
104
+ let callFindBy: jest.SpyInstance;
105
+ let whatsAppFindBy: jest.SpyInstance;
106
+ let pushFindBy: jest.SpyInstance;
107
+ let telegramFindBy: jest.SpyInstance;
108
+ let webhookFindBy: jest.SpyInstance;
109
+
110
+ let emailCreate: jest.SpyInstance;
111
+ let smsCreate: jest.SpyInstance;
112
+ let smsFindOneBy: jest.SpyInstance;
113
+ let smsDelete: jest.SpyInstance;
114
+ let smsResend: jest.SpyInstance;
115
+ let deletionImpact: jest.SpyInstance;
116
+
117
+ function props(): DatabaseCommonInteractionProps {
118
+ return {
119
+ tenantId: projectId,
120
+ userId: actorUserId,
121
+ };
122
+ }
123
+
124
+ /* Every string anywhere in a returned structure, however deeply nested. */
125
+ function everyStringIn(value: unknown): Array<string> {
126
+ if (typeof value === "string") {
127
+ return [value];
128
+ }
129
+
130
+ if (value instanceof ObjectID || value instanceof Date) {
131
+ return [value.toString()];
132
+ }
133
+
134
+ if (Array.isArray(value)) {
135
+ return value.flatMap(everyStringIn);
136
+ }
137
+
138
+ if (value && typeof value === "object") {
139
+ return Object.values(value as Record<string, unknown>).flatMap(
140
+ everyStringIn,
141
+ );
142
+ }
143
+
144
+ return [];
145
+ }
146
+
147
+ function expectNothingRawIn(value: unknown): void {
148
+ const strings: string = everyStringIn(value).join(" || ");
149
+
150
+ for (const raw of ALL_RAW_VALUES) {
151
+ expect(strings).not.toContain(raw);
152
+ }
153
+ }
154
+
155
+ function buildEmailRow(): UserEmail {
156
+ const row: UserEmail = new UserEmail();
157
+ row._id = emailMethodId.toString();
158
+ row.projectId = projectId;
159
+ row.userId = targetUserId;
160
+ row.email = new Email(RAW_EMAIL);
161
+ row.isVerified = true;
162
+ return row;
163
+ }
164
+
165
+ function buildSmsRow(): UserSMS {
166
+ const row: UserSMS = new UserSMS();
167
+ row._id = smsMethodId.toString();
168
+ row.projectId = projectId;
169
+ row.userId = targetUserId;
170
+ row.phone = new Phone(RAW_PHONE);
171
+ row.isVerified = false;
172
+ return row;
173
+ }
174
+
175
+ function buildTelegramRow(): UserTelegram {
176
+ const row: UserTelegram = new UserTelegram();
177
+ row._id = ObjectID.generate().toString();
178
+ row.projectId = projectId;
179
+ row.userId = targetUserId;
180
+ row.telegramUserHandle = RAW_TELEGRAM_HANDLE;
181
+ row.isVerified = true;
182
+ return row;
183
+ }
184
+
185
+ function buildWebhookRow(): UserWebhook {
186
+ const row: UserWebhook = new UserWebhook();
187
+ row._id = ObjectID.generate().toString();
188
+ row.projectId = projectId;
189
+ row.userId = targetUserId;
190
+ row.name = RAW_WEBHOOK_NAME;
191
+ return row;
192
+ }
193
+
194
+ function buildPushRow(): UserPush {
195
+ const row: UserPush = new UserPush();
196
+ row._id = ObjectID.generate().toString();
197
+ row.projectId = projectId;
198
+ row.userId = targetUserId;
199
+ row.deviceName = RAW_DEVICE_NAME;
200
+ row.isVerified = true;
201
+ return row;
202
+ }
203
+
204
+ function viewFor(
205
+ methods: Array<AdminNotificationMethodView>,
206
+ methodType: ReadinessMethodType,
207
+ ): AdminNotificationMethodView {
208
+ const view: AdminNotificationMethodView | undefined = methods.find(
209
+ (candidate: AdminNotificationMethodView): boolean => {
210
+ return candidate.methodType === methodType;
211
+ },
212
+ );
213
+
214
+ if (!view) {
215
+ throw new Error(`no view for ${methodType}`);
216
+ }
217
+
218
+ return view;
219
+ }
220
+
221
+ beforeEach(() => {
222
+ jest.restoreAllMocks();
223
+ jest.clearAllMocks();
224
+
225
+ projectId = ObjectID.generate();
226
+ targetUserId = ObjectID.generate();
227
+ actorUserId = ObjectID.generate();
228
+ emailMethodId = ObjectID.generate();
229
+ smsMethodId = ObjectID.generate();
230
+
231
+ teamMemberFindOneBy = jest
232
+ .spyOn(TeamMemberService, "findOneBy")
233
+ .mockResolvedValue({ _id: ObjectID.generate().toString() } as never);
234
+
235
+ mailSpy = jest
236
+ .spyOn(MailService, "sendMail")
237
+ .mockResolvedValue(undefined as never);
238
+
239
+ clearCacheSpy = jest
240
+ .spyOn(OnCallReadinessService, "clearCache")
241
+ .mockImplementation((): void => {
242
+ return undefined;
243
+ });
244
+
245
+ const owner: User = new User();
246
+ owner._id = targetUserId.toString();
247
+ owner.name = new Name("Jane Ops");
248
+ owner.email = new Email("jane.login@example.com");
249
+
250
+ const actor: User = new User();
251
+ actor._id = actorUserId.toString();
252
+ actor.name = new Name("Alex Admin");
253
+ actor.email = new Email("alex.admin@example.com");
254
+
255
+ jest
256
+ .spyOn(UserService, "findOneById")
257
+ .mockImplementation((data: any): Promise<User | null> => {
258
+ return Promise.resolve(
259
+ data.id.toString() === targetUserId.toString() ? owner : actor,
260
+ );
261
+ });
262
+
263
+ const project: Project = new Project();
264
+ project._id = projectId.toString();
265
+ project.name = "Payments";
266
+
267
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue(project as never);
268
+
269
+ emailFindBy = jest
270
+ .spyOn(UserEmailService, "findBy")
271
+ .mockResolvedValue([buildEmailRow()] as never);
272
+ smsFindBy = jest
273
+ .spyOn(UserSmsService, "findBy")
274
+ .mockResolvedValue([buildSmsRow()] as never);
275
+ callFindBy = jest
276
+ .spyOn(UserCallService, "findBy")
277
+ .mockResolvedValue([] as never);
278
+ whatsAppFindBy = jest
279
+ .spyOn(UserWhatsAppService, "findBy")
280
+ .mockResolvedValue([] as never);
281
+ pushFindBy = jest
282
+ .spyOn(UserPushService, "findBy")
283
+ .mockResolvedValue([buildPushRow()] as never);
284
+ telegramFindBy = jest
285
+ .spyOn(UserTelegramService, "findBy")
286
+ .mockResolvedValue([buildTelegramRow()] as never);
287
+ webhookFindBy = jest
288
+ .spyOn(UserWebhookService, "findBy")
289
+ .mockResolvedValue([buildWebhookRow()] as never);
290
+
291
+ jest
292
+ .spyOn(UserEmailService, "findOneBy")
293
+ .mockResolvedValue(buildEmailRow() as never);
294
+ smsFindOneBy = jest
295
+ .spyOn(UserSmsService, "findOneBy")
296
+ .mockResolvedValue(buildSmsRow() as never);
297
+ jest.spyOn(UserCallService, "findOneBy").mockResolvedValue(null as never);
298
+ jest.spyOn(UserWhatsAppService, "findOneBy").mockResolvedValue(null as never);
299
+ jest.spyOn(UserPushService, "findOneBy").mockResolvedValue(null as never);
300
+ jest.spyOn(UserTelegramService, "findOneBy").mockResolvedValue(null as never);
301
+ jest.spyOn(UserWebhookService, "findOneBy").mockResolvedValue(null as never);
302
+
303
+ emailCreate = jest
304
+ .spyOn(UserEmailService, "create")
305
+ .mockImplementation((data: any): Promise<UserEmail> => {
306
+ const created: UserEmail = data.data as UserEmail;
307
+ created._id = ObjectID.generate().toString();
308
+ return Promise.resolve(created);
309
+ });
310
+
311
+ smsCreate = jest
312
+ .spyOn(UserSmsService, "create")
313
+ .mockImplementation((data: any): Promise<UserSMS> => {
314
+ const created: UserSMS = data.data as UserSMS;
315
+ created._id = ObjectID.generate().toString();
316
+ return Promise.resolve(created);
317
+ });
318
+
319
+ jest
320
+ .spyOn(UserCallService, "create")
321
+ .mockImplementation((data: any): Promise<UserCall> => {
322
+ const created: UserCall = data.data as UserCall;
323
+ created._id = ObjectID.generate().toString();
324
+ return Promise.resolve(created);
325
+ });
326
+
327
+ jest
328
+ .spyOn(UserWhatsAppService, "create")
329
+ .mockImplementation((data: any): Promise<UserWhatsApp> => {
330
+ const created: UserWhatsApp = data.data as UserWhatsApp;
331
+ created._id = ObjectID.generate().toString();
332
+ return Promise.resolve(created);
333
+ });
334
+
335
+ smsDelete = jest
336
+ .spyOn(UserSmsService, "deleteOneById")
337
+ .mockResolvedValue(undefined as never);
338
+
339
+ smsResend = jest
340
+ .spyOn(UserSmsService, "resendVerificationCode")
341
+ .mockResolvedValue(undefined as never);
342
+
343
+ deletionImpact = jest
344
+ .spyOn(UserNotificationRuleService, "getNotificationMethodDeletionImpact")
345
+ .mockResolvedValue({
346
+ projectId: projectId,
347
+ userId: targetUserId,
348
+ isOnCallResponder: true,
349
+ reachedVia: [],
350
+ rulesDeletedCount: 4,
351
+ coverageLost: [
352
+ { ruleType: "x", rulesRemoved: 1 },
353
+ { ruleType: "y", rulesRemoved: 1 },
354
+ ],
355
+ handoffNotificationsLost: [],
356
+ reachability: "NotReachable",
357
+ verifiedMethodCountAfterDeletion: 0,
358
+ isFallbackEnabled: true,
359
+ isTruncated: false,
360
+ } as never);
361
+ });
362
+
363
+ describe("listing", () => {
364
+ test("returns every channel, masked, and never a raw value", async () => {
365
+ const methods: Array<AdminNotificationMethodView> =
366
+ await UserNotificationMethodAdminService.listMethodsForUser({
367
+ projectId: projectId,
368
+ userId: targetUserId,
369
+ });
370
+
371
+ expect(methods).toHaveLength(5);
372
+
373
+ /*
374
+ * The real maskIdentifier, not a stub, so these are the exact shapes an
375
+ * administrator sees on screen.
376
+ */
377
+ expect(viewFor(methods, ReadinessMethodType.Email).maskedIdentifier).toBe(
378
+ `j${IDENTIFIER_MASK}@example.com`,
379
+ );
380
+ expect(viewFor(methods, ReadinessMethodType.SMS).maskedIdentifier).toBe(
381
+ `+1 ${IDENTIFIER_MASK} ${IDENTIFIER_MASK} 4821`,
382
+ );
383
+ expect(
384
+ viewFor(methods, ReadinessMethodType.Telegram).maskedIdentifier,
385
+ ).toBe(`@ja${IDENTIFIER_MASK}`);
386
+
387
+ /*
388
+ * Scanned as a whole rather than field by field, so a view that grew a raw
389
+ * field this test never thought of still fails.
390
+ */
391
+ expectNothingRawIn(methods);
392
+ });
393
+
394
+ test("selects no column that carries a credential", async () => {
395
+ await UserNotificationMethodAdminService.listMethodsForUser({
396
+ projectId: projectId,
397
+ userId: targetUserId,
398
+ });
399
+
400
+ /*
401
+ * UserWebhook.webhookUrl is a bearer credential — anyone holding a
402
+ * Slack/Discord/Teams hook url can post as the integration — and
403
+ * UserPush.deviceToken addresses the device directly. Neither may be
404
+ * selected on this path at all: a value that is never read cannot be
405
+ * leaked by a later serialiser.
406
+ */
407
+ const webhookSelect: any = webhookFindBy.mock.calls[0]![0].select;
408
+ expect(webhookSelect.webhookUrl).toBeUndefined();
409
+ expect(webhookSelect.secret).toBeUndefined();
410
+
411
+ const pushSelect: any = pushFindBy.mock.calls[0]![0].select;
412
+ expect(pushSelect.deviceToken).toBeUndefined();
413
+
414
+ /*
415
+ * And the telegram CHAT ID, which is the addressable target a bot sends to.
416
+ * The handle is the human-facing label and is the one an owner recognises.
417
+ */
418
+ const telegramSelect: any = telegramFindBy.mock.calls[0]![0].select;
419
+ expect(telegramSelect.telegramChatId).toBeUndefined();
420
+ expect(telegramSelect.telegramUserHandle).toBe(true);
421
+
422
+ // Verification codes are live account-takeover material. Never selected.
423
+ for (const spy of [emailFindBy, smsFindBy, callFindBy, whatsAppFindBy]) {
424
+ const select: any = spy.mock.calls[0]![0].select;
425
+ expect(select.verificationCode).toBeUndefined();
426
+ }
427
+ });
428
+
429
+ test("reads only this user's rows, in this project", async () => {
430
+ await UserNotificationMethodAdminService.listMethodsForUser({
431
+ projectId: projectId,
432
+ userId: targetUserId,
433
+ });
434
+
435
+ for (const spy of [emailFindBy, smsFindBy, telegramFindBy, webhookFindBy]) {
436
+ const call: any = spy.mock.calls[0]![0];
437
+
438
+ expect(call.query.projectId.toString()).toBe(projectId.toString());
439
+ expect(call.query.userId.toString()).toBe(targetUserId.toString());
440
+
441
+ /*
442
+ * isRoot, necessarily: the models are scoped to their owner, so a
443
+ * non-root read on behalf of an administrator would be refused. It is
444
+ * also exactly why the router above this is the only gate there is.
445
+ */
446
+ expect(call.props.isRoot).toBe(true);
447
+ }
448
+ });
449
+
450
+ test("marks the three channels an administrator cannot create", async () => {
451
+ const methods: Array<AdminNotificationMethodView> =
452
+ await UserNotificationMethodAdminService.listMethodsForUser({
453
+ projectId: projectId,
454
+ userId: targetUserId,
455
+ });
456
+
457
+ expect(viewFor(methods, ReadinessMethodType.Email).isAdminAddable).toBe(
458
+ true,
459
+ );
460
+ expect(viewFor(methods, ReadinessMethodType.SMS).isAdminAddable).toBe(true);
461
+
462
+ expect(viewFor(methods, ReadinessMethodType.Push).isAdminAddable).toBe(
463
+ false,
464
+ );
465
+ expect(viewFor(methods, ReadinessMethodType.Telegram).isAdminAddable).toBe(
466
+ false,
467
+ );
468
+ expect(viewFor(methods, ReadinessMethodType.Webhook).isAdminAddable).toBe(
469
+ false,
470
+ );
471
+ });
472
+ });
473
+
474
+ describe("adding", () => {
475
+ test("writes the row for the TARGET and leaves it unverified", async () => {
476
+ smsFindBy.mockResolvedValue([] as never);
477
+
478
+ await UserNotificationMethodAdminService.addMethodForUser({
479
+ projectId: projectId,
480
+ targetUserId: targetUserId,
481
+ actorUserId: actorUserId,
482
+ methodType: "SMS",
483
+ value: RAW_PHONE,
484
+ props: props(),
485
+ });
486
+
487
+ const created: UserSMS = smsCreate.mock.calls[0]![0].data as UserSMS;
488
+
489
+ /*
490
+ * The TARGET's id, not the actor's. A row written with the admin's id would
491
+ * be the admin's own method — harmless — but a row written with the
492
+ * target's id and the ADMIN's number is the redirect this whole design
493
+ * exists to prevent, and the two are one typo apart.
494
+ */
495
+ expect(created.userId!.toString()).toBe(targetUserId.toString());
496
+ expect(created.projectId!.toString()).toBe(projectId.toString());
497
+ expect(created.phone!.toString()).toBe(RAW_PHONE);
498
+
499
+ /*
500
+ * UNVERIFIED, by not being set at all. This is the property everything
501
+ * else rests on: the method service's onCreateSuccess sends a verification
502
+ * code to the number itself precisely because isVerified is falsy, and the
503
+ * verify endpoints then refuse anybody but the row's owner. Setting this
504
+ * true here would make an administrator's typing immediately live.
505
+ */
506
+ expect(created.isVerified).toBeFalsy();
507
+
508
+ // And nothing about the row is verified afterwards either.
509
+ expect(smsCreate.mock.calls[0]![0].props.isRoot).toBe(true);
510
+ });
511
+
512
+ test("refuses the three channels that cannot be verified by their owner", async () => {
513
+ for (const methodType of [
514
+ "Push",
515
+ "Telegram",
516
+ "Webhook",
517
+ "Carrier Pigeon",
518
+ ]) {
519
+ await expect(
520
+ UserNotificationMethodAdminService.addMethodForUser({
521
+ projectId: projectId,
522
+ targetUserId: targetUserId,
523
+ actorUserId: actorUserId,
524
+ methodType: methodType,
525
+ value: "anything",
526
+ props: props(),
527
+ }),
528
+ ).rejects.toThrow(BadDataException);
529
+ }
530
+
531
+ /*
532
+ * One sentence for "no such channel" and for "that channel exists but an
533
+ * administrator may not create it": naming which one it was describes the
534
+ * shape of the feature to somebody probing it.
535
+ */
536
+ await expect(
537
+ UserNotificationMethodAdminService.addMethodForUser({
538
+ projectId: projectId,
539
+ targetUserId: targetUserId,
540
+ actorUserId: actorUserId,
541
+ methodType: "Webhook",
542
+ value: "https://hooks.example.com/abc",
543
+ props: props(),
544
+ }),
545
+ ).rejects.toThrow(/can only add Email, SMS, Call and WhatsApp/);
546
+ });
547
+
548
+ test("validates the value before writing anything", async () => {
549
+ smsFindBy.mockResolvedValue([] as never);
550
+
551
+ await expect(
552
+ UserNotificationMethodAdminService.addMethodForUser({
553
+ projectId: projectId,
554
+ targetUserId: targetUserId,
555
+ actorUserId: actorUserId,
556
+ methodType: "Email",
557
+ value: "not-an-email",
558
+ props: props(),
559
+ }),
560
+ ).rejects.toThrow(BadDataException);
561
+
562
+ /*
563
+ * Refused before the row exists rather than stored and discovered later by
564
+ * a page that fails to deliver — which, on this surface, means discovered
565
+ * during an incident.
566
+ */
567
+ expect(emailCreate).not.toHaveBeenCalled();
568
+ });
569
+
570
+ test("refuses a duplicate rather than creating a second row", async () => {
571
+ await expect(
572
+ UserNotificationMethodAdminService.addMethodForUser({
573
+ projectId: projectId,
574
+ targetUserId: targetUserId,
575
+ actorUserId: actorUserId,
576
+ methodType: "SMS",
577
+ value: RAW_PHONE,
578
+ props: props(),
579
+ }),
580
+ ).rejects.toThrow(/already has a SMS notification method/);
581
+
582
+ /*
583
+ * Two identical rows are not merely untidy: they show up as two entries an
584
+ * admin has to tell apart on a masked list, each with its own verification
585
+ * state, and a rule pointed at the unverified one looks correct and pages
586
+ * nobody.
587
+ */
588
+ expect(smsCreate).not.toHaveBeenCalled();
589
+ });
590
+
591
+ test("refuses a target who is not a member of the project", async () => {
592
+ teamMemberFindOneBy.mockResolvedValue(null as never);
593
+
594
+ await expect(
595
+ UserNotificationMethodAdminService.addMethodForUser({
596
+ projectId: projectId,
597
+ targetUserId: targetUserId,
598
+ actorUserId: actorUserId,
599
+ methodType: "SMS",
600
+ value: RAW_PHONE,
601
+ props: props(),
602
+ }),
603
+ ).rejects.toThrow(/not a member of this project/);
604
+
605
+ expect(smsCreate).not.toHaveBeenCalled();
606
+
607
+ /*
608
+ * Membership is read with root props and NO cache.
609
+ * TeamMemberService.getTeamIdsForUser would answer the same question with
610
+ * one fewer query, but it memoises for 60 seconds — and a security decision
611
+ * that keeps saying "yes" for a minute after somebody was removed from the
612
+ * project is not a security decision.
613
+ */
614
+ const call: any = teamMemberFindOneBy.mock.calls[0]![0];
615
+
616
+ expect(call.query.hasAcceptedInvitation).toBe(true);
617
+ expect(call.props.isRoot).toBe(true);
618
+ });
619
+
620
+ test("tells the owner, naming the actor and the mask", async () => {
621
+ smsFindBy.mockResolvedValue([] as never);
622
+
623
+ await UserNotificationMethodAdminService.addMethodForUser({
624
+ projectId: projectId,
625
+ targetUserId: targetUserId,
626
+ actorUserId: actorUserId,
627
+ methodType: "SMS",
628
+ value: RAW_PHONE,
629
+ props: props(),
630
+ });
631
+
632
+ await new Promise<void>((resolve: () => void): void => {
633
+ setTimeout(resolve, 0);
634
+ });
635
+
636
+ expect(mailSpy).toHaveBeenCalled();
637
+
638
+ const mail: any = mailSpy.mock.calls[0]![0];
639
+ const recipientOptions: any = mailSpy.mock.calls[0]![1];
640
+
641
+ // To the OWNER, never to the admin who made the change.
642
+ expect(mail.toEmail.toString()).toBe("jane.login@example.com");
643
+ expect(recipientOptions.userId.toString()).toBe(targetUserId.toString());
644
+
645
+ /*
646
+ * The actor is named by email as well as by name: display names are not
647
+ * unique and are user-editable, so "Alex added a phone number" is not
648
+ * something the reader can act on. An address is.
649
+ */
650
+ expect(mail.vars.message).toContain("Alex Admin");
651
+ expect(mail.vars.message).toContain("alex.admin@example.com");
652
+
653
+ /*
654
+ * And the instruction that makes the mail a control rather than a notice:
655
+ * an addition is inert until the owner verifies it, so "do not verify it"
656
+ * is an action they can take.
657
+ */
658
+ expect(mail.vars.message).toContain("cannot be used to notify you");
659
+ expect(mail.vars.message).toContain("do not verify it");
660
+
661
+ // The MASK, never the number — this mail crosses an untrusted transport.
662
+ expect(mail.vars.message).toContain(`+1 ${IDENTIFIER_MASK}`);
663
+ expectNothingRawIn(mail.vars);
664
+ });
665
+
666
+ test("does not mail somebody about their own change", async () => {
667
+ smsFindBy.mockResolvedValue([] as never);
668
+
669
+ await UserNotificationMethodAdminService.addMethodForUser({
670
+ projectId: projectId,
671
+ targetUserId: targetUserId,
672
+ actorUserId: targetUserId,
673
+ methodType: "SMS",
674
+ value: RAW_PHONE,
675
+ props: props(),
676
+ });
677
+
678
+ await new Promise<void>((resolve: () => void): void => {
679
+ setTimeout(resolve, 0);
680
+ });
681
+
682
+ // A person who just did the thing does not need to be warned about it.
683
+ expect(mailSpy).not.toHaveBeenCalled();
684
+ });
685
+
686
+ test("drops the readiness cache so the change is visible immediately", async () => {
687
+ smsFindBy.mockResolvedValue([] as never);
688
+
689
+ await UserNotificationMethodAdminService.addMethodForUser({
690
+ projectId: projectId,
691
+ targetUserId: targetUserId,
692
+ actorUserId: actorUserId,
693
+ methodType: "SMS",
694
+ value: RAW_PHONE,
695
+ props: props(),
696
+ });
697
+
698
+ /*
699
+ * The readiness service holds this user's method list for 60 seconds, and
700
+ * the page the admin is looking at re-reads readiness the moment this
701
+ * returns. Left alone it would redraw the responder exactly as they were,
702
+ * which reads as "the add did not work".
703
+ */
704
+ expect(clearCacheSpy).toHaveBeenCalled();
705
+ });
706
+
707
+ test("returns the created method already masked", async () => {
708
+ smsFindBy.mockResolvedValue([] as never);
709
+
710
+ const created: AdminNotificationMethodView =
711
+ await UserNotificationMethodAdminService.addMethodForUser({
712
+ projectId: projectId,
713
+ targetUserId: targetUserId,
714
+ actorUserId: actorUserId,
715
+ methodType: "SMS",
716
+ value: RAW_PHONE,
717
+ props: props(),
718
+ });
719
+
720
+ expect(created.maskedIdentifier).toBe(
721
+ `+1 ${IDENTIFIER_MASK} ${IDENTIFIER_MASK} 4821`,
722
+ );
723
+ expect(created.isVerified).toBe(false);
724
+
725
+ expectNothingRawIn(created);
726
+ });
727
+ });
728
+
729
+ describe("removing", () => {
730
+ test("deletes the row and tells the owner what they lost", async () => {
731
+ await UserNotificationMethodAdminService.deleteMethodForUser({
732
+ projectId: projectId,
733
+ targetUserId: targetUserId,
734
+ actorUserId: actorUserId,
735
+ methodType: "SMS",
736
+ methodId: smsMethodId,
737
+ props: props(),
738
+ });
739
+
740
+ expect(smsDelete).toHaveBeenCalled();
741
+ expect(smsDelete.mock.calls[0]![0].id.toString()).toBe(
742
+ smsMethodId.toString(),
743
+ );
744
+
745
+ await new Promise<void>((resolve: () => void): void => {
746
+ setTimeout(resolve, 0);
747
+ });
748
+
749
+ const mail: any = mailSpy.mock.calls[0]![0];
750
+
751
+ /*
752
+ * Deletion gets its own sentence rather than the generic one. It is the
753
+ * only one of the two actions that can leave a responder silently
754
+ * unreachable, which is the failure this whole epic exists to stop
755
+ * happening unnoticed.
756
+ */
757
+ expect(mail.subject).toContain("removed");
758
+ expect(mail.vars.message).toContain("notification rules that used it");
759
+ expect(mail.vars.message).toContain("may no longer be paged");
760
+
761
+ // Still the mask: the owner's own number is not echoed back over SMTP.
762
+ expect(mail.vars.message).toContain(`+1 ${IDENTIFIER_MASK}`);
763
+ expectNothingRawIn(mail.vars);
764
+ });
765
+
766
+ test("refuses a method that belongs to somebody else", async () => {
767
+ /*
768
+ * The row is looked up by (id, projectId, userId) together, so a row
769
+ * belonging to another user simply does not come back — and "not found" and
770
+ * "not yours" are deliberately the same answer, because in both cases the
771
+ * caller named a row they have no business naming.
772
+ */
773
+ smsFindOneBy.mockResolvedValue(null as never);
774
+
775
+ await expect(
776
+ UserNotificationMethodAdminService.deleteMethodForUser({
777
+ projectId: projectId,
778
+ targetUserId: targetUserId,
779
+ actorUserId: actorUserId,
780
+ methodType: "SMS",
781
+ methodId: smsMethodId,
782
+ props: props(),
783
+ }),
784
+ ).rejects.toThrow(/could not be found for this user/);
785
+
786
+ expect(smsDelete).not.toHaveBeenCalled();
787
+ });
788
+
789
+ test("scopes the ownership lookup to the project and the user", async () => {
790
+ await UserNotificationMethodAdminService.deleteMethodForUser({
791
+ projectId: projectId,
792
+ targetUserId: targetUserId,
793
+ actorUserId: actorUserId,
794
+ methodType: "SMS",
795
+ methodId: smsMethodId,
796
+ props: props(),
797
+ });
798
+
799
+ const call: any = smsFindOneBy.mock.calls[0]![0];
800
+
801
+ expect(call.query._id.toString()).toBe(smsMethodId.toString());
802
+ expect(call.query.projectId.toString()).toBe(projectId.toString());
803
+ expect(call.query.userId.toString()).toBe(targetUserId.toString());
804
+ });
805
+
806
+ test("refuses a target who is not a member of the project", async () => {
807
+ teamMemberFindOneBy.mockResolvedValue(null as never);
808
+
809
+ await expect(
810
+ UserNotificationMethodAdminService.deleteMethodForUser({
811
+ projectId: projectId,
812
+ targetUserId: targetUserId,
813
+ actorUserId: actorUserId,
814
+ methodType: "SMS",
815
+ methodId: smsMethodId,
816
+ props: props(),
817
+ }),
818
+ ).rejects.toThrow(/not a member of this project/);
819
+
820
+ expect(smsDelete).not.toHaveBeenCalled();
821
+ });
822
+
823
+ test("previews the cost without deleting anything", async () => {
824
+ const preview: unknown =
825
+ await UserNotificationMethodAdminService.getDeletionPreview({
826
+ projectId: projectId,
827
+ targetUserId: targetUserId,
828
+ methodType: "SMS",
829
+ methodId: smsMethodId,
830
+ });
831
+
832
+ expect(preview).toEqual({
833
+ rulesDeletedCount: 4,
834
+ coverageLostCount: 2,
835
+ verifiedMethodCountAfterDeletion: 0,
836
+ reachability: "NotReachable",
837
+ isFallbackEnabled: true,
838
+ isTruncated: false,
839
+ });
840
+
841
+ expect(smsDelete).not.toHaveBeenCalled();
842
+
843
+ /*
844
+ * The impact is computed by the rules service, which reads EVERY rule for
845
+ * the user rather than a page and derives each rule's severity from the
846
+ * column its rule type dictates. That method was written for this caller
847
+ * and had no production caller until now.
848
+ */
849
+ const call: any = deletionImpact.mock.calls[0]![0];
850
+ expect(call.methodType).toBe(ReadinessMethodType.SMS);
851
+ expect(call.methodId.toString()).toBe(smsMethodId.toString());
852
+ });
853
+
854
+ test("the preview refuses a method that is not this user's", async () => {
855
+ smsFindOneBy.mockResolvedValue(null as never);
856
+
857
+ await expect(
858
+ UserNotificationMethodAdminService.getDeletionPreview({
859
+ projectId: projectId,
860
+ targetUserId: targetUserId,
861
+ methodType: "SMS",
862
+ methodId: smsMethodId,
863
+ }),
864
+ ).rejects.toThrow(/could not be found for this user/);
865
+
866
+ /*
867
+ * Otherwise the preview is a probe: it would report rule counts for a
868
+ * method id belonging to another user or another project.
869
+ */
870
+ expect(deletionImpact).not.toHaveBeenCalled();
871
+ });
872
+ });
873
+
874
+ describe("resending a verification code", () => {
875
+ test("asks the channel's own service to send it again", async () => {
876
+ await UserNotificationMethodAdminService.resendVerificationCodeForUser({
877
+ projectId: projectId,
878
+ targetUserId: targetUserId,
879
+ methodType: "SMS",
880
+ methodId: smsMethodId,
881
+ });
882
+
883
+ expect(smsResend).toHaveBeenCalledWith(smsMethodId);
884
+
885
+ /*
886
+ * It discloses nothing: the code goes to the number on the row, which is
887
+ * the one thing on it the administrator cannot read. That is what makes
888
+ * this the useful lever after adding a method — "I added your work mobile,
889
+ * here is another code" rather than re-typing the number.
890
+ */
891
+ expect(mailSpy).not.toHaveBeenCalled();
892
+ });
893
+
894
+ test("refuses a channel with no verification concept", async () => {
895
+ jest
896
+ .spyOn(UserWebhookService, "findOneBy")
897
+ .mockResolvedValue(buildWebhookRow() as never);
898
+
899
+ await expect(
900
+ UserNotificationMethodAdminService.resendVerificationCodeForUser({
901
+ projectId: projectId,
902
+ targetUserId: targetUserId,
903
+ methodType: "Webhook",
904
+ methodId: ObjectID.generate(),
905
+ }),
906
+ ).rejects.toThrow(/do not use a verification code/);
907
+ });
908
+
909
+ test("refuses a method that is not this user's", async () => {
910
+ smsFindOneBy.mockResolvedValue(null as never);
911
+
912
+ await expect(
913
+ UserNotificationMethodAdminService.resendVerificationCodeForUser({
914
+ projectId: projectId,
915
+ targetUserId: targetUserId,
916
+ methodType: "SMS",
917
+ methodId: smsMethodId,
918
+ }),
919
+ ).rejects.toThrow(/could not be found for this user/);
920
+
921
+ expect(smsResend).not.toHaveBeenCalled();
922
+ });
923
+ });
924
+
925
+ describe("the mail cannot break the write", () => {
926
+ test("a failed notification leaves the method in place", async () => {
927
+ smsFindBy.mockResolvedValue([] as never);
928
+ mailSpy.mockRejectedValue(new Error("smtp is down") as never);
929
+
930
+ const created: AdminNotificationMethodView =
931
+ await UserNotificationMethodAdminService.addMethodForUser({
932
+ projectId: projectId,
933
+ targetUserId: targetUserId,
934
+ actorUserId: actorUserId,
935
+ methodType: "SMS",
936
+ value: RAW_PHONE,
937
+ props: props(),
938
+ });
939
+
940
+ /*
941
+ * The row is already committed by the time the mail is attempted, so
942
+ * throwing then would report a failure for a change that happened — and the
943
+ * caller would be entitled to believe it did not, and to try again.
944
+ */
945
+ expect(created.methodId).toBeDefined();
946
+ expect(smsCreate).toHaveBeenCalled();
947
+ });
948
+ });