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