@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,586 @@
|
|
|
1
|
+
import Redis, { ClientType } from "../Infrastructure/Redis";
|
|
2
|
+
import {
|
|
3
|
+
ExpressRequest,
|
|
4
|
+
ExpressResponse,
|
|
5
|
+
NextFunction,
|
|
6
|
+
OneUptimeRequest,
|
|
7
|
+
} from "../Utils/Express";
|
|
8
|
+
import logger from "../Utils/Logger";
|
|
9
|
+
import resolveTrustedClientIp, { ClientIpRequestLike } from "../Utils/ClientIp";
|
|
10
|
+
import Response from "../Utils/Response";
|
|
11
|
+
import ObjectID from "../../Types/ObjectID";
|
|
12
|
+
import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Request limiting for the notification-channel verification routes:
|
|
16
|
+
* /user-email/verify, /user-sms/verify, /user-call/verify,
|
|
17
|
+
* /user-whats-app/verify, /user-incoming-call-number/verify and each of their
|
|
18
|
+
* resend siblings.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS IS FOR, GIVEN THE DATABASE ALREADY COUNTS ATTEMPTS
|
|
21
|
+
*
|
|
22
|
+
* The durable controls in ChannelVerification bound guessing against ONE
|
|
23
|
+
* issued code: five attempts, then the code is burned. This middleware bounds
|
|
24
|
+
* the things a per-row counter cannot see.
|
|
25
|
+
*
|
|
26
|
+
* ROW ROTATION. Nothing stops a caller creating a hundred channel rows —
|
|
27
|
+
* that is the ordinary "add my phone" flow — and each new row is a fresh
|
|
28
|
+
* code with a fresh attempt budget. Five guesses per row multiplied by
|
|
29
|
+
* however many rows the caller cares to create is not bounded by anything
|
|
30
|
+
* on the row. The per-user and per-address counters are.
|
|
31
|
+
*
|
|
32
|
+
* MESSAGE VOLUME. Every issued code is a real email, SMS, voice call or
|
|
33
|
+
* WhatsApp message to somebody who did not ask for it, billed to the
|
|
34
|
+
* project. The resend bucket bounds that independently of whether any
|
|
35
|
+
* individual row is inside its own cooldown.
|
|
36
|
+
*
|
|
37
|
+
* COST BEFORE THE CHECK. A verify request costs a session lookup and a row
|
|
38
|
+
* read before the attempt counter is even consulted. This runs first.
|
|
39
|
+
*
|
|
40
|
+
* THIS FAILS OPEN, DELIBERATELY
|
|
41
|
+
*
|
|
42
|
+
* If Redis is unreachable, requests are allowed through with a throttled
|
|
43
|
+
* warning. That is the opposite of the choice PublicDashboardRateLimit makes
|
|
44
|
+
* for /master-password, and the difference is that there the counter IS the
|
|
45
|
+
* only thing bounding guesses, whereas here the expiry, the attempt counter,
|
|
46
|
+
* the rotation and the resend cooldown all live in Postgres and are all still
|
|
47
|
+
* in force. Failing closed would mean nobody can verify a new phone number
|
|
48
|
+
* during a Redis blip, in exchange for a control that is at that moment
|
|
49
|
+
* redundant with four others.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
export enum VerificationCodeRateLimitBucket {
|
|
53
|
+
/* Submitting a code. The guessing surface. */
|
|
54
|
+
Verify = "verify",
|
|
55
|
+
|
|
56
|
+
/* Asking for a new code. The spending-somebody-else's-attention surface. */
|
|
57
|
+
Resend = "resend",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export enum VerificationCodeRateLimitScope {
|
|
61
|
+
/* One channel row, for one user. */
|
|
62
|
+
Item = "item",
|
|
63
|
+
|
|
64
|
+
/* One signed-in user, across every row they own. Survives row rotation. */
|
|
65
|
+
User = "user",
|
|
66
|
+
|
|
67
|
+
/* One client address, across every account. Survives account rotation. */
|
|
68
|
+
Ip = "ip",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export enum VerificationCodeRateLimitOutcome {
|
|
72
|
+
Allowed = "allowed",
|
|
73
|
+
RateLimited = "rate-limited",
|
|
74
|
+
|
|
75
|
+
/* Redis is unreachable, so no limit can be honoured either way. */
|
|
76
|
+
CounterUnavailable = "counter-unavailable",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VerificationCodeRateLimitDecision {
|
|
80
|
+
outcome: VerificationCodeRateLimitOutcome;
|
|
81
|
+
retryAfterSeconds?: number | undefined;
|
|
82
|
+
|
|
83
|
+
/* Which counter rejected, for logs. Unset unless rejected. */
|
|
84
|
+
scope?: VerificationCodeRateLimitScope | undefined;
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* True only on the request that first crossed the line in this window, so a
|
|
88
|
+
* flood produces one log line per key per window rather than one per
|
|
89
|
+
* request.
|
|
90
|
+
*/
|
|
91
|
+
isFirstRejectionInWindow?: boolean | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsePositiveIntFromEnv: (envKey: string, fallback: number) => number = (
|
|
95
|
+
envKey: string,
|
|
96
|
+
fallback: number,
|
|
97
|
+
): number => {
|
|
98
|
+
const rawValue: string | undefined = process.env[envKey];
|
|
99
|
+
|
|
100
|
+
if (!rawValue) {
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsedValue: number = parseInt(rawValue, 10);
|
|
105
|
+
|
|
106
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return parsedValue;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
interface BucketConfig {
|
|
114
|
+
windowSeconds: number;
|
|
115
|
+
perItemLimit: number;
|
|
116
|
+
perUserLimit: number;
|
|
117
|
+
perIpLimit: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* Verify budget, over a window matching the code's own lifetime.
|
|
122
|
+
*
|
|
123
|
+
* Per item is 10 against a per-code attempt limit of 5, so it never fires
|
|
124
|
+
* before the durable counter does for a single honest code — it is there to
|
|
125
|
+
* bound the requests that arrive after the row is already locked out.
|
|
126
|
+
*
|
|
127
|
+
* Per user is the one that closes row rotation: 50 submissions per quarter
|
|
128
|
+
* hour across every channel row the account owns. A person adding a phone
|
|
129
|
+
* number and mistyping the code a few times is nowhere near it; a script
|
|
130
|
+
* creating rows to farm attempt budgets hits it after ten rows.
|
|
131
|
+
*
|
|
132
|
+
* Per address is higher than per user because a corporate NAT can put a whole
|
|
133
|
+
* office behind one address, and several colleagues onboarding at once is a
|
|
134
|
+
* real thing that must not be refused.
|
|
135
|
+
*/
|
|
136
|
+
const VERIFY_BUCKET: BucketConfig = {
|
|
137
|
+
windowSeconds: parsePositiveIntFromEnv(
|
|
138
|
+
"NOTIFICATION_VERIFICATION_RATE_LIMIT_WINDOW_SECONDS",
|
|
139
|
+
15 * 60,
|
|
140
|
+
),
|
|
141
|
+
perItemLimit: parsePositiveIntFromEnv(
|
|
142
|
+
"NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_ITEM_PER_WINDOW",
|
|
143
|
+
10,
|
|
144
|
+
),
|
|
145
|
+
perUserLimit: parsePositiveIntFromEnv(
|
|
146
|
+
"NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_USER_PER_WINDOW",
|
|
147
|
+
50,
|
|
148
|
+
),
|
|
149
|
+
perIpLimit: parsePositiveIntFromEnv(
|
|
150
|
+
"NOTIFICATION_VERIFICATION_RATE_LIMIT_PER_IP_PER_WINDOW",
|
|
151
|
+
150,
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/*
|
|
156
|
+
* Resend budget. Tighter than verify, because each one of these is a message
|
|
157
|
+
* that arrives on somebody's phone and a line on somebody's bill. The
|
|
158
|
+
* per-item figure sits just above what the 60-second per-row cooldown allows
|
|
159
|
+
* in the window, so an honest user pressing "resend" whenever the cooldown
|
|
160
|
+
* lets them is never refused by this.
|
|
161
|
+
*/
|
|
162
|
+
const RESEND_BUCKET: BucketConfig = {
|
|
163
|
+
windowSeconds: parsePositiveIntFromEnv(
|
|
164
|
+
"NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_WINDOW_SECONDS",
|
|
165
|
+
15 * 60,
|
|
166
|
+
),
|
|
167
|
+
perItemLimit: parsePositiveIntFromEnv(
|
|
168
|
+
"NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_ITEM_PER_WINDOW",
|
|
169
|
+
5,
|
|
170
|
+
),
|
|
171
|
+
perUserLimit: parsePositiveIntFromEnv(
|
|
172
|
+
"NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_USER_PER_WINDOW",
|
|
173
|
+
15,
|
|
174
|
+
),
|
|
175
|
+
perIpLimit: parsePositiveIntFromEnv(
|
|
176
|
+
"NOTIFICATION_VERIFICATION_RESEND_RATE_LIMIT_PER_IP_PER_WINDOW",
|
|
177
|
+
45,
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const KEY_PREFIX: string = "notifverify:rl:";
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* Counter keys outlive their window by one full window, so a request landing
|
|
185
|
+
* on a boundary cannot read a key that was reclaimed mid-window.
|
|
186
|
+
*/
|
|
187
|
+
const TTL_MULTIPLIER: number = 2;
|
|
188
|
+
|
|
189
|
+
/* Bounds a Redis key built from caller-supplied values. */
|
|
190
|
+
const MAX_KEY_SEGMENT_LENGTH: number = 64;
|
|
191
|
+
|
|
192
|
+
/* How often the "counter unavailable" condition may be logged, per bucket. */
|
|
193
|
+
const COUNTER_UNAVAILABLE_LOG_INTERVAL_MS: number = 60 * 1000;
|
|
194
|
+
|
|
195
|
+
export default class VerificationCodeRateLimit {
|
|
196
|
+
/*
|
|
197
|
+
* The client address to bill this request to.
|
|
198
|
+
*
|
|
199
|
+
* Uses the shared ClientIp helper, which reads X-Forwarded-For from the
|
|
200
|
+
* trusted right-hand end. Express.getClientIp takes the LEFTMOST entry,
|
|
201
|
+
* which the caller can set themselves — for a rate limiter that is fatal,
|
|
202
|
+
* because a fresh spoofed value per request is a fresh bucket per request
|
|
203
|
+
* and therefore no limit at all.
|
|
204
|
+
*/
|
|
205
|
+
public static resolveClientIp(req: ExpressRequest): string {
|
|
206
|
+
const clientIp: string | undefined = resolveTrustedClientIp(
|
|
207
|
+
req as unknown as ClientIpRequestLike,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (clientIp) {
|
|
211
|
+
return VerificationCodeRateLimit.sanitizeKeySegment(clientIp);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* Everything with no resolvable address shares one bucket. That is the
|
|
216
|
+
* conservative direction: an unidentifiable caller should not be handed a
|
|
217
|
+
* private allowance.
|
|
218
|
+
*/
|
|
219
|
+
return "unknown";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/*
|
|
223
|
+
* The signed-in user this request belongs to.
|
|
224
|
+
*
|
|
225
|
+
* These routes all sit behind UserMiddleware, so in practice this is always
|
|
226
|
+
* present. If it somehow is not, everything anonymous shares one bucket
|
|
227
|
+
* rather than each getting its own.
|
|
228
|
+
*/
|
|
229
|
+
public static resolveUserKey(req: ExpressRequest): string {
|
|
230
|
+
const userId: ObjectID | undefined = (req as OneUptimeRequest)
|
|
231
|
+
?.userAuthorization?.userId;
|
|
232
|
+
|
|
233
|
+
if (!userId) {
|
|
234
|
+
return "anonymous";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return VerificationCodeRateLimit.sanitizeKeySegment(userId.toString());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
* The channel row this request is about.
|
|
242
|
+
*
|
|
243
|
+
* Anything that is not a valid id collapses into one shared "invalid"
|
|
244
|
+
* bucket. That bounds Redis memory against a caller feeding junk ids, and
|
|
245
|
+
* it is the right grouping anyway: those requests cannot name a real row,
|
|
246
|
+
* so one small shared allowance between all of them is what we want.
|
|
247
|
+
*/
|
|
248
|
+
public static resolveItemKey(req: ExpressRequest): string {
|
|
249
|
+
const raw: unknown = (req.body as Record<string, unknown> | undefined)?.[
|
|
250
|
+
"itemId"
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
254
|
+
return "none";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const value: string = raw.trim();
|
|
258
|
+
|
|
259
|
+
if (!ObjectID.isValidUUID(value)) {
|
|
260
|
+
return "invalid";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return value.toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static sanitizeKeySegment(value: string): string {
|
|
267
|
+
return (
|
|
268
|
+
value
|
|
269
|
+
.slice(0, MAX_KEY_SEGMENT_LENGTH)
|
|
270
|
+
/*
|
|
271
|
+
* Redis keys are binary safe, but a predictable charset keeps
|
|
272
|
+
* operational tooling (SCAN patterns, dashboards) sane.
|
|
273
|
+
*/
|
|
274
|
+
.replace(/[^a-zA-Z0-9._:%\-[\]]/g, "_")
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public static getBucketConfig(
|
|
279
|
+
bucket: VerificationCodeRateLimitBucket,
|
|
280
|
+
): BucketConfig {
|
|
281
|
+
return bucket === VerificationCodeRateLimitBucket.Resend
|
|
282
|
+
? RESEND_BUCKET
|
|
283
|
+
: VERIFY_BUCKET;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/*
|
|
287
|
+
* Increment all three counters for this request and decide.
|
|
288
|
+
*
|
|
289
|
+
* All three INCRs go out in one pipeline so the common path is a single
|
|
290
|
+
* round trip; the EXPIRE follow-ups only happen on the request that created
|
|
291
|
+
* a key.
|
|
292
|
+
*
|
|
293
|
+
* Every counter is incremented on every request, INCLUDING a rejected one.
|
|
294
|
+
* That is standard fixed-window behaviour and it is deliberate: a caller
|
|
295
|
+
* who keeps hammering keeps their window pinned rather than being handed a
|
|
296
|
+
* fresh allowance for free.
|
|
297
|
+
*/
|
|
298
|
+
public static async consume(data: {
|
|
299
|
+
itemKey: string;
|
|
300
|
+
userKey: string;
|
|
301
|
+
clientIp: string;
|
|
302
|
+
bucket: VerificationCodeRateLimitBucket;
|
|
303
|
+
}): Promise<VerificationCodeRateLimitDecision> {
|
|
304
|
+
const client: ClientType | null = Redis.getClient();
|
|
305
|
+
|
|
306
|
+
if (!client || !Redis.isConnected()) {
|
|
307
|
+
return { outcome: VerificationCodeRateLimitOutcome.CounterUnavailable };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const config: BucketConfig = VerificationCodeRateLimit.getBucketConfig(
|
|
311
|
+
data.bucket,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const windowMs: number = config.windowSeconds * 1000;
|
|
315
|
+
const windowIndex: number = Math.floor(Date.now() / windowMs);
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
* The item counter is keyed on item AND user. Two users cannot own the
|
|
319
|
+
* same row, so this changes nothing about correctness — but it means a
|
|
320
|
+
* caller cannot deny a row's real owner their allowance by burning it
|
|
321
|
+
* against an id they do not own.
|
|
322
|
+
*/
|
|
323
|
+
const itemCounterKey: string = `${KEY_PREFIX}${data.bucket}:t:${data.userKey}:${data.itemKey}:${windowIndex}`;
|
|
324
|
+
const userCounterKey: string = `${KEY_PREFIX}${data.bucket}:u:${data.userKey}:${windowIndex}`;
|
|
325
|
+
const ipCounterKey: string = `${KEY_PREFIX}${data.bucket}:i:${data.clientIp}:${windowIndex}`;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const pipelineResults: Array<[Error | null, unknown]> | null =
|
|
329
|
+
(await client
|
|
330
|
+
.pipeline()
|
|
331
|
+
.incr(itemCounterKey)
|
|
332
|
+
.incr(userCounterKey)
|
|
333
|
+
.incr(ipCounterKey)
|
|
334
|
+
.exec()) as Array<[Error | null, unknown]> | null;
|
|
335
|
+
|
|
336
|
+
if (!pipelineResults || pipelineResults.length < 3) {
|
|
337
|
+
throw new Error("Rate limit pipeline returned no result");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const itemCount: number = VerificationCodeRateLimit.readCounterResult(
|
|
341
|
+
pipelineResults[0],
|
|
342
|
+
);
|
|
343
|
+
const userCount: number = VerificationCodeRateLimit.readCounterResult(
|
|
344
|
+
pipelineResults[1],
|
|
345
|
+
);
|
|
346
|
+
const ipCount: number = VerificationCodeRateLimit.readCounterResult(
|
|
347
|
+
pipelineResults[2],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
/*
|
|
351
|
+
* Set the expiry only on the write that created the key. Re-issuing
|
|
352
|
+
* EXPIRE on every increment would slide the window forward for as long
|
|
353
|
+
* as the load continues, so a caller who tripped the limit once could
|
|
354
|
+
* never recover.
|
|
355
|
+
*/
|
|
356
|
+
const ttlSeconds: number = config.windowSeconds * TTL_MULTIPLIER;
|
|
357
|
+
const keysToExpire: Array<string> = [];
|
|
358
|
+
|
|
359
|
+
if (itemCount === 1) {
|
|
360
|
+
keysToExpire.push(itemCounterKey);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (userCount === 1) {
|
|
364
|
+
keysToExpire.push(userCounterKey);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (ipCount === 1) {
|
|
368
|
+
keysToExpire.push(ipCounterKey);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (keysToExpire.length > 0) {
|
|
372
|
+
const expirePipeline: ReturnType<ClientType["pipeline"]> =
|
|
373
|
+
client.pipeline();
|
|
374
|
+
|
|
375
|
+
for (const key of keysToExpire) {
|
|
376
|
+
expirePipeline.expire(key, ttlSeconds);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await expirePipeline.exec();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const retryAfterSeconds: number =
|
|
383
|
+
VerificationCodeRateLimit.getSecondsUntilWindowEnd(
|
|
384
|
+
config.windowSeconds,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* Reported most specific first, because that is the most useful thing
|
|
389
|
+
* to see in a log line when more than one is over.
|
|
390
|
+
*/
|
|
391
|
+
if (itemCount > config.perItemLimit) {
|
|
392
|
+
return {
|
|
393
|
+
outcome: VerificationCodeRateLimitOutcome.RateLimited,
|
|
394
|
+
retryAfterSeconds,
|
|
395
|
+
scope: VerificationCodeRateLimitScope.Item,
|
|
396
|
+
isFirstRejectionInWindow: itemCount === config.perItemLimit + 1,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (userCount > config.perUserLimit) {
|
|
401
|
+
return {
|
|
402
|
+
outcome: VerificationCodeRateLimitOutcome.RateLimited,
|
|
403
|
+
retryAfterSeconds,
|
|
404
|
+
scope: VerificationCodeRateLimitScope.User,
|
|
405
|
+
isFirstRejectionInWindow: userCount === config.perUserLimit + 1,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (ipCount > config.perIpLimit) {
|
|
410
|
+
return {
|
|
411
|
+
outcome: VerificationCodeRateLimitOutcome.RateLimited,
|
|
412
|
+
retryAfterSeconds,
|
|
413
|
+
scope: VerificationCodeRateLimitScope.Ip,
|
|
414
|
+
isFirstRejectionInWindow: ipCount === config.perIpLimit + 1,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { outcome: VerificationCodeRateLimitOutcome.Allowed };
|
|
419
|
+
} catch (err) {
|
|
420
|
+
/*
|
|
421
|
+
* Throttled: whatever broke Redis is unlikely to have broken it for one
|
|
422
|
+
* request only, and a per-request log line buries the incident it is
|
|
423
|
+
* reporting.
|
|
424
|
+
*/
|
|
425
|
+
if (VerificationCodeRateLimit.shouldLogCounterUnavailable(data.bucket)) {
|
|
426
|
+
logger.warn(
|
|
427
|
+
`VerificationCodeRateLimit: counter failed for ${data.bucket} request from user ${data.userKey}`,
|
|
428
|
+
);
|
|
429
|
+
logger.warn(err);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { outcome: VerificationCodeRateLimitOutcome.CounterUnavailable };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private static readCounterResult(
|
|
437
|
+
result: [Error | null, unknown] | undefined,
|
|
438
|
+
): number {
|
|
439
|
+
if (!result) {
|
|
440
|
+
throw new Error("Rate limit counter returned no result");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const [error, value] = result;
|
|
444
|
+
|
|
445
|
+
if (error) {
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (typeof value !== "number") {
|
|
450
|
+
throw new Error("Rate limit counter returned a non-numeric value");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/*
|
|
457
|
+
* Remaining seconds in the current fixed window, so every rejected caller
|
|
458
|
+
* is told to come back when the window actually rolls rather than all
|
|
459
|
+
* backing off by the same constant and re-colliding.
|
|
460
|
+
*/
|
|
461
|
+
private static getSecondsUntilWindowEnd(windowSeconds: number): number {
|
|
462
|
+
const windowMs: number = windowSeconds * 1000;
|
|
463
|
+
const msIntoWindow: number = Date.now() % windowMs;
|
|
464
|
+
|
|
465
|
+
return Math.max(1, Math.ceil((windowMs - msIntoWindow) / 1000));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/*
|
|
469
|
+
* Express middleware. Register AFTER UserMiddleware — the user counter is
|
|
470
|
+
* the one that closes row rotation, and it needs the session — but before
|
|
471
|
+
* the route handler, so a flood is refused before it costs a row read.
|
|
472
|
+
*/
|
|
473
|
+
public static getMiddleware(
|
|
474
|
+
bucket: VerificationCodeRateLimitBucket,
|
|
475
|
+
): (
|
|
476
|
+
req: ExpressRequest,
|
|
477
|
+
res: ExpressResponse,
|
|
478
|
+
next: NextFunction,
|
|
479
|
+
) => Promise<void> {
|
|
480
|
+
return async (
|
|
481
|
+
req: ExpressRequest,
|
|
482
|
+
res: ExpressResponse,
|
|
483
|
+
next: NextFunction,
|
|
484
|
+
): Promise<void> => {
|
|
485
|
+
const itemKey: string = VerificationCodeRateLimit.resolveItemKey(req);
|
|
486
|
+
const userKey: string = VerificationCodeRateLimit.resolveUserKey(req);
|
|
487
|
+
const clientIp: string = VerificationCodeRateLimit.resolveClientIp(req);
|
|
488
|
+
|
|
489
|
+
const decision: VerificationCodeRateLimitDecision =
|
|
490
|
+
await VerificationCodeRateLimit.consume({
|
|
491
|
+
itemKey,
|
|
492
|
+
userKey,
|
|
493
|
+
clientIp,
|
|
494
|
+
bucket,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
if (decision.outcome === VerificationCodeRateLimitOutcome.RateLimited) {
|
|
498
|
+
if (decision.retryAfterSeconds) {
|
|
499
|
+
VerificationCodeRateLimit.setRetryAfterHeader(
|
|
500
|
+
res,
|
|
501
|
+
decision.retryAfterSeconds,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (decision.isFirstRejectionInWindow) {
|
|
506
|
+
logger.warn(
|
|
507
|
+
`VerificationCodeRateLimit: rejected ${bucket} request for item ${itemKey} from user ${userKey} at ${clientIp} (${decision.scope} limit)`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return Response.sendErrorResponse(
|
|
512
|
+
req,
|
|
513
|
+
res,
|
|
514
|
+
new TooManyRequestsException(
|
|
515
|
+
bucket === VerificationCodeRateLimitBucket.Resend
|
|
516
|
+
? "Too many verification codes requested. Please try again later."
|
|
517
|
+
: "Too many verification attempts. Please try again later.",
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
decision.outcome === VerificationCodeRateLimitOutcome.CounterUnavailable
|
|
524
|
+
) {
|
|
525
|
+
/*
|
|
526
|
+
* Fails open. See the note at the top of this file: the expiry, the
|
|
527
|
+
* attempt counter, the rotation and the resend cooldown are all in
|
|
528
|
+
* Postgres and all still in force, so allowing the request through
|
|
529
|
+
* costs a bounded amount of extra attempt budget rather than an
|
|
530
|
+
* unlimited guessing oracle.
|
|
531
|
+
*/
|
|
532
|
+
if (VerificationCodeRateLimit.shouldLogCounterUnavailable(bucket)) {
|
|
533
|
+
logger.warn(
|
|
534
|
+
`VerificationCodeRateLimit: rate limit counter unavailable, allowing ${bucket} requests unthrottled`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return next();
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/*
|
|
544
|
+
* A Redis outage means EVERY request takes the unavailable path, so an
|
|
545
|
+
* unguarded log line there is one per request for as long as the outage
|
|
546
|
+
* lasts. One line per bucket per interval makes the condition visible
|
|
547
|
+
* without burying everything else.
|
|
548
|
+
*/
|
|
549
|
+
private static shouldLogCounterUnavailable(
|
|
550
|
+
bucket: VerificationCodeRateLimitBucket,
|
|
551
|
+
): boolean {
|
|
552
|
+
const now: number = Date.now();
|
|
553
|
+
const lastLoggedAt: number =
|
|
554
|
+
VerificationCodeRateLimit.counterUnavailableLastLoggedAt.get(bucket) || 0;
|
|
555
|
+
|
|
556
|
+
if (now - lastLoggedAt < COUNTER_UNAVAILABLE_LOG_INTERVAL_MS) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
VerificationCodeRateLimit.counterUnavailableLastLoggedAt.set(bucket, now);
|
|
561
|
+
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private static counterUnavailableLastLoggedAt: Map<
|
|
566
|
+
VerificationCodeRateLimitBucket,
|
|
567
|
+
number
|
|
568
|
+
> = new Map();
|
|
569
|
+
|
|
570
|
+
private static setRetryAfterHeader(
|
|
571
|
+
res: ExpressResponse,
|
|
572
|
+
retryAfterSeconds: number,
|
|
573
|
+
): void {
|
|
574
|
+
const setHeader: unknown = (res as unknown as Record<string, unknown>)[
|
|
575
|
+
"setHeader"
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
if (typeof setHeader === "function") {
|
|
579
|
+
(setHeader as (name: string, value: string) => void).call(
|
|
580
|
+
res,
|
|
581
|
+
"Retry-After",
|
|
582
|
+
String(retryAfterSeconds),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|