@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.
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
- package/Models/DatabaseModels/UserCall.ts +94 -3
- package/Models/DatabaseModels/UserEmail.ts +94 -3
- package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
- package/Models/DatabaseModels/UserPush.ts +37 -0
- package/Models/DatabaseModels/UserSMS.ts +94 -3
- package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
- package/Server/API/BaseAPI.ts +79 -14
- package/Server/API/UserCallAPI.ts +53 -45
- package/Server/API/UserEmailAPI.ts +53 -44
- package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
- package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
- package/Server/API/UserPushAPI.ts +126 -2
- package/Server/API/UserSmsAPI.ts +53 -44
- package/Server/API/UserWhatsAppAPI.ts +53 -50
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
- package/Server/Services/AnalyticsDatabaseService.ts +169 -0
- package/Server/Services/DatabaseService.ts +101 -0
- package/Server/Services/LogAggregationService.ts +5 -0
- package/Server/Services/PushNotificationService.ts +127 -12
- package/Server/Services/TeamMemberService.ts +53 -13
- package/Server/Services/UserCallService.ts +55 -24
- package/Server/Services/UserEmailService.ts +83 -17
- package/Server/Services/UserIncomingCallNumberService.ts +49 -85
- package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
- package/Server/Services/UserNotificationRuleService.ts +26 -0
- package/Server/Services/UserPushService.ts +89 -0
- package/Server/Services/UserSmsService.ts +50 -17
- package/Server/Services/UserWhatsAppService.ts +48 -15
- package/Server/Types/Database/QueryHelper.ts +30 -0
- package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
- package/Server/Utils/ChannelVerification.ts +510 -0
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
- package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
- package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
- package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
- package/Server/Utils/TotpAuth.ts +137 -9
- package/Server/Utils/UserRegistrationToken.ts +182 -0
- package/Server/Utils/VerificationCode.ts +134 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
- package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
- package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
- package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
- package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
- package/Tests/Server/API/UserSmsApi.test.ts +107 -14
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
- package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
- package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
- package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
- package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
- package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
- package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
- package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
- package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
- package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
- package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
- package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
- package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
- package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
- package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
- package/Tests/Server/Utils/Browser.test.ts +162 -0
- package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
- package/Tests/Server/Utils/Express.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
- package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
- package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
- package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
- package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
- package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
- package/Tests/Types/Call/CallRequest.test.ts +189 -0
- package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
- package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
- package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
- package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
- package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
- package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
- package/Tests/Types/TextRandomGeneration.test.ts +211 -0
- package/Tests/UI/Components/Button.test.tsx +175 -1
- package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
- package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
- package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
- package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
- package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
- package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
- package/Tests/UI/Components/List.test.tsx +9 -2
- package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
- package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
- package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
- package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
- package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
- package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
- package/Tests/UI/Components/Skeleton.test.tsx +71 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
- package/Tests/UI/Components/Tabs.test.tsx +24 -0
- package/Tests/UI/Components/Toast.test.tsx +130 -7
- package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
- package/Tests/UI/Utils/Dropdown.test.ts +423 -0
- package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
- package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
- package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
- package/Tests/Utils/API.test.ts +16 -2
- package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
- package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
- package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
- package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
- package/Types/Permission.ts +35 -0
- package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
- package/Types/PushNotification/PushNotificationMessage.ts +15 -0
- package/Types/PushNotification/PushNotificationRequest.ts +8 -16
- package/Types/Service/ServiceLanguage.ts +144 -0
- package/Types/Text.ts +111 -19
- package/UI/Components/Button/Button.tsx +27 -14
- package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
- package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
- package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
- package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
- package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
- package/UI/Components/CommandPalette/Types.ts +49 -0
- package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/Feed/FeedItem.tsx +1 -1
- package/UI/Components/List/List.tsx +37 -5
- package/UI/Components/List/ListSkeleton.tsx +54 -0
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
- package/UI/Components/Modal/Modal.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
- package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
- package/UI/Components/Navbar/NavBar.tsx +14 -1
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
- package/UI/Components/SideOver/SideOver.tsx +37 -2
- package/UI/Components/Skeleton/Skeleton.tsx +53 -0
- package/UI/Components/Table/Table.tsx +74 -44
- package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
- package/UI/Components/Tabs/Tab.tsx +1 -1
- package/UI/Components/Toast/Toast.tsx +118 -53
- package/UI/Components/Toast/ToastInit.tsx +82 -32
- package/UI/Styles/Theme.css +40 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
- package/UI/Utils/ModelListCache.ts +188 -0
- package/UI/Utils/Project.ts +29 -0
- package/Utils/API.ts +6 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +69 -13
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +42 -38
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +42 -38
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
- package/build/dist/Server/API/UserPushAPI.js +95 -2
- package/build/dist/Server/API/UserPushAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +42 -38
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +74 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +5 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +69 -30
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +43 -7
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +54 -22
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +78 -17
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
- package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +77 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +50 -17
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
- package/build/dist/Server/Utils/ChannelVerification.js +348 -0
- package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/TotpAuth.js +127 -8
- package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
- package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
- package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
- package/build/dist/Server/Utils/VerificationCode.js +120 -0
- package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +33 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
- package/build/dist/Types/Service/ServiceLanguage.js +96 -0
- package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
- package/build/dist/Types/Text.js +82 -17
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +22 -14
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
- package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
- package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +19 -5
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
- package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
- package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
- package/build/dist/UI/Components/Table/Table.js +35 -24
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
- package/build/dist/UI/Components/Tabs/Tab.js +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/Toast/Toast.js +77 -23
- package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
- package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelListCache.js +126 -0
- package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
- package/build/dist/UI/Utils/Project.js +24 -1
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/Utils/API.js +6 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/jest.config.json +1 -1
- package/package.json +2 -3
- package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
- package/Tests/__mocks__/otpauth.js +0 -30
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
- 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
|
+
});
|