@oneuptime/common 14.0.7 → 14.0.8
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/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1535 -65
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +68 -18
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1052 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -74,6 +74,17 @@ import TooManyRequestsException from "../../Types/Exception/TooManyRequestsExcep
|
|
|
74
74
|
* DatabaseNotConnectedException when Redis is down. Logging in is already
|
|
75
75
|
* impossible in that state; the difference this makes is a 503 that says to
|
|
76
76
|
* come back shortly instead of a 500.
|
|
77
|
+
*
|
|
78
|
+
* ONE ROUTE HERE SENDS MAIL RATHER THAN CHECKING A CREDENTIAL
|
|
79
|
+
*
|
|
80
|
+
* POST /resend-verification-email (Authentication.ts) is limited by this
|
|
81
|
+
* middleware too, on a bucket of its own. It is not a guessing oracle against
|
|
82
|
+
* a stored credential -- the credential it takes is server-minted and signed,
|
|
83
|
+
* or a random UUID from an emailed link -- but every request that gets past its
|
|
84
|
+
* checks makes the server send an email, and an anonymous route that sends
|
|
85
|
+
* mail needs a ceiling in front of its database reads and its caps. It shares
|
|
86
|
+
* the fail-closed choice for the same reason: its own cooldown fence lives in
|
|
87
|
+
* Redis, so with Redis down it could not bound anything either.
|
|
77
88
|
*/
|
|
78
89
|
|
|
79
90
|
/*
|
|
@@ -144,6 +155,15 @@ export enum IdentityRateLimitBucket {
|
|
|
144
155
|
|
|
145
156
|
// Discoverable passkey requests have no account identifier until verified.
|
|
146
157
|
Passkey = "passkey",
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* POST /resend-verification-email -- an unverified account asking for its
|
|
161
|
+
* verification link again. Mail, not guessing: see
|
|
162
|
+
* VERIFICATION_EMAIL_RESEND_BUCKET. Its own counter so that a user mashing
|
|
163
|
+
* "resend" cannot spend the sign-in attempts they are about to need, and so
|
|
164
|
+
* that sign-in noise cannot refuse the resend.
|
|
165
|
+
*/
|
|
166
|
+
VerificationEmailResend = "verification-email-resend",
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
export enum IdentityRateLimitScope {
|
|
@@ -413,6 +433,55 @@ const PASSKEY_BUCKET: BucketConfig = {
|
|
|
413
433
|
perIpLimit: PASSKEY_RATE_LIMIT,
|
|
414
434
|
};
|
|
415
435
|
|
|
436
|
+
/*
|
|
437
|
+
* Verification email resend budget.
|
|
438
|
+
*
|
|
439
|
+
* Shaped like the passkey bucket, and for the same reason: the request carries
|
|
440
|
+
* no email address (it carries a signed resend token or a verification link
|
|
441
|
+
* token instead), so resolveAccountKey answers "none" and the account counter
|
|
442
|
+
* is keyed on the client address alone. Both counters are therefore the same
|
|
443
|
+
* per-address budget and are given the same number; a caller who adds an
|
|
444
|
+
* `email` field to rotate the account key still meets the address ceiling.
|
|
445
|
+
*
|
|
446
|
+
* This is NOT what bounds mail to any one inbox. The route's own caps do that
|
|
447
|
+
* per account -- a cooldown between sends, a per-hour cap, and a small fixed
|
|
448
|
+
* number of sends per credential (VerificationEmailResendPolicy) -- and those
|
|
449
|
+
* hold however many addresses a caller has. What this bounds is how much work
|
|
450
|
+
* one address can make the route do before those caps are even consulted:
|
|
451
|
+
* token verification, a verification-token lookup, a user lookup and a Redis
|
|
452
|
+
* read per request.
|
|
453
|
+
*
|
|
454
|
+
* Twenty a quarter hour is far past an honest user, who presses the button a
|
|
455
|
+
* handful of times at most because the page itself counts the cooldown down,
|
|
456
|
+
* and still generous to an office behind one NAT where several people are
|
|
457
|
+
* finishing signup at once.
|
|
458
|
+
*/
|
|
459
|
+
const VERIFICATION_EMAIL_RESEND_RATE_LIMIT: number = parsePositiveIntFromEnv(
|
|
460
|
+
"IDENTITY_VERIFICATION_EMAIL_RESEND_RATE_LIMIT_PER_IP_PER_WINDOW",
|
|
461
|
+
20,
|
|
462
|
+
);
|
|
463
|
+
const VERIFICATION_EMAIL_RESEND_BUCKET: BucketConfig = {
|
|
464
|
+
windowSeconds: parsePositiveIntFromEnv(
|
|
465
|
+
"IDENTITY_VERIFICATION_EMAIL_RESEND_RATE_LIMIT_WINDOW_SECONDS",
|
|
466
|
+
15 * 60,
|
|
467
|
+
),
|
|
468
|
+
perAccountLimit: VERIFICATION_EMAIL_RESEND_RATE_LIMIT,
|
|
469
|
+
perIpLimit: VERIFICATION_EMAIL_RESEND_RATE_LIMIT,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
* What a refused caller is told. See getRefusalMessages for why the sign-in
|
|
474
|
+
* routes all share one pair and the resend route has its own.
|
|
475
|
+
*/
|
|
476
|
+
export const IDENTITY_RATE_LIMITED_MESSAGE: string =
|
|
477
|
+
"Too many sign-in attempts. Please try again later.";
|
|
478
|
+
export const IDENTITY_COUNTER_UNAVAILABLE_MESSAGE: string =
|
|
479
|
+
"Unable to sign you in right now. Please try again shortly.";
|
|
480
|
+
export const VERIFICATION_EMAIL_RESEND_RATE_LIMITED_MESSAGE: string =
|
|
481
|
+
"Too many requests for a new verification email. Please try again later.";
|
|
482
|
+
export const VERIFICATION_EMAIL_RESEND_UNAVAILABLE_MESSAGE: string =
|
|
483
|
+
"Unable to send a verification email right now. Please try again shortly.";
|
|
484
|
+
|
|
416
485
|
const KEY_PREFIX: string = "identity:rl:";
|
|
417
486
|
|
|
418
487
|
/*
|
|
@@ -574,9 +643,44 @@ export default class IdentityRateLimit {
|
|
|
574
643
|
return STATUS_PAGE_LOGIN_BUCKET;
|
|
575
644
|
}
|
|
576
645
|
|
|
646
|
+
if (bucket === IdentityRateLimitBucket.VerificationEmailResend) {
|
|
647
|
+
return VERIFICATION_EMAIL_RESEND_BUCKET;
|
|
648
|
+
}
|
|
649
|
+
|
|
577
650
|
return LOGIN_BUCKET;
|
|
578
651
|
}
|
|
579
652
|
|
|
653
|
+
/*
|
|
654
|
+
* What a refused request is told, for the 429 and for the fail-closed 503.
|
|
655
|
+
*
|
|
656
|
+
* Every sign-in bucket gets the same pair, byte for byte, whichever counter
|
|
657
|
+
* fired and whichever route was hit -- see the note in getMiddleware on why
|
|
658
|
+
* a sign-in limiter must not answer differently for different accounts.
|
|
659
|
+
*
|
|
660
|
+
* The resend route gets its own pair because the sign-in wording is simply
|
|
661
|
+
* wrong there: somebody who pressed "resend verification email" has made no
|
|
662
|
+
* sign-in attempt, and telling them they made too many would send them
|
|
663
|
+
* looking for a problem they do not have. The pair is still uniform WITHIN
|
|
664
|
+
* the route: the same text for every caller, token and account, so it
|
|
665
|
+
* discloses nothing the sign-in pair does not.
|
|
666
|
+
*/
|
|
667
|
+
public static getRefusalMessages(bucket: IdentityRateLimitBucket): {
|
|
668
|
+
rateLimited: string;
|
|
669
|
+
unavailable: string;
|
|
670
|
+
} {
|
|
671
|
+
if (bucket === IdentityRateLimitBucket.VerificationEmailResend) {
|
|
672
|
+
return {
|
|
673
|
+
rateLimited: VERIFICATION_EMAIL_RESEND_RATE_LIMITED_MESSAGE,
|
|
674
|
+
unavailable: VERIFICATION_EMAIL_RESEND_UNAVAILABLE_MESSAGE,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
rateLimited: IDENTITY_RATE_LIMITED_MESSAGE,
|
|
680
|
+
unavailable: IDENTITY_COUNTER_UNAVAILABLE_MESSAGE,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
580
684
|
/*
|
|
581
685
|
* Increment both counters for this request and decide.
|
|
582
686
|
*
|
|
@@ -773,13 +877,14 @@ export default class IdentityRateLimit {
|
|
|
773
877
|
* account exists. These routes are careful not to say which half of a
|
|
774
878
|
* credential was wrong; a limiter that answered differently for a real
|
|
775
879
|
* address than for an invented one would hand back the account
|
|
776
|
-
* enumeration the handlers withhold.
|
|
880
|
+
* enumeration the handlers withhold. (The resend route has wording of
|
|
881
|
+
* its own, but it is just as uniform; see getRefusalMessages.)
|
|
777
882
|
*/
|
|
778
883
|
return Response.sendErrorResponse(
|
|
779
884
|
req,
|
|
780
885
|
res,
|
|
781
886
|
new TooManyRequestsException(
|
|
782
|
-
|
|
887
|
+
IdentityRateLimit.getRefusalMessages(bucket).rateLimited,
|
|
783
888
|
),
|
|
784
889
|
);
|
|
785
890
|
}
|
|
@@ -800,7 +905,7 @@ export default class IdentityRateLimit {
|
|
|
800
905
|
req,
|
|
801
906
|
res,
|
|
802
907
|
new ServiceUnavailableException(
|
|
803
|
-
|
|
908
|
+
IdentityRateLimit.getRefusalMessages(bucket).unavailable,
|
|
804
909
|
),
|
|
805
910
|
);
|
|
806
911
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import Redis, { ClientType } from "../Infrastructure/Redis";
|
|
2
|
+
import {
|
|
3
|
+
ExpressRequest,
|
|
4
|
+
ExpressResponse,
|
|
5
|
+
NextFunction,
|
|
6
|
+
} from "../Utils/Express";
|
|
7
|
+
import logger from "../Utils/Logger";
|
|
8
|
+
import Response from "../Utils/Response";
|
|
9
|
+
import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
|
|
10
|
+
import SubscriberNotificationTestSendRateLimit from "./SubscriberNotificationTestSendRateLimit";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Request limiting for "Preview notification" (POST /preview on the
|
|
14
|
+
* subscriber notification preview router).
|
|
15
|
+
*
|
|
16
|
+
* A preview sends nothing, but it is not free: it renders the incident's
|
|
17
|
+
* description and custom fields once, then an email for every status page
|
|
18
|
+
* the caller can read, and returns all of that HTML in one answer. Any
|
|
19
|
+
* project member holding one of the audience roles may call it, as often as
|
|
20
|
+
* they like, so this bounds how much rendering one person can ask the App
|
|
21
|
+
* server for. The request itself is bounded too
|
|
22
|
+
* (SubscriberNotificationPreviewBuilder.parseRequest caps the text and the
|
|
23
|
+
* custom field values).
|
|
24
|
+
*
|
|
25
|
+
* The limit is loose: someone writing an incident or a note and previewing
|
|
26
|
+
* it after each change is nowhere near it. The counter is per user, across
|
|
27
|
+
* every project and incident, like the test email's
|
|
28
|
+
* (SubscriberNotificationTestSendRateLimit, whose user key this shares).
|
|
29
|
+
*
|
|
30
|
+
* THIS FAILS OPEN
|
|
31
|
+
*
|
|
32
|
+
* If Redis is unreachable the preview is served. That is the opposite of the
|
|
33
|
+
* test email's choice, and the difference is what is at stake: a test email
|
|
34
|
+
* is real mail, and this counter would be the only bound on it; a preview
|
|
35
|
+
* sends nothing, its request is size-capped, and refusing it would take the
|
|
36
|
+
* preview away from everyone for as long as Redis is down.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export enum SubscriberNotificationPreviewRateLimitOutcome {
|
|
40
|
+
Allowed = "allowed",
|
|
41
|
+
RateLimited = "rate-limited",
|
|
42
|
+
|
|
43
|
+
// Redis is unreachable: served, since this fails open.
|
|
44
|
+
CounterUnavailable = "counter-unavailable",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SubscriberNotificationPreviewRateLimitDecision {
|
|
48
|
+
outcome: SubscriberNotificationPreviewRateLimitOutcome;
|
|
49
|
+
retryAfterSeconds?: number | undefined;
|
|
50
|
+
// True only on the request that first crossed the line in this window.
|
|
51
|
+
isFirstRejectionInWindow?: boolean | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SubscriberNotificationPreviewRateLimitConfig {
|
|
55
|
+
windowSeconds: number;
|
|
56
|
+
perUserLimit: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const KEY_PREFIX: string = "subnotifpreview:rl:";
|
|
60
|
+
|
|
61
|
+
// Counter keys outlive their window by one full window.
|
|
62
|
+
const TTL_MULTIPLIER: number = 2;
|
|
63
|
+
|
|
64
|
+
// How often the "counter unavailable" condition may be logged.
|
|
65
|
+
const COUNTER_UNAVAILABLE_LOG_INTERVAL_MS: number = 60 * 1000;
|
|
66
|
+
|
|
67
|
+
export default class SubscriberNotificationPreviewRateLimit {
|
|
68
|
+
// Sixty previews every ten minutes: one every ten seconds, sustained.
|
|
69
|
+
private static readonly config: SubscriberNotificationPreviewRateLimitConfig =
|
|
70
|
+
{
|
|
71
|
+
windowSeconds: 10 * 60,
|
|
72
|
+
perUserLimit: 60,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
private static counterUnavailableLastLoggedAt: number = 0;
|
|
76
|
+
|
|
77
|
+
// The limit in force, for tests and for operators' diagnostics.
|
|
78
|
+
public static getConfig(): SubscriberNotificationPreviewRateLimitConfig {
|
|
79
|
+
return { ...SubscriberNotificationPreviewRateLimit.config };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Count this request against the user and decide. A rejected request is
|
|
84
|
+
* counted too, so a caller who keeps trying keeps their window pinned.
|
|
85
|
+
*/
|
|
86
|
+
public static async consume(data: {
|
|
87
|
+
userKey: string;
|
|
88
|
+
}): Promise<SubscriberNotificationPreviewRateLimitDecision> {
|
|
89
|
+
const client: ClientType | null = Redis.getClient();
|
|
90
|
+
|
|
91
|
+
if (!client || !Redis.isConnected()) {
|
|
92
|
+
return {
|
|
93
|
+
outcome:
|
|
94
|
+
SubscriberNotificationPreviewRateLimitOutcome.CounterUnavailable,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const config: SubscriberNotificationPreviewRateLimitConfig =
|
|
99
|
+
SubscriberNotificationPreviewRateLimit.config;
|
|
100
|
+
const windowMs: number = config.windowSeconds * 1000;
|
|
101
|
+
const windowIndex: number = Math.floor(Date.now() / windowMs);
|
|
102
|
+
const counterKey: string = `${KEY_PREFIX}u:${data.userKey}:${windowIndex}`;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const count: number = await client.incr(counterKey);
|
|
106
|
+
|
|
107
|
+
if (typeof count !== "number") {
|
|
108
|
+
throw new Error("Rate limit counter returned a non-numeric value");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Set only by the request that created the key, so the window is fixed.
|
|
112
|
+
if (count === 1) {
|
|
113
|
+
await client.expire(counterKey, config.windowSeconds * TTL_MULTIPLIER);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (count > config.perUserLimit) {
|
|
117
|
+
return {
|
|
118
|
+
outcome: SubscriberNotificationPreviewRateLimitOutcome.RateLimited,
|
|
119
|
+
retryAfterSeconds:
|
|
120
|
+
SubscriberNotificationTestSendRateLimit.getSecondsUntilWindowEnd(
|
|
121
|
+
config.windowSeconds,
|
|
122
|
+
),
|
|
123
|
+
isFirstRejectionInWindow: count === config.perUserLimit + 1,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
outcome: SubscriberNotificationPreviewRateLimitOutcome.Allowed,
|
|
129
|
+
};
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (
|
|
132
|
+
SubscriberNotificationPreviewRateLimit.shouldLogCounterUnavailable()
|
|
133
|
+
) {
|
|
134
|
+
logger.warn(
|
|
135
|
+
`SubscriberNotificationPreviewRateLimit: counter failed for user ${data.userKey}`,
|
|
136
|
+
);
|
|
137
|
+
logger.warn(err);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
outcome:
|
|
142
|
+
SubscriberNotificationPreviewRateLimitOutcome.CounterUnavailable,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
* Express middleware. Register after UserMiddleware (the counter is per
|
|
149
|
+
* user) and before the handler, so a flood is refused before it renders
|
|
150
|
+
* anything.
|
|
151
|
+
*/
|
|
152
|
+
public static getMiddleware(): (
|
|
153
|
+
req: ExpressRequest,
|
|
154
|
+
res: ExpressResponse,
|
|
155
|
+
next: NextFunction,
|
|
156
|
+
) => Promise<void> {
|
|
157
|
+
return async (
|
|
158
|
+
req: ExpressRequest,
|
|
159
|
+
res: ExpressResponse,
|
|
160
|
+
next: NextFunction,
|
|
161
|
+
): Promise<void> => {
|
|
162
|
+
const userKey: string =
|
|
163
|
+
SubscriberNotificationTestSendRateLimit.resolveUserKey(req);
|
|
164
|
+
|
|
165
|
+
const decision: SubscriberNotificationPreviewRateLimitDecision =
|
|
166
|
+
await SubscriberNotificationPreviewRateLimit.consume({
|
|
167
|
+
userKey: userKey,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
decision.outcome ===
|
|
172
|
+
SubscriberNotificationPreviewRateLimitOutcome.RateLimited
|
|
173
|
+
) {
|
|
174
|
+
if (decision.retryAfterSeconds) {
|
|
175
|
+
const setHeader: unknown = (
|
|
176
|
+
res as unknown as Record<string, unknown>
|
|
177
|
+
)["setHeader"];
|
|
178
|
+
|
|
179
|
+
if (typeof setHeader === "function") {
|
|
180
|
+
(setHeader as (name: string, value: string) => void).call(
|
|
181
|
+
res,
|
|
182
|
+
"Retry-After",
|
|
183
|
+
String(decision.retryAfterSeconds),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (decision.isFirstRejectionInWindow) {
|
|
189
|
+
logger.warn(
|
|
190
|
+
`SubscriberNotificationPreviewRateLimit: rejected previews from user ${userKey} for the rest of this window`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Response.sendErrorResponse(
|
|
195
|
+
req,
|
|
196
|
+
res,
|
|
197
|
+
new TooManyRequestsException(
|
|
198
|
+
"You have previewed notifications too many times in a short while. Please try again in a few minutes.",
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
decision.outcome ===
|
|
205
|
+
SubscriberNotificationPreviewRateLimitOutcome.CounterUnavailable &&
|
|
206
|
+
SubscriberNotificationPreviewRateLimit.shouldLogCounterUnavailable()
|
|
207
|
+
) {
|
|
208
|
+
// Fails open: see the note at the top of this file.
|
|
209
|
+
logger.warn(
|
|
210
|
+
"SubscriberNotificationPreviewRateLimit: rate limit counter unavailable, serving previews without a limit",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return next();
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* A Redis outage means every request takes the unavailable path; one log
|
|
220
|
+
* line per interval makes the condition visible without burying the rest.
|
|
221
|
+
*/
|
|
222
|
+
private static shouldLogCounterUnavailable(): boolean {
|
|
223
|
+
const now: number = Date.now();
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
now -
|
|
227
|
+
SubscriberNotificationPreviewRateLimit.counterUnavailableLastLoggedAt <
|
|
228
|
+
COUNTER_UNAVAILABLE_LOG_INTERVAL_MS
|
|
229
|
+
) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
SubscriberNotificationPreviewRateLimit.counterUnavailableLastLoggedAt = now;
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
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 Response from "../Utils/Response";
|
|
10
|
+
import ObjectID from "../../Types/ObjectID";
|
|
11
|
+
import ServiceUnavailableException from "../../Types/Exception/ServiceUnavailableException";
|
|
12
|
+
import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Request limiting for "Send test to me" (POST /send-test on the subscriber
|
|
16
|
+
* notification preview router), which emails the signed-in user the status
|
|
17
|
+
* page subscriber email they are previewing.
|
|
18
|
+
*
|
|
19
|
+
* The route only ever sends to the caller's own verified account email, so
|
|
20
|
+
* what this bounds is volume: every allowed request is a real email, through
|
|
21
|
+
* the status page's own SMTP server or the instance's mail provider, and
|
|
22
|
+
* nothing else stops a script (or a held-down button) from sending hundreds.
|
|
23
|
+
* The counter is per user, across every project, status page and incident:
|
|
24
|
+
* a user is one inbox.
|
|
25
|
+
*
|
|
26
|
+
* THIS FAILS CLOSED
|
|
27
|
+
*
|
|
28
|
+
* If Redis is unreachable, the test is refused with "try again later". That
|
|
29
|
+
* is the opposite of the choice VerificationCodeRateLimit makes, and the
|
|
30
|
+
* difference is what else bounds the requests: there, the code's expiry, its
|
|
31
|
+
* attempt counter and the resend cooldown all live in Postgres and stay in
|
|
32
|
+
* force; here this counter is the only bound on how much mail is sent. A
|
|
33
|
+
* test email is a convenience, and a Redis blip costs nothing but a retry.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export enum SubscriberNotificationTestSendRateLimitOutcome {
|
|
37
|
+
Allowed = "allowed",
|
|
38
|
+
RateLimited = "rate-limited",
|
|
39
|
+
|
|
40
|
+
// Redis is unreachable, so the limit cannot be honoured.
|
|
41
|
+
CounterUnavailable = "counter-unavailable",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SubscriberNotificationTestSendRateLimitDecision {
|
|
45
|
+
outcome: SubscriberNotificationTestSendRateLimitOutcome;
|
|
46
|
+
retryAfterSeconds?: number | undefined;
|
|
47
|
+
/*
|
|
48
|
+
* True only on the request that first crossed the line in this window, so
|
|
49
|
+
* a flood produces one log line per user per window.
|
|
50
|
+
*/
|
|
51
|
+
isFirstRejectionInWindow?: boolean | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SubscriberNotificationTestSendRateLimitConfig {
|
|
55
|
+
windowSeconds: number;
|
|
56
|
+
perUserLimit: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const KEY_PREFIX: string = "subnotiftest:rl:";
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* Counter keys outlive their window by one full window, so a request landing
|
|
63
|
+
* on a boundary cannot read a key that was reclaimed mid-window.
|
|
64
|
+
*/
|
|
65
|
+
const TTL_MULTIPLIER: number = 2;
|
|
66
|
+
|
|
67
|
+
// How often the "counter unavailable" condition may be logged.
|
|
68
|
+
const COUNTER_UNAVAILABLE_LOG_INTERVAL_MS: number = 60 * 1000;
|
|
69
|
+
|
|
70
|
+
export default class SubscriberNotificationTestSendRateLimit {
|
|
71
|
+
/*
|
|
72
|
+
* Ten test emails a quarter of an hour: someone checking each status
|
|
73
|
+
* page's email in turn, or trying a template change a few times, is
|
|
74
|
+
* nowhere near it.
|
|
75
|
+
*/
|
|
76
|
+
private static readonly config: SubscriberNotificationTestSendRateLimitConfig =
|
|
77
|
+
{
|
|
78
|
+
windowSeconds: 15 * 60,
|
|
79
|
+
perUserLimit: 10,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private static counterUnavailableLastLoggedAt: number = 0;
|
|
83
|
+
|
|
84
|
+
// The limit in force, for tests and for operators' diagnostics.
|
|
85
|
+
public static getConfig(): SubscriberNotificationTestSendRateLimitConfig {
|
|
86
|
+
return { ...SubscriberNotificationTestSendRateLimit.config };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* The signed-in user the request is billed to. The route sits behind
|
|
91
|
+
* UserMiddleware.requireUserAuthentication, so there always is one; if
|
|
92
|
+
* somehow there is not, every such request shares one bucket rather than
|
|
93
|
+
* each getting its own.
|
|
94
|
+
*/
|
|
95
|
+
public static resolveUserKey(req: ExpressRequest): string {
|
|
96
|
+
const userId: ObjectID | undefined = (req as OneUptimeRequest)
|
|
97
|
+
?.userAuthorization?.userId;
|
|
98
|
+
|
|
99
|
+
if (!userId) {
|
|
100
|
+
return "anonymous";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const key: string = userId.toString().toLowerCase();
|
|
104
|
+
|
|
105
|
+
return ObjectID.isValidUUID(key) ? key : "invalid";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Count this request against the user and decide. The counter is
|
|
110
|
+
* incremented on every request, a rejected one included, so a caller who
|
|
111
|
+
* keeps trying keeps their window pinned rather than getting a fresh
|
|
112
|
+
* allowance for free.
|
|
113
|
+
*/
|
|
114
|
+
public static async consume(data: {
|
|
115
|
+
userKey: string;
|
|
116
|
+
}): Promise<SubscriberNotificationTestSendRateLimitDecision> {
|
|
117
|
+
const client: ClientType | null = Redis.getClient();
|
|
118
|
+
|
|
119
|
+
if (!client || !Redis.isConnected()) {
|
|
120
|
+
return {
|
|
121
|
+
outcome:
|
|
122
|
+
SubscriberNotificationTestSendRateLimitOutcome.CounterUnavailable,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const config: SubscriberNotificationTestSendRateLimitConfig =
|
|
127
|
+
SubscriberNotificationTestSendRateLimit.config;
|
|
128
|
+
const windowMs: number = config.windowSeconds * 1000;
|
|
129
|
+
const windowIndex: number = Math.floor(Date.now() / windowMs);
|
|
130
|
+
const counterKey: string = `${KEY_PREFIX}u:${data.userKey}:${windowIndex}`;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const count: number = await client.incr(counterKey);
|
|
134
|
+
|
|
135
|
+
if (typeof count !== "number") {
|
|
136
|
+
throw new Error("Rate limit counter returned a non-numeric value");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* The expiry is set only by the request that created the key: setting
|
|
141
|
+
* it on every increment would slide the window for as long as the
|
|
142
|
+
* requests keep coming.
|
|
143
|
+
*/
|
|
144
|
+
if (count === 1) {
|
|
145
|
+
await client.expire(counterKey, config.windowSeconds * TTL_MULTIPLIER);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (count > config.perUserLimit) {
|
|
149
|
+
return {
|
|
150
|
+
outcome: SubscriberNotificationTestSendRateLimitOutcome.RateLimited,
|
|
151
|
+
retryAfterSeconds:
|
|
152
|
+
SubscriberNotificationTestSendRateLimit.getSecondsUntilWindowEnd(
|
|
153
|
+
config.windowSeconds,
|
|
154
|
+
),
|
|
155
|
+
isFirstRejectionInWindow: count === config.perUserLimit + 1,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
outcome: SubscriberNotificationTestSendRateLimitOutcome.Allowed,
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (
|
|
164
|
+
SubscriberNotificationTestSendRateLimit.shouldLogCounterUnavailable()
|
|
165
|
+
) {
|
|
166
|
+
logger.warn(
|
|
167
|
+
`SubscriberNotificationTestSendRateLimit: counter failed for user ${data.userKey}`,
|
|
168
|
+
);
|
|
169
|
+
logger.warn(err);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
outcome:
|
|
174
|
+
SubscriberNotificationTestSendRateLimitOutcome.CounterUnavailable,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/*
|
|
180
|
+
* Express middleware. Register after UserMiddleware (the counter is per
|
|
181
|
+
* user) and before the handler, so a flood is refused before it reads a
|
|
182
|
+
* single row.
|
|
183
|
+
*/
|
|
184
|
+
public static getMiddleware(): (
|
|
185
|
+
req: ExpressRequest,
|
|
186
|
+
res: ExpressResponse,
|
|
187
|
+
next: NextFunction,
|
|
188
|
+
) => Promise<void> {
|
|
189
|
+
return async (
|
|
190
|
+
req: ExpressRequest,
|
|
191
|
+
res: ExpressResponse,
|
|
192
|
+
next: NextFunction,
|
|
193
|
+
): Promise<void> => {
|
|
194
|
+
const userKey: string =
|
|
195
|
+
SubscriberNotificationTestSendRateLimit.resolveUserKey(req);
|
|
196
|
+
|
|
197
|
+
const decision: SubscriberNotificationTestSendRateLimitDecision =
|
|
198
|
+
await SubscriberNotificationTestSendRateLimit.consume({
|
|
199
|
+
userKey: userKey,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
decision.outcome ===
|
|
204
|
+
SubscriberNotificationTestSendRateLimitOutcome.RateLimited
|
|
205
|
+
) {
|
|
206
|
+
if (decision.retryAfterSeconds) {
|
|
207
|
+
SubscriberNotificationTestSendRateLimit.setRetryAfterHeader(
|
|
208
|
+
res,
|
|
209
|
+
decision.retryAfterSeconds,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (decision.isFirstRejectionInWindow) {
|
|
214
|
+
logger.warn(
|
|
215
|
+
`SubscriberNotificationTestSendRateLimit: rejected test emails from user ${userKey} for the rest of this window`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Response.sendErrorResponse(
|
|
220
|
+
req,
|
|
221
|
+
res,
|
|
222
|
+
new TooManyRequestsException(
|
|
223
|
+
"You have sent yourself too many test emails. Please try again later.",
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
decision.outcome ===
|
|
230
|
+
SubscriberNotificationTestSendRateLimitOutcome.CounterUnavailable
|
|
231
|
+
) {
|
|
232
|
+
// Fails closed: see the note at the top of this file.
|
|
233
|
+
if (
|
|
234
|
+
SubscriberNotificationTestSendRateLimit.shouldLogCounterUnavailable()
|
|
235
|
+
) {
|
|
236
|
+
logger.warn(
|
|
237
|
+
"SubscriberNotificationTestSendRateLimit: rate limit counter unavailable, refusing test emails",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return Response.sendErrorResponse(
|
|
242
|
+
req,
|
|
243
|
+
res,
|
|
244
|
+
new ServiceUnavailableException(
|
|
245
|
+
"Test emails cannot be sent right now. Please try again in a few minutes.",
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return next();
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
* Remaining seconds in the current fixed window, so a refused caller is
|
|
256
|
+
* told to come back when the window actually rolls. The preview limiter
|
|
257
|
+
* (SubscriberNotificationPreviewRateLimit) counts in the same fixed
|
|
258
|
+
* windows, and uses this too.
|
|
259
|
+
*/
|
|
260
|
+
public static getSecondsUntilWindowEnd(windowSeconds: number): number {
|
|
261
|
+
const windowMs: number = windowSeconds * 1000;
|
|
262
|
+
const msIntoWindow: number = Date.now() % windowMs;
|
|
263
|
+
|
|
264
|
+
return Math.max(1, Math.ceil((windowMs - msIntoWindow) / 1000));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
* A Redis outage means every request takes the unavailable path; one log
|
|
269
|
+
* line per interval makes the condition visible without burying the rest.
|
|
270
|
+
*/
|
|
271
|
+
private static shouldLogCounterUnavailable(): boolean {
|
|
272
|
+
const now: number = Date.now();
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
now -
|
|
276
|
+
SubscriberNotificationTestSendRateLimit.counterUnavailableLastLoggedAt <
|
|
277
|
+
COUNTER_UNAVAILABLE_LOG_INTERVAL_MS
|
|
278
|
+
) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
SubscriberNotificationTestSendRateLimit.counterUnavailableLastLoggedAt =
|
|
283
|
+
now;
|
|
284
|
+
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private static setRetryAfterHeader(
|
|
289
|
+
res: ExpressResponse,
|
|
290
|
+
retryAfterSeconds: number,
|
|
291
|
+
): void {
|
|
292
|
+
const setHeader: unknown = (res as unknown as Record<string, unknown>)[
|
|
293
|
+
"setHeader"
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
if (typeof setHeader === "function") {
|
|
297
|
+
(setHeader as (name: string, value: string) => void).call(
|
|
298
|
+
res,
|
|
299
|
+
"Retry-After",
|
|
300
|
+
String(retryAfterSeconds),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|