@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,775 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
jest,
|
|
8
|
+
} from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* GHSA-5cr8-vph4-3hrf — the request limiter in front of the notification
|
|
12
|
+
* channel verify and resend routes.
|
|
13
|
+
*
|
|
14
|
+
* The durable controls in ChannelVerification already bound guessing against
|
|
15
|
+
* one issued code. What this limiter exists for is what a per-row counter
|
|
16
|
+
* cannot see: a caller creating fresh rows to farm fresh attempt budgets, and
|
|
17
|
+
* the volume of real messages sent to somebody who never asked for them. So
|
|
18
|
+
* the tests that matter here are the ones about what the counters are KEYED
|
|
19
|
+
* on — an address the caller cannot forge, a user id that survives row
|
|
20
|
+
* rotation — plus the window arithmetic and the Redis-outage behaviour.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
jest.mock("../../../Server/Infrastructure/Redis", () => {
|
|
24
|
+
return {
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: {
|
|
27
|
+
getClient: jest.fn(),
|
|
28
|
+
isConnected: jest.fn(),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
34
|
+
return {
|
|
35
|
+
__esModule: true,
|
|
36
|
+
default: {
|
|
37
|
+
debug: jest.fn(),
|
|
38
|
+
info: jest.fn(),
|
|
39
|
+
warn: jest.fn(),
|
|
40
|
+
error: jest.fn(),
|
|
41
|
+
},
|
|
42
|
+
getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
47
|
+
return {
|
|
48
|
+
__esModule: true,
|
|
49
|
+
default: {
|
|
50
|
+
sendErrorResponse: jest.fn(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
import Redis from "../../../Server/Infrastructure/Redis";
|
|
56
|
+
import Response from "../../../Server/Utils/Response";
|
|
57
|
+
import logger from "../../../Server/Utils/Logger";
|
|
58
|
+
import VerificationCodeRateLimit, {
|
|
59
|
+
VerificationCodeRateLimitBucket,
|
|
60
|
+
VerificationCodeRateLimitDecision,
|
|
61
|
+
VerificationCodeRateLimitOutcome,
|
|
62
|
+
VerificationCodeRateLimitScope,
|
|
63
|
+
} from "../../../Server/Middleware/VerificationCodeRateLimit";
|
|
64
|
+
import ExceptionCode from "../../../Types/Exception/ExceptionCode";
|
|
65
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
66
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
67
|
+
import {
|
|
68
|
+
ExpressRequest,
|
|
69
|
+
ExpressResponse,
|
|
70
|
+
NextFunction,
|
|
71
|
+
} from "../../../Server/Utils/Express";
|
|
72
|
+
|
|
73
|
+
type MockedFn = ReturnType<typeof jest.fn>;
|
|
74
|
+
|
|
75
|
+
const getClientMock: MockedFn = Redis.getClient as unknown as MockedFn;
|
|
76
|
+
const isConnectedMock: MockedFn = Redis.isConnected as unknown as MockedFn;
|
|
77
|
+
const sendErrorResponseMock: MockedFn =
|
|
78
|
+
Response.sendErrorResponse as unknown as MockedFn;
|
|
79
|
+
const loggerWarnMock: MockedFn = logger.warn as unknown as MockedFn;
|
|
80
|
+
|
|
81
|
+
/* Defaults baked into the limiter; asserted directly in one test below. */
|
|
82
|
+
const VERIFY_WINDOW_SECONDS: number = 15 * 60;
|
|
83
|
+
const VERIFY_PER_ITEM_LIMIT: number = 10;
|
|
84
|
+
const VERIFY_PER_USER_LIMIT: number = 50;
|
|
85
|
+
const VERIFY_PER_IP_LIMIT: number = 150;
|
|
86
|
+
const RESEND_PER_ITEM_LIMIT: number = 5;
|
|
87
|
+
const RESEND_PER_USER_LIMIT: number = 15;
|
|
88
|
+
const RESEND_PER_IP_LIMIT: number = 45;
|
|
89
|
+
|
|
90
|
+
const ITEM_A: string = "1a2b3c4d-1111-4111-8111-111111111111";
|
|
91
|
+
const ITEM_B: string = "1a2b3c4d-2222-4222-8222-222222222222";
|
|
92
|
+
const USER_A: string = "1a2b3c4d-3333-4333-8333-333333333333";
|
|
93
|
+
const USER_B: string = "1a2b3c4d-4444-4444-8444-444444444444";
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* A fake that behaves like a Redis counter store rather than a bag of
|
|
97
|
+
* assertions: INCR really increments and EXPIRE really records a TTL, so the
|
|
98
|
+
* window and reset tests exercise the limiter's arithmetic instead of
|
|
99
|
+
* restating it.
|
|
100
|
+
*/
|
|
101
|
+
interface RecordedExpire {
|
|
102
|
+
key: string;
|
|
103
|
+
ttlSeconds: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class FakeRedisClient {
|
|
107
|
+
public counters: Map<string, number> = new Map();
|
|
108
|
+
public expires: Array<RecordedExpire> = [];
|
|
109
|
+
public failNextExec: Error | null = null;
|
|
110
|
+
public malformedExecResult: unknown | undefined = undefined;
|
|
111
|
+
|
|
112
|
+
public pipeline(): FakePipeline {
|
|
113
|
+
return new FakePipeline(this);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public expiresForKey(key: string): Array<RecordedExpire> {
|
|
117
|
+
return this.expires.filter((recorded: RecordedExpire) => {
|
|
118
|
+
return recorded.key === key;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type QueuedCommand = () => [Error | null, unknown];
|
|
124
|
+
|
|
125
|
+
class FakePipeline {
|
|
126
|
+
private commands: Array<QueuedCommand> = [];
|
|
127
|
+
|
|
128
|
+
public constructor(private client: FakeRedisClient) {}
|
|
129
|
+
|
|
130
|
+
public incr(key: string): FakePipeline {
|
|
131
|
+
this.commands.push((): [Error | null, unknown] => {
|
|
132
|
+
const next: number = (this.client.counters.get(key) || 0) + 1;
|
|
133
|
+
this.client.counters.set(key, next);
|
|
134
|
+
return [null, next];
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public expire(key: string, ttlSeconds: number): FakePipeline {
|
|
141
|
+
this.commands.push((): [Error | null, unknown] => {
|
|
142
|
+
this.client.expires.push({ key, ttlSeconds });
|
|
143
|
+
return [null, 1];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async exec(): Promise<unknown> {
|
|
150
|
+
if (this.client.failNextExec) {
|
|
151
|
+
const error: Error = this.client.failNextExec;
|
|
152
|
+
this.client.failNextExec = null;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof this.client.malformedExecResult !== "undefined") {
|
|
157
|
+
const result: unknown = this.client.malformedExecResult;
|
|
158
|
+
this.client.malformedExecResult = undefined;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this.commands.map((command: QueuedCommand) => {
|
|
163
|
+
return command();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface BuildRequestOverrides {
|
|
169
|
+
body?: Record<string, unknown>;
|
|
170
|
+
headers?: Record<string, string | Array<string>>;
|
|
171
|
+
socketAddress?: string | undefined;
|
|
172
|
+
userId?: string | undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const buildRequest: (overrides?: BuildRequestOverrides) => ExpressRequest = (
|
|
176
|
+
overrides: BuildRequestOverrides = {},
|
|
177
|
+
) => {
|
|
178
|
+
return {
|
|
179
|
+
body: overrides.body,
|
|
180
|
+
headers: overrides.headers || {},
|
|
181
|
+
socket: { remoteAddress: overrides.socketAddress },
|
|
182
|
+
userAuthorization: overrides.userId
|
|
183
|
+
? { userId: new ObjectID(overrides.userId) }
|
|
184
|
+
: undefined,
|
|
185
|
+
} as unknown as ExpressRequest;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
interface BuiltResponse {
|
|
189
|
+
response: ExpressResponse;
|
|
190
|
+
headers: Record<string, string>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const buildResponse: () => BuiltResponse = () => {
|
|
194
|
+
const headers: Record<string, string> = {};
|
|
195
|
+
|
|
196
|
+
const response: ExpressResponse = {
|
|
197
|
+
setHeader: (name: string, value: string): void => {
|
|
198
|
+
headers[name] = value;
|
|
199
|
+
},
|
|
200
|
+
} as unknown as ExpressResponse;
|
|
201
|
+
|
|
202
|
+
return { response, headers };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
describe("VerificationCodeRateLimit", () => {
|
|
206
|
+
let client: FakeRedisClient;
|
|
207
|
+
let nowSpy: ReturnType<typeof jest.spyOn>;
|
|
208
|
+
let currentTime: number;
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
jest.clearAllMocks();
|
|
212
|
+
|
|
213
|
+
client = new FakeRedisClient();
|
|
214
|
+
getClientMock.mockReturnValue(client);
|
|
215
|
+
isConnectedMock.mockReturnValue(true);
|
|
216
|
+
|
|
217
|
+
currentTime = 1_800_000_000_000;
|
|
218
|
+
currentTime = currentTime - (currentTime % (VERIFY_WINDOW_SECONDS * 1000));
|
|
219
|
+
nowSpy = jest.spyOn(Date, "now").mockImplementation(() => {
|
|
220
|
+
return currentTime;
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
afterEach(() => {
|
|
225
|
+
nowSpy.mockRestore();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const consume: (data: {
|
|
229
|
+
itemKey?: string;
|
|
230
|
+
userKey?: string;
|
|
231
|
+
clientIp?: string;
|
|
232
|
+
bucket?: VerificationCodeRateLimitBucket;
|
|
233
|
+
}) => Promise<VerificationCodeRateLimitDecision> = (data: {
|
|
234
|
+
itemKey?: string;
|
|
235
|
+
userKey?: string;
|
|
236
|
+
clientIp?: string;
|
|
237
|
+
bucket?: VerificationCodeRateLimitBucket;
|
|
238
|
+
}) => {
|
|
239
|
+
return VerificationCodeRateLimit.consume({
|
|
240
|
+
itemKey: data.itemKey || ITEM_A,
|
|
241
|
+
userKey: data.userKey || USER_A,
|
|
242
|
+
clientIp: data.clientIp || "203.0.113.7",
|
|
243
|
+
bucket: data.bucket || VerificationCodeRateLimitBucket.Verify,
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
describe("resolveClientIp", () => {
|
|
248
|
+
/*
|
|
249
|
+
* The security-critical one. Nginx uses $proxy_add_x_forwarded_for, which
|
|
250
|
+
* APPENDS the peer address to whatever the caller sent — so a caller that
|
|
251
|
+
* sends its own X-Forwarded-For controls the LEFT of the list. A limiter
|
|
252
|
+
* keyed on the leftmost entry is defeated with a header: one fresh bucket
|
|
253
|
+
* per request, which is no limit at all.
|
|
254
|
+
*/
|
|
255
|
+
it("bills the hop our own proxy appended, not the one the caller forged", () => {
|
|
256
|
+
const request: ExpressRequest = buildRequest({
|
|
257
|
+
headers: { "x-forwarded-for": "9.9.9.9, 203.0.113.7" },
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(VerificationCodeRateLimit.resolveClientIp(request)).toBe(
|
|
261
|
+
"203.0.113.7",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("falls back to the socket address when there is no forwarded header", () => {
|
|
266
|
+
expect(
|
|
267
|
+
VerificationCodeRateLimit.resolveClientIp(
|
|
268
|
+
buildRequest({ socketAddress: "198.51.100.4" }),
|
|
269
|
+
),
|
|
270
|
+
).toBe("198.51.100.4");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("puts every unidentifiable caller in one shared bucket", () => {
|
|
274
|
+
expect(VerificationCodeRateLimit.resolveClientIp(buildRequest())).toBe(
|
|
275
|
+
"unknown",
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("resolveUserKey", () => {
|
|
281
|
+
it("keys on the signed-in user", () => {
|
|
282
|
+
expect(
|
|
283
|
+
VerificationCodeRateLimit.resolveUserKey(
|
|
284
|
+
buildRequest({ userId: USER_A }),
|
|
285
|
+
),
|
|
286
|
+
).toBe(USER_A);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("puts a request with no session in one shared bucket", () => {
|
|
290
|
+
expect(VerificationCodeRateLimit.resolveUserKey(buildRequest())).toBe(
|
|
291
|
+
"anonymous",
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("resolveItemKey", () => {
|
|
297
|
+
it("keys on the item id from the body", () => {
|
|
298
|
+
expect(
|
|
299
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
300
|
+
buildRequest({ body: { itemId: ITEM_A } }),
|
|
301
|
+
),
|
|
302
|
+
).toBe(ITEM_A);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("normalises case so one row cannot become two buckets", () => {
|
|
306
|
+
expect(
|
|
307
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
308
|
+
buildRequest({ body: { itemId: ITEM_A.toUpperCase() } }),
|
|
309
|
+
),
|
|
310
|
+
).toBe(ITEM_A);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/*
|
|
314
|
+
* Bounds Redis memory against a caller feeding junk ids, and groups them
|
|
315
|
+
* correctly: none of those requests can name a real row, so one small
|
|
316
|
+
* shared allowance between all of them is exactly right.
|
|
317
|
+
*/
|
|
318
|
+
it("collapses junk ids into one shared bucket", () => {
|
|
319
|
+
expect(
|
|
320
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
321
|
+
buildRequest({ body: { itemId: "not-a-uuid" } }),
|
|
322
|
+
),
|
|
323
|
+
).toBe("invalid");
|
|
324
|
+
|
|
325
|
+
expect(
|
|
326
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
327
|
+
buildRequest({ body: { itemId: "x".repeat(5000) } }),
|
|
328
|
+
),
|
|
329
|
+
).toBe("invalid");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("handles a missing or non-string item id", () => {
|
|
333
|
+
expect(VerificationCodeRateLimit.resolveItemKey(buildRequest())).toBe(
|
|
334
|
+
"none",
|
|
335
|
+
);
|
|
336
|
+
expect(
|
|
337
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
338
|
+
buildRequest({ body: { itemId: 12345 } }),
|
|
339
|
+
),
|
|
340
|
+
).toBe("none");
|
|
341
|
+
expect(
|
|
342
|
+
VerificationCodeRateLimit.resolveItemKey(
|
|
343
|
+
buildRequest({ body: { itemId: " " } }),
|
|
344
|
+
),
|
|
345
|
+
).toBe("none");
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("counting", () => {
|
|
350
|
+
it("allows requests up to the per-item limit and refuses the next one", async () => {
|
|
351
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT; i++) {
|
|
352
|
+
const decision: VerificationCodeRateLimitDecision = await consume({});
|
|
353
|
+
|
|
354
|
+
expect(decision.outcome).toBe(VerificationCodeRateLimitOutcome.Allowed);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const rejected: VerificationCodeRateLimitDecision = await consume({});
|
|
358
|
+
|
|
359
|
+
expect(rejected.outcome).toBe(
|
|
360
|
+
VerificationCodeRateLimitOutcome.RateLimited,
|
|
361
|
+
);
|
|
362
|
+
expect(rejected.scope).toBe(VerificationCodeRateLimitScope.Item);
|
|
363
|
+
expect(rejected.isFirstRejectionInWindow).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("reports only the first rejection of a window", async () => {
|
|
367
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 1; i++) {
|
|
368
|
+
await consume({});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const second: VerificationCodeRateLimitDecision = await consume({});
|
|
372
|
+
|
|
373
|
+
expect(second.outcome).toBe(VerificationCodeRateLimitOutcome.RateLimited);
|
|
374
|
+
expect(second.isFirstRejectionInWindow).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* THE bypass this middleware exists to close. Nothing stops a caller
|
|
379
|
+
* creating channel rows, and each new row is a fresh code with a fresh
|
|
380
|
+
* five-attempt budget — so a per-row limit alone is worth nothing. The
|
|
381
|
+
* user counter is what survives row rotation.
|
|
382
|
+
*/
|
|
383
|
+
it("does not hand out a fresh allowance for a fresh row", async () => {
|
|
384
|
+
let allowed: number = 0;
|
|
385
|
+
|
|
386
|
+
for (let i: number = 0; i < VERIFY_PER_USER_LIMIT + 20; i++) {
|
|
387
|
+
/* A different row every single time. */
|
|
388
|
+
const decision: VerificationCodeRateLimitDecision = await consume({
|
|
389
|
+
itemKey: `1a2b3c4d-0000-4000-8000-${i.toString().padStart(12, "0")}`,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (decision.outcome === VerificationCodeRateLimitOutcome.Allowed) {
|
|
393
|
+
allowed++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
expect(allowed).toBe(VERIFY_PER_USER_LIMIT);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("refuses on the user counter and names it", async () => {
|
|
401
|
+
for (let i: number = 0; i < VERIFY_PER_USER_LIMIT; i++) {
|
|
402
|
+
await consume({
|
|
403
|
+
itemKey: `1a2b3c4d-0000-4000-8000-${i.toString().padStart(12, "0")}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const rejected: VerificationCodeRateLimitDecision = await consume({
|
|
408
|
+
itemKey: ITEM_B,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(rejected.scope).toBe(VerificationCodeRateLimitScope.User);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* And rotating the ACCOUNT does not work either, because the address
|
|
416
|
+
* counter sits behind the user counter.
|
|
417
|
+
*/
|
|
418
|
+
it("does not hand out a fresh allowance for a fresh account", async () => {
|
|
419
|
+
let allowed: number = 0;
|
|
420
|
+
|
|
421
|
+
for (let i: number = 0; i < VERIFY_PER_IP_LIMIT + 20; i++) {
|
|
422
|
+
const decision: VerificationCodeRateLimitDecision = await consume({
|
|
423
|
+
itemKey: `1a2b3c4d-0000-4000-8000-${i.toString().padStart(12, "0")}`,
|
|
424
|
+
userKey: `1a2b3c4d-1000-4000-8000-${i.toString().padStart(12, "0")}`,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (decision.outcome === VerificationCodeRateLimitOutcome.Allowed) {
|
|
428
|
+
allowed++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
expect(allowed).toBe(VERIFY_PER_IP_LIMIT);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("keeps separate users apart", async () => {
|
|
436
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 1; i++) {
|
|
437
|
+
await consume({ userKey: USER_A });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const otherUser: VerificationCodeRateLimitDecision = await consume({
|
|
441
|
+
userKey: USER_B,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(otherUser.outcome).toBe(VerificationCodeRateLimitOutcome.Allowed);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
* The item counter is keyed on item AND user, so a caller cannot burn the
|
|
449
|
+
* real owner's allowance by hammering an id they do not own.
|
|
450
|
+
*/
|
|
451
|
+
it("does not let one user spend another user's per-item allowance", async () => {
|
|
452
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 5; i++) {
|
|
453
|
+
await consume({ userKey: USER_B, itemKey: ITEM_A });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
expect(
|
|
457
|
+
(await consume({ userKey: USER_A, itemKey: ITEM_A })).outcome,
|
|
458
|
+
).toBe(VerificationCodeRateLimitOutcome.Allowed);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("counts verify and resend against separate budgets", async () => {
|
|
462
|
+
for (let i: number = 0; i < RESEND_PER_ITEM_LIMIT + 1; i++) {
|
|
463
|
+
await consume({ bucket: VerificationCodeRateLimitBucket.Resend });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
expect(
|
|
467
|
+
(await consume({ bucket: VerificationCodeRateLimitBucket.Verify }))
|
|
468
|
+
.outcome,
|
|
469
|
+
).toBe(VerificationCodeRateLimitOutcome.Allowed);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("holds resends to a tighter budget than verifies", async () => {
|
|
473
|
+
let allowed: number = 0;
|
|
474
|
+
|
|
475
|
+
for (let i: number = 0; i < RESEND_PER_USER_LIMIT + 10; i++) {
|
|
476
|
+
const decision: VerificationCodeRateLimitDecision = await consume({
|
|
477
|
+
itemKey: `1a2b3c4d-0000-4000-8000-${i.toString().padStart(12, "0")}`,
|
|
478
|
+
bucket: VerificationCodeRateLimitBucket.Resend,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
if (decision.outcome === VerificationCodeRateLimitOutcome.Allowed) {
|
|
482
|
+
allowed++;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
expect(allowed).toBe(RESEND_PER_USER_LIMIT);
|
|
487
|
+
expect(RESEND_PER_USER_LIMIT).toBeLessThan(VERIFY_PER_USER_LIMIT);
|
|
488
|
+
expect(RESEND_PER_IP_LIMIT).toBeLessThan(VERIFY_PER_IP_LIMIT);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
/*
|
|
492
|
+
* A rejected request still consumes its counter. Otherwise a caller who
|
|
493
|
+
* keeps hammering is handed a fresh allowance the moment they cross the
|
|
494
|
+
* line, which is the opposite of what a limiter is for.
|
|
495
|
+
*/
|
|
496
|
+
it("keeps counting rejected requests, so hammering does not reset the window", async () => {
|
|
497
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 25; i++) {
|
|
498
|
+
await consume({});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const itemKeyPattern: string = `:t:${USER_A}:${ITEM_A}:`;
|
|
502
|
+
const counterKey: string = Array.from(client.counters.keys()).find(
|
|
503
|
+
(key: string) => {
|
|
504
|
+
return key.includes(itemKeyPattern);
|
|
505
|
+
},
|
|
506
|
+
) as string;
|
|
507
|
+
|
|
508
|
+
expect(client.counters.get(counterKey)).toBe(VERIFY_PER_ITEM_LIMIT + 25);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("windows", () => {
|
|
513
|
+
it("sets a TTL only on the request that created the counter", async () => {
|
|
514
|
+
await consume({});
|
|
515
|
+
await consume({});
|
|
516
|
+
await consume({});
|
|
517
|
+
|
|
518
|
+
const counterKey: string = Array.from(client.counters.keys()).find(
|
|
519
|
+
(key: string) => {
|
|
520
|
+
return key.includes(`:t:${USER_A}:${ITEM_A}:`);
|
|
521
|
+
},
|
|
522
|
+
) as string;
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
* Re-issuing EXPIRE on every increment would slide the window forward
|
|
526
|
+
* for as long as the load continued, so a caller who tripped the limit
|
|
527
|
+
* once could never recover.
|
|
528
|
+
*/
|
|
529
|
+
expect(client.expiresForKey(counterKey)).toHaveLength(1);
|
|
530
|
+
expect(client.expiresForKey(counterKey)[0]?.ttlSeconds).toBe(
|
|
531
|
+
VERIFY_WINDOW_SECONDS * 2,
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("starts a fresh allowance in the next window", async () => {
|
|
536
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 1; i++) {
|
|
537
|
+
await consume({});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
currentTime += VERIFY_WINDOW_SECONDS * 1000;
|
|
541
|
+
|
|
542
|
+
expect((await consume({})).outcome).toBe(
|
|
543
|
+
VerificationCodeRateLimitOutcome.Allowed,
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("tells a rejected caller when the window actually rolls", async () => {
|
|
548
|
+
currentTime += 60 * 1000;
|
|
549
|
+
|
|
550
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 1; i++) {
|
|
551
|
+
await consume({});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const rejected: VerificationCodeRateLimitDecision = await consume({});
|
|
555
|
+
|
|
556
|
+
expect(rejected.retryAfterSeconds).toBe(VERIFY_WINDOW_SECONDS - 60);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe("when Redis is gone", () => {
|
|
561
|
+
/*
|
|
562
|
+
* Fails OPEN, unlike PublicDashboardRateLimit's master-password bucket.
|
|
563
|
+
* The difference is that there the counter IS the only thing bounding
|
|
564
|
+
* guesses; here the expiry, the attempt counter, the rotation and the
|
|
565
|
+
* resend cooldown all live in Postgres and are all still in force, so
|
|
566
|
+
* refusing every verification during a Redis blip would cost real users a
|
|
567
|
+
* great deal to buy a control that is momentarily redundant.
|
|
568
|
+
*/
|
|
569
|
+
it("reports the counter as unavailable when there is no client", async () => {
|
|
570
|
+
getClientMock.mockReturnValue(null);
|
|
571
|
+
|
|
572
|
+
expect((await consume({})).outcome).toBe(
|
|
573
|
+
VerificationCodeRateLimitOutcome.CounterUnavailable,
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("reports the counter as unavailable when Redis is disconnected", async () => {
|
|
578
|
+
isConnectedMock.mockReturnValue(false);
|
|
579
|
+
|
|
580
|
+
expect((await consume({})).outcome).toBe(
|
|
581
|
+
VerificationCodeRateLimitOutcome.CounterUnavailable,
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("reports the counter as unavailable when the pipeline throws", async () => {
|
|
586
|
+
client.failNextExec = new Error("redis is unwell");
|
|
587
|
+
|
|
588
|
+
expect((await consume({})).outcome).toBe(
|
|
589
|
+
VerificationCodeRateLimitOutcome.CounterUnavailable,
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("reports the counter as unavailable on a malformed reply", async () => {
|
|
594
|
+
client.malformedExecResult = [[null, "not-a-number"]];
|
|
595
|
+
|
|
596
|
+
expect((await consume({})).outcome).toBe(
|
|
597
|
+
VerificationCodeRateLimitOutcome.CounterUnavailable,
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("lets the request through rather than blocking verification", async () => {
|
|
602
|
+
getClientMock.mockReturnValue(null);
|
|
603
|
+
|
|
604
|
+
const next: NextFunction = jest.fn() as unknown as NextFunction;
|
|
605
|
+
const built: BuiltResponse = buildResponse();
|
|
606
|
+
|
|
607
|
+
await VerificationCodeRateLimit.getMiddleware(
|
|
608
|
+
VerificationCodeRateLimitBucket.Verify,
|
|
609
|
+
)(
|
|
610
|
+
buildRequest({ body: { itemId: ITEM_A }, userId: USER_A }),
|
|
611
|
+
built.response,
|
|
612
|
+
next,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
expect(next).toHaveBeenCalled();
|
|
616
|
+
expect(sendErrorResponseMock).not.toHaveBeenCalled();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
/*
|
|
620
|
+
* An outage means EVERY request takes this path, so an unguarded log line
|
|
621
|
+
* is one per request for as long as the outage lasts — burying the
|
|
622
|
+
* incident it is reporting.
|
|
623
|
+
*/
|
|
624
|
+
it("does not log once per request during an outage", async () => {
|
|
625
|
+
getClientMock.mockReturnValue(null);
|
|
626
|
+
|
|
627
|
+
const middleware: (
|
|
628
|
+
req: ExpressRequest,
|
|
629
|
+
res: ExpressResponse,
|
|
630
|
+
next: NextFunction,
|
|
631
|
+
) => Promise<void> = VerificationCodeRateLimit.getMiddleware(
|
|
632
|
+
VerificationCodeRateLimitBucket.Verify,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const built: BuiltResponse = buildResponse();
|
|
636
|
+
|
|
637
|
+
for (let i: number = 0; i < 20; i++) {
|
|
638
|
+
await middleware(
|
|
639
|
+
buildRequest({ body: { itemId: ITEM_A }, userId: USER_A }),
|
|
640
|
+
built.response,
|
|
641
|
+
jest.fn() as unknown as NextFunction,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
expect(loggerWarnMock.mock.calls.length).toBeLessThanOrEqual(1);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe("middleware", () => {
|
|
650
|
+
const runMiddleware: (data: {
|
|
651
|
+
bucket: VerificationCodeRateLimitBucket;
|
|
652
|
+
times: number;
|
|
653
|
+
}) => Promise<{
|
|
654
|
+
next: NextFunction;
|
|
655
|
+
headers: Record<string, string>;
|
|
656
|
+
}> = async (data: {
|
|
657
|
+
bucket: VerificationCodeRateLimitBucket;
|
|
658
|
+
times: number;
|
|
659
|
+
}) => {
|
|
660
|
+
const middleware: (
|
|
661
|
+
req: ExpressRequest,
|
|
662
|
+
res: ExpressResponse,
|
|
663
|
+
next: NextFunction,
|
|
664
|
+
) => Promise<void> = VerificationCodeRateLimit.getMiddleware(data.bucket);
|
|
665
|
+
|
|
666
|
+
let next: NextFunction = jest.fn() as unknown as NextFunction;
|
|
667
|
+
const built: BuiltResponse = buildResponse();
|
|
668
|
+
|
|
669
|
+
for (let i: number = 0; i < data.times; i++) {
|
|
670
|
+
next = jest.fn() as unknown as NextFunction;
|
|
671
|
+
|
|
672
|
+
await middleware(
|
|
673
|
+
buildRequest({
|
|
674
|
+
body: { itemId: ITEM_A },
|
|
675
|
+
userId: USER_A,
|
|
676
|
+
headers: { "x-forwarded-for": "203.0.113.7" },
|
|
677
|
+
}),
|
|
678
|
+
built.response,
|
|
679
|
+
next,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return { next, headers: built.headers };
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
it("calls next while the caller is inside the budget", async () => {
|
|
687
|
+
const result: {
|
|
688
|
+
next: NextFunction;
|
|
689
|
+
headers: Record<string, string>;
|
|
690
|
+
} = await runMiddleware({
|
|
691
|
+
bucket: VerificationCodeRateLimitBucket.Verify,
|
|
692
|
+
times: 1,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(result.next).toHaveBeenCalled();
|
|
696
|
+
expect(sendErrorResponseMock).not.toHaveBeenCalled();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("answers 429 and sets Retry-After once the budget is spent", async () => {
|
|
700
|
+
const result: {
|
|
701
|
+
next: NextFunction;
|
|
702
|
+
headers: Record<string, string>;
|
|
703
|
+
} = await runMiddleware({
|
|
704
|
+
bucket: VerificationCodeRateLimitBucket.Verify,
|
|
705
|
+
times: VERIFY_PER_ITEM_LIMIT + 1,
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
expect(result.next).not.toHaveBeenCalled();
|
|
709
|
+
expect(sendErrorResponseMock).toHaveBeenCalled();
|
|
710
|
+
|
|
711
|
+
const exception: Exception = sendErrorResponseMock.mock
|
|
712
|
+
.calls[0]?.[2] as Exception;
|
|
713
|
+
|
|
714
|
+
expect(exception.code).toBe(ExceptionCode.TooManyRequestsException);
|
|
715
|
+
expect(exception.message).toBe(
|
|
716
|
+
"Too many verification attempts. Please try again later.",
|
|
717
|
+
);
|
|
718
|
+
expect(result.headers["Retry-After"]).toBe(String(VERIFY_WINDOW_SECONDS));
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("says something different when it is codes rather than attempts", async () => {
|
|
722
|
+
await runMiddleware({
|
|
723
|
+
bucket: VerificationCodeRateLimitBucket.Resend,
|
|
724
|
+
times: RESEND_PER_ITEM_LIMIT + 1,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const exception: Exception = sendErrorResponseMock.mock
|
|
728
|
+
.calls[0]?.[2] as Exception;
|
|
729
|
+
|
|
730
|
+
expect(exception.message).toBe(
|
|
731
|
+
"Too many verification codes requested. Please try again later.",
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
/*
|
|
736
|
+
* The forged-header case again, this time end to end: a caller who sends
|
|
737
|
+
* a fresh X-Forwarded-For on every request must not land in a fresh
|
|
738
|
+
* bucket every time.
|
|
739
|
+
*/
|
|
740
|
+
it("cannot be escaped by rotating a forged X-Forwarded-For", async () => {
|
|
741
|
+
const middleware: (
|
|
742
|
+
req: ExpressRequest,
|
|
743
|
+
res: ExpressResponse,
|
|
744
|
+
next: NextFunction,
|
|
745
|
+
) => Promise<void> = VerificationCodeRateLimit.getMiddleware(
|
|
746
|
+
VerificationCodeRateLimitBucket.Verify,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const built: BuiltResponse = buildResponse();
|
|
750
|
+
let allowed: number = 0;
|
|
751
|
+
|
|
752
|
+
for (let i: number = 0; i < VERIFY_PER_ITEM_LIMIT + 10; i++) {
|
|
753
|
+
const next: MockedFn = jest.fn();
|
|
754
|
+
|
|
755
|
+
await middleware(
|
|
756
|
+
buildRequest({
|
|
757
|
+
body: { itemId: ITEM_A },
|
|
758
|
+
userId: USER_A,
|
|
759
|
+
headers: {
|
|
760
|
+
"x-forwarded-for": `10.0.0.${i}, 203.0.113.7`,
|
|
761
|
+
},
|
|
762
|
+
}),
|
|
763
|
+
built.response,
|
|
764
|
+
next as unknown as NextFunction,
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
if (next.mock.calls.length > 0) {
|
|
768
|
+
allowed++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
expect(allowed).toBe(VERIFY_PER_ITEM_LIMIT);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|