@oneuptime/common 14.0.6 → 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/InventoryItem.ts +23 -12
- 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/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- 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 +16 -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/AlertService.ts +32 -36
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentAlertService.ts +609 -2
- 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 +1663 -103
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +118 -64
- 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/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/Response.ts +31 -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/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- 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/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -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 +534 -6
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- 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/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- 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/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- 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/RenameInventoryItemMigration.test.ts +90 -21
- 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/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- 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/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -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/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -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/SloAffectedResourceCreatedFeed.test.ts +92 -36
- 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/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -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/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -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/Types/Topology/TopologyTypeRules.test.ts +176 -0
- 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/IncidentFromAlerts.test.ts +302 -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/Incident/IncidentAlertLink.ts +19 -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/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- 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/IncidentFromAlerts.ts +78 -0
- 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/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.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/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- 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 +16 -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/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.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 +1148 -42
- 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/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.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 +84 -57
- 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/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.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/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- 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/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- 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/Response.js +30 -0
- package/build/dist/Server/Utils/Response.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/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- 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/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- 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/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- 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/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.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
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import Model from "../../../Models/DatabaseModels/IoTDeviceCredential";
|
|
2
|
+
import IoTFleet from "../../../Models/DatabaseModels/IoTFleet";
|
|
3
|
+
import CreateBy from "../../../Server/Types/Database/CreateBy";
|
|
4
|
+
import DeleteBy from "../../../Server/Types/Database/DeleteBy";
|
|
5
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
6
|
+
import {
|
|
7
|
+
OnCreate,
|
|
8
|
+
OnDelete,
|
|
9
|
+
OnUpdate,
|
|
10
|
+
} from "../../../Server/Types/Database/Hooks";
|
|
11
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
12
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
13
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
14
|
+
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* WHAT THIS FILE IS DEFENDING
|
|
18
|
+
*
|
|
19
|
+
* IoTDeviceCredential is an MQTT password. The broker authenticates a device
|
|
20
|
+
* CONNECT against it and then trusts the (fleet, device) identity it returns
|
|
21
|
+
* to scope everything that device is allowed to publish. Three of the service's
|
|
22
|
+
* rules are the only thing standing between that and a cross-tenant forgery,
|
|
23
|
+
* and none of them can be expressed in Postgres:
|
|
24
|
+
*
|
|
25
|
+
* 1. THE SECRET IS ALWAYS SERVER-GENERATED. secretKey is a computed column
|
|
26
|
+
* (create ACL []), and the create-permission check SKIPS computed columns
|
|
27
|
+
* rather than stripping them - so a client-supplied secretKey would
|
|
28
|
+
* survive verbatim. A device whose password was chosen by whoever
|
|
29
|
+
* registered it is a device whose password is guessable, which defeats
|
|
30
|
+
* the entire point of the column.
|
|
31
|
+
*
|
|
32
|
+
* 2. THE FLEET MUST BELONG TO THE CALLER'S PROJECT. projectId is forced by
|
|
33
|
+
* the framework; the iotFleetId RELATION is not. Attaching a credential
|
|
34
|
+
* to another tenant's fleet mints an auth context for that tenant and
|
|
35
|
+
* lets the credential steer that fleet's inventory cleanup.
|
|
36
|
+
*
|
|
37
|
+
* 3. THE DUPLICATE GUARD IS BYTE-EXACT. device.id labels are matched
|
|
38
|
+
* byte-exact, and the DB unique index is case-sensitive, so the service
|
|
39
|
+
* check must be too - case-insensitive would reject two legitimately
|
|
40
|
+
* distinct device ids that differ only in case.
|
|
41
|
+
*
|
|
42
|
+
* The rest of the file pins revocation and the two hot paths, which fail
|
|
43
|
+
* quietly rather than loudly:
|
|
44
|
+
*
|
|
45
|
+
* - REVOCATION LAG. Disabling or deleting a credential only reaches the
|
|
46
|
+
* broker when the in-process context cache is dropped. Every write hook
|
|
47
|
+
* clears it, and update/delete clear it AGAIN on success - the
|
|
48
|
+
* before-hook clear races a concurrent read that could re-cache the
|
|
49
|
+
* pre-revocation row before the write lands.
|
|
50
|
+
*
|
|
51
|
+
* - markConnected FAILS OPEN. It throttles a heartbeat write through Redis.
|
|
52
|
+
* If Redis is unreachable the write must still happen, because
|
|
53
|
+
* lastConnectedAt is the registry UI's liveness column - a Redis blip
|
|
54
|
+
* must not make every device look dead.
|
|
55
|
+
*
|
|
56
|
+
* - THE EXPECTED-DEVICE SET IS ENABLED-ONLY AND VERBATIM. It feeds
|
|
57
|
+
* absent-series injection in IoT Device monitor evaluation, so a revoked
|
|
58
|
+
* device must drop out of it (its silent-death alerting pauses) and an id
|
|
59
|
+
* must never be canonicalized on the way through.
|
|
60
|
+
*
|
|
61
|
+
* Nothing here touches Postgres or Redis: the hooks and public methods are
|
|
62
|
+
* driven directly on a fresh service instance whose reads and writes are
|
|
63
|
+
* stubbed.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
const CREDENTIAL_ID: ObjectID = new ObjectID(
|
|
67
|
+
"11111111-1111-4111-8111-111111111111",
|
|
68
|
+
);
|
|
69
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
70
|
+
"22222222-2222-4222-8222-222222222222",
|
|
71
|
+
);
|
|
72
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
73
|
+
"33333333-3333-4333-8333-333333333333",
|
|
74
|
+
);
|
|
75
|
+
const FLEET_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
|
|
76
|
+
const OTHER_FLEET_ID: ObjectID = new ObjectID(
|
|
77
|
+
"55555555-5555-4555-8555-555555555555",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Typed loosely on purpose, and in one place: jest.fn()'s Mock and
|
|
82
|
+
* jest.spyOn()'s SpiedFunction each disagree with this repo's @types/jest
|
|
83
|
+
* about whether mock.lastCall is optional, so the narrow generic forms do not
|
|
84
|
+
* assign. Nothing below needs more than these members, and `unknown`
|
|
85
|
+
* arguments keep every call site free of a cast.
|
|
86
|
+
*/
|
|
87
|
+
type MockLike = {
|
|
88
|
+
mockReset: () => void;
|
|
89
|
+
mockResolvedValue: (value: unknown) => unknown;
|
|
90
|
+
mockRejectedValue: (value: unknown) => unknown;
|
|
91
|
+
mockResolvedValueOnce: (value: unknown) => unknown;
|
|
92
|
+
mock: { calls: Array<Array<unknown>> };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const findOneFleetById: MockLike = jest.fn() as unknown as MockLike;
|
|
96
|
+
|
|
97
|
+
jest.mock("../../../Server/Services/IoTFleetService", () => {
|
|
98
|
+
return {
|
|
99
|
+
__esModule: true,
|
|
100
|
+
default: {
|
|
101
|
+
findOneById: findOneFleetById,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const cacheGetString: MockLike = jest.fn() as unknown as MockLike;
|
|
107
|
+
const cacheSetString: MockLike = jest.fn() as unknown as MockLike;
|
|
108
|
+
|
|
109
|
+
jest.mock("../../../Server/Infrastructure/GlobalCache", () => {
|
|
110
|
+
return {
|
|
111
|
+
__esModule: true,
|
|
112
|
+
default: {
|
|
113
|
+
getString: cacheGetString,
|
|
114
|
+
setString: cacheSetString,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Imported after the mocks above are registered: the service captures
|
|
121
|
+
* IoTFleetService and GlobalCache at module load.
|
|
122
|
+
*/
|
|
123
|
+
import {
|
|
124
|
+
IoTDeviceCredentialContext,
|
|
125
|
+
Service as IoTDeviceCredentialServiceType,
|
|
126
|
+
} from "../../../Server/Services/IoTDeviceCredentialService";
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* The protected hooks and the private cache, reachable for the test. Driving
|
|
130
|
+
* the real hooks is the point - the rules live in them, and an API-layer
|
|
131
|
+
* permission check is not what an internal caller meets.
|
|
132
|
+
*/
|
|
133
|
+
type ServiceInternals = {
|
|
134
|
+
onBeforeCreate: (createBy: CreateBy<Model>) => Promise<OnCreate<Model>>;
|
|
135
|
+
onBeforeUpdate: (updateBy: UpdateBy<Model>) => Promise<OnUpdate<Model>>;
|
|
136
|
+
onBeforeDelete: (deleteBy: DeleteBy<Model>) => Promise<OnDelete<Model>>;
|
|
137
|
+
onUpdateSuccess: (
|
|
138
|
+
onUpdate: OnUpdate<Model>,
|
|
139
|
+
ids: Array<ObjectID>,
|
|
140
|
+
) => Promise<OnUpdate<Model>>;
|
|
141
|
+
onDeleteSuccess: (
|
|
142
|
+
onDelete: OnDelete<Model>,
|
|
143
|
+
ids: Array<ObjectID>,
|
|
144
|
+
) => Promise<OnDelete<Model>>;
|
|
145
|
+
contextCache: {
|
|
146
|
+
size: () => number;
|
|
147
|
+
set: (
|
|
148
|
+
key: string,
|
|
149
|
+
value: IoTDeviceCredentialContext | null,
|
|
150
|
+
ttlMs: number,
|
|
151
|
+
) => void;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/* The spies use the same loose shape; see MockLike above. */
|
|
156
|
+
type Spy = MockLike;
|
|
157
|
+
|
|
158
|
+
interface Harness {
|
|
159
|
+
service: IoTDeviceCredentialServiceType;
|
|
160
|
+
internals: ServiceInternals;
|
|
161
|
+
findOneBy: Spy;
|
|
162
|
+
findBy: Spy;
|
|
163
|
+
countBy: Spy;
|
|
164
|
+
updateWithoutHooks: Spy;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildService(): Harness {
|
|
168
|
+
const service: IoTDeviceCredentialServiceType =
|
|
169
|
+
new IoTDeviceCredentialServiceType();
|
|
170
|
+
|
|
171
|
+
const findOneBy: Spy = jest.spyOn(service, "findOneBy") as unknown as Spy;
|
|
172
|
+
findOneBy.mockResolvedValue(null);
|
|
173
|
+
|
|
174
|
+
const findBy: Spy = jest.spyOn(service, "findBy") as unknown as Spy;
|
|
175
|
+
findBy.mockResolvedValue([]);
|
|
176
|
+
|
|
177
|
+
const countBy: Spy = jest.spyOn(service, "countBy") as unknown as Spy;
|
|
178
|
+
countBy.mockResolvedValue(new PositiveNumber(0));
|
|
179
|
+
|
|
180
|
+
const updateWithoutHooks: Spy = jest.spyOn(
|
|
181
|
+
service,
|
|
182
|
+
"updateColumnsByIdWithoutHooks",
|
|
183
|
+
) as unknown as Spy;
|
|
184
|
+
updateWithoutHooks.mockResolvedValue(undefined);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
service: service,
|
|
188
|
+
internals: service as unknown as ServiceInternals,
|
|
189
|
+
findOneBy: findOneBy,
|
|
190
|
+
findBy: findBy,
|
|
191
|
+
countBy: countBy,
|
|
192
|
+
updateWithoutHooks: updateWithoutHooks,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* A model instance, which is what the API layer hands the service: every
|
|
198
|
+
* declared column is an own property on it, so this is the shape the hook's
|
|
199
|
+
* guards actually meet.
|
|
200
|
+
*/
|
|
201
|
+
function createByModel(payload: Record<string, unknown>): CreateBy<Model> {
|
|
202
|
+
return {
|
|
203
|
+
data: Object.assign(new Model(), payload) as Model,
|
|
204
|
+
props: { isRoot: true, tenantId: PROJECT_ID },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function fleetIn(projectId: ObjectID, id: ObjectID = FLEET_ID): IoTFleet {
|
|
209
|
+
const fleet: IoTFleet = new IoTFleet(id);
|
|
210
|
+
fleet.projectId = projectId;
|
|
211
|
+
return fleet;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function credentialRow(overrides: Record<string, unknown> = {}): Model {
|
|
215
|
+
const row: Model = new Model(CREDENTIAL_ID);
|
|
216
|
+
const fleet: IoTFleet = new IoTFleet(FLEET_ID);
|
|
217
|
+
fleet.name = "roof-sensors";
|
|
218
|
+
|
|
219
|
+
return Object.assign(
|
|
220
|
+
row,
|
|
221
|
+
{
|
|
222
|
+
projectId: PROJECT_ID,
|
|
223
|
+
iotFleetId: FLEET_ID,
|
|
224
|
+
externalId: "Device-01",
|
|
225
|
+
secretKey: new ObjectID("66666666-6666-4666-8666-666666666666"),
|
|
226
|
+
isEnabled: true,
|
|
227
|
+
iotFleet: fleet,
|
|
228
|
+
},
|
|
229
|
+
overrides,
|
|
230
|
+
) as Model;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function writtenValues(createBy: CreateBy<Model>): Record<string, unknown> {
|
|
234
|
+
return createBy.data as unknown as Record<string, unknown>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function lastCall(spy: Spy): Record<string, unknown> {
|
|
238
|
+
const calls: Array<Array<unknown>> = spy.mock.calls;
|
|
239
|
+
|
|
240
|
+
return calls[calls.length - 1]![0] as Record<string, unknown>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
jest.restoreAllMocks();
|
|
245
|
+
findOneFleetById.mockReset();
|
|
246
|
+
findOneFleetById.mockResolvedValue(fleetIn(PROJECT_ID));
|
|
247
|
+
cacheGetString.mockReset();
|
|
248
|
+
cacheGetString.mockResolvedValue(null);
|
|
249
|
+
cacheSetString.mockReset();
|
|
250
|
+
cacheSetString.mockResolvedValue(undefined);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("the device secret is always server-generated", () => {
|
|
254
|
+
test("a create with no secret gets one", async () => {
|
|
255
|
+
const harness: Harness = buildService();
|
|
256
|
+
const createBy: CreateBy<Model> = createByModel({
|
|
257
|
+
projectId: PROJECT_ID,
|
|
258
|
+
iotFleetId: FLEET_ID,
|
|
259
|
+
externalId: "Device-01",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await harness.internals.onBeforeCreate(createBy);
|
|
263
|
+
|
|
264
|
+
expect(writtenValues(createBy)["secretKey"]).toBeInstanceOf(ObjectID);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a client-supplied secret is overwritten, not honoured", async () => {
|
|
268
|
+
/*
|
|
269
|
+
* The rule this whole column depends on. secretKey is computed, and the
|
|
270
|
+
* create-permission check skips computed columns instead of stripping
|
|
271
|
+
* them, so without this assignment the value below would be the device's
|
|
272
|
+
* MQTT password - chosen by the caller, and therefore guessable.
|
|
273
|
+
*/
|
|
274
|
+
const harness: Harness = buildService();
|
|
275
|
+
const planted: string = "00000000-0000-4000-8000-00000000dead";
|
|
276
|
+
const createBy: CreateBy<Model> = createByModel({
|
|
277
|
+
projectId: PROJECT_ID,
|
|
278
|
+
iotFleetId: FLEET_ID,
|
|
279
|
+
externalId: "Device-01",
|
|
280
|
+
secretKey: new ObjectID(planted),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await harness.internals.onBeforeCreate(createBy);
|
|
284
|
+
|
|
285
|
+
expect(String(writtenValues(createBy)["secretKey"])).not.toBe(planted);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("two devices never share a secret", async () => {
|
|
289
|
+
const harness: Harness = buildService();
|
|
290
|
+
const secrets: Array<string> = [];
|
|
291
|
+
|
|
292
|
+
for (const externalId of ["Device-01", "Device-02"]) {
|
|
293
|
+
const createBy: CreateBy<Model> = createByModel({
|
|
294
|
+
projectId: PROJECT_ID,
|
|
295
|
+
iotFleetId: FLEET_ID,
|
|
296
|
+
externalId: externalId,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await harness.internals.onBeforeCreate(createBy);
|
|
300
|
+
secrets.push(String(writtenValues(createBy)["secretKey"]));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
expect(new Set(secrets).size).toBe(2);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("a credential cannot be attached to another project's fleet", () => {
|
|
308
|
+
test("a fleet in another project is refused", async () => {
|
|
309
|
+
const harness: Harness = buildService();
|
|
310
|
+
findOneFleetById.mockResolvedValue(fleetIn(OTHER_PROJECT_ID));
|
|
311
|
+
|
|
312
|
+
await expect(
|
|
313
|
+
harness.internals.onBeforeCreate(
|
|
314
|
+
createByModel({
|
|
315
|
+
projectId: PROJECT_ID,
|
|
316
|
+
iotFleetId: FLEET_ID,
|
|
317
|
+
externalId: "Device-01",
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
320
|
+
).rejects.toThrow(BadDataException);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("a fleet that does not exist is refused", async () => {
|
|
324
|
+
const harness: Harness = buildService();
|
|
325
|
+
findOneFleetById.mockResolvedValue(null);
|
|
326
|
+
|
|
327
|
+
await expect(
|
|
328
|
+
harness.internals.onBeforeCreate(
|
|
329
|
+
createByModel({
|
|
330
|
+
projectId: PROJECT_ID,
|
|
331
|
+
iotFleetId: FLEET_ID,
|
|
332
|
+
externalId: "Device-01",
|
|
333
|
+
}),
|
|
334
|
+
),
|
|
335
|
+
).rejects.toThrow("IoT Fleet not found in this project.");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("a fleet with no project at all is refused", async () => {
|
|
339
|
+
/*
|
|
340
|
+
* A row read back without projectId must not compare equal to anything.
|
|
341
|
+
* `undefined.toString()` would throw rather than refuse, so the guard has
|
|
342
|
+
* to test the field before comparing.
|
|
343
|
+
*/
|
|
344
|
+
const harness: Harness = buildService();
|
|
345
|
+
findOneFleetById.mockResolvedValue(new IoTFleet(FLEET_ID));
|
|
346
|
+
|
|
347
|
+
await expect(
|
|
348
|
+
harness.internals.onBeforeCreate(
|
|
349
|
+
createByModel({
|
|
350
|
+
projectId: PROJECT_ID,
|
|
351
|
+
iotFleetId: FLEET_ID,
|
|
352
|
+
externalId: "Device-01",
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
).rejects.toThrow(BadDataException);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("a fleet in the caller's project is allowed through", async () => {
|
|
359
|
+
const harness: Harness = buildService();
|
|
360
|
+
|
|
361
|
+
const result: OnCreate<Model> = await harness.internals.onBeforeCreate(
|
|
362
|
+
createByModel({
|
|
363
|
+
projectId: PROJECT_ID,
|
|
364
|
+
iotFleetId: FLEET_ID,
|
|
365
|
+
externalId: "Device-01",
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(result.createBy).toBeDefined();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("the fleet is looked up by the id the caller asked for, as root", async () => {
|
|
373
|
+
/*
|
|
374
|
+
* Read as root deliberately: the check must see a fleet that the caller
|
|
375
|
+
* cannot read, or a caller with no read permission on another tenant's
|
|
376
|
+
* fleet would get "not found" turned into a successful attach.
|
|
377
|
+
*/
|
|
378
|
+
const harness: Harness = buildService();
|
|
379
|
+
|
|
380
|
+
await harness.internals.onBeforeCreate(
|
|
381
|
+
createByModel({
|
|
382
|
+
projectId: PROJECT_ID,
|
|
383
|
+
iotFleetId: OTHER_FLEET_ID,
|
|
384
|
+
externalId: "Device-01",
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const call: Record<string, unknown> = findOneFleetById.mock
|
|
389
|
+
.calls[0]![0] as Record<string, unknown>;
|
|
390
|
+
|
|
391
|
+
expect(String(call["id"])).toBe(OTHER_FLEET_ID.toString());
|
|
392
|
+
expect(call["props"]).toEqual({ isRoot: true });
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("a device id is unique within its fleet, byte-exact", () => {
|
|
397
|
+
test("a second registration of the same id is refused", async () => {
|
|
398
|
+
const harness: Harness = buildService();
|
|
399
|
+
harness.countBy.mockResolvedValue(new PositiveNumber(1));
|
|
400
|
+
|
|
401
|
+
await expect(
|
|
402
|
+
harness.internals.onBeforeCreate(
|
|
403
|
+
createByModel({
|
|
404
|
+
projectId: PROJECT_ID,
|
|
405
|
+
iotFleetId: FLEET_ID,
|
|
406
|
+
externalId: "Device-01",
|
|
407
|
+
}),
|
|
408
|
+
),
|
|
409
|
+
).rejects.toThrow(
|
|
410
|
+
"A device with this Device ID is already registered in this fleet.",
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("the duplicate query is scoped to the project AND the fleet", async () => {
|
|
415
|
+
/*
|
|
416
|
+
* The same device id in a different fleet, or a different project, is a
|
|
417
|
+
* different device. Dropping either column from the query would refuse a
|
|
418
|
+
* legitimate registration.
|
|
419
|
+
*/
|
|
420
|
+
const harness: Harness = buildService();
|
|
421
|
+
|
|
422
|
+
await harness.internals.onBeforeCreate(
|
|
423
|
+
createByModel({
|
|
424
|
+
projectId: PROJECT_ID,
|
|
425
|
+
iotFleetId: FLEET_ID,
|
|
426
|
+
externalId: "Device-01",
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const query: Record<string, unknown> = lastCall(harness.countBy)[
|
|
431
|
+
"query"
|
|
432
|
+
] as Record<string, unknown>;
|
|
433
|
+
|
|
434
|
+
expect(String(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
435
|
+
expect(String(query["iotFleetId"])).toBe(FLEET_ID.toString());
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("the id is matched verbatim, so case is significant", async () => {
|
|
439
|
+
/*
|
|
440
|
+
* device.id labels are matched byte-exact and the DB unique index is
|
|
441
|
+
* case-sensitive. A lowercased or trimmed query here would reject
|
|
442
|
+
* "Device-01" as a duplicate of "device-01", which are two real devices.
|
|
443
|
+
*/
|
|
444
|
+
const harness: Harness = buildService();
|
|
445
|
+
|
|
446
|
+
await harness.internals.onBeforeCreate(
|
|
447
|
+
createByModel({
|
|
448
|
+
projectId: PROJECT_ID,
|
|
449
|
+
iotFleetId: FLEET_ID,
|
|
450
|
+
externalId: " Device-01 ",
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const query: Record<string, unknown> = lastCall(harness.countBy)[
|
|
455
|
+
"query"
|
|
456
|
+
] as Record<string, unknown>;
|
|
457
|
+
|
|
458
|
+
expect(query["externalId"]).toBe(" Device-01 ");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("no duplicate check is attempted without a device id", async () => {
|
|
462
|
+
// Nothing to compare: the column's own validation refuses it later.
|
|
463
|
+
const harness: Harness = buildService();
|
|
464
|
+
|
|
465
|
+
await harness.internals.onBeforeCreate(
|
|
466
|
+
createByModel({ projectId: PROJECT_ID, iotFleetId: FLEET_ID }),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
expect(harness.countBy.mock.calls).toHaveLength(0);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("resolving a credential to an auth context", () => {
|
|
474
|
+
test("a complete, enabled credential resolves", async () => {
|
|
475
|
+
const harness: Harness = buildService();
|
|
476
|
+
harness.findOneBy.mockResolvedValue(credentialRow());
|
|
477
|
+
|
|
478
|
+
const context: IoTDeviceCredentialContext | null =
|
|
479
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
480
|
+
|
|
481
|
+
expect(context).toEqual({
|
|
482
|
+
credentialId: CREDENTIAL_ID.toString(),
|
|
483
|
+
projectId: PROJECT_ID.toString(),
|
|
484
|
+
iotFleetId: FLEET_ID.toString(),
|
|
485
|
+
fleetName: "roof-sensors",
|
|
486
|
+
externalId: "Device-01",
|
|
487
|
+
secretKey: "66666666-6666-4666-8666-666666666666",
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("the secret is returned for the CALLER to compare, not compared here", async () => {
|
|
492
|
+
/*
|
|
493
|
+
* This method authenticates nothing. The broker compares the presented
|
|
494
|
+
* MQTT password against context.secretKey - so a context that omitted the
|
|
495
|
+
* secret would make every device unauthenticatable, and one that resolved
|
|
496
|
+
* without it would be worse.
|
|
497
|
+
*/
|
|
498
|
+
const harness: Harness = buildService();
|
|
499
|
+
harness.findOneBy.mockResolvedValue(credentialRow());
|
|
500
|
+
|
|
501
|
+
const context: IoTDeviceCredentialContext | null =
|
|
502
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
503
|
+
|
|
504
|
+
expect(context?.secretKey).toBe("66666666-6666-4666-8666-666666666666");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("an unknown credential resolves to null", async () => {
|
|
508
|
+
const harness: Harness = buildService();
|
|
509
|
+
harness.findOneBy.mockResolvedValue(null);
|
|
510
|
+
|
|
511
|
+
await expect(
|
|
512
|
+
harness.service.getCredentialContext(CREDENTIAL_ID.toString()),
|
|
513
|
+
).resolves.toBeNull();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("a DISABLED credential resolves to null", async () => {
|
|
517
|
+
// This is revocation-by-toggle: the broker must stop accepting it.
|
|
518
|
+
const harness: Harness = buildService();
|
|
519
|
+
harness.findOneBy.mockResolvedValue(credentialRow({ isEnabled: false }));
|
|
520
|
+
|
|
521
|
+
await expect(
|
|
522
|
+
harness.service.getCredentialContext(CREDENTIAL_ID.toString()),
|
|
523
|
+
).resolves.toBeNull();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test.each([
|
|
527
|
+
["no project", { projectId: undefined }],
|
|
528
|
+
["no fleet", { iotFleetId: undefined }],
|
|
529
|
+
["no device id", { externalId: undefined }],
|
|
530
|
+
["no secret", { secretKey: undefined }],
|
|
531
|
+
["no fleet name", { iotFleet: undefined }],
|
|
532
|
+
])(
|
|
533
|
+
"a credential with %s resolves to null",
|
|
534
|
+
async (
|
|
535
|
+
_label: string,
|
|
536
|
+
overrides: Record<string, unknown>,
|
|
537
|
+
): Promise<void> => {
|
|
538
|
+
/*
|
|
539
|
+
* A half-populated row must not produce a context: every field feeds the
|
|
540
|
+
* topic scope the broker then trusts, and an undefined one would widen it.
|
|
541
|
+
*/
|
|
542
|
+
const harness: Harness = buildService();
|
|
543
|
+
harness.findOneBy.mockResolvedValue(credentialRow(overrides));
|
|
544
|
+
|
|
545
|
+
await expect(
|
|
546
|
+
harness.service.getCredentialContext(CREDENTIAL_ID.toString()),
|
|
547
|
+
).resolves.toBeNull();
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
test("a malformed credential id never reaches the database", async () => {
|
|
552
|
+
/*
|
|
553
|
+
* The id arrives from an MQTT username, so it is attacker-controlled. A
|
|
554
|
+
* flood of garbage usernames must not become a flood of queries.
|
|
555
|
+
*/
|
|
556
|
+
const harness: Harness = buildService();
|
|
557
|
+
|
|
558
|
+
await expect(
|
|
559
|
+
harness.service.getCredentialContext("not-a-uuid"),
|
|
560
|
+
).resolves.toBeNull();
|
|
561
|
+
expect(harness.findOneBy.mock.calls).toHaveLength(0);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("a resolved credential is cached, so the hot path reads once", async () => {
|
|
565
|
+
const harness: Harness = buildService();
|
|
566
|
+
harness.findOneBy.mockResolvedValue(credentialRow());
|
|
567
|
+
|
|
568
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
569
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
570
|
+
|
|
571
|
+
expect(harness.findOneBy.mock.calls).toHaveLength(1);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("a MISS is cached too, so invalid credentials cannot flood Postgres", async () => {
|
|
575
|
+
const harness: Harness = buildService();
|
|
576
|
+
harness.findOneBy.mockResolvedValue(null);
|
|
577
|
+
|
|
578
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
579
|
+
await harness.service.getCredentialContext(CREDENTIAL_ID.toString());
|
|
580
|
+
|
|
581
|
+
expect(harness.findOneBy.mock.calls).toHaveLength(1);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe("revocation reaches the broker", () => {
|
|
586
|
+
/*
|
|
587
|
+
* The cache is what makes revocation lag, so every write path drops it.
|
|
588
|
+
* Update and delete drop it TWICE - the before-hook clear races a concurrent
|
|
589
|
+
* getCredentialContext that could re-cache the pre-revocation row before the
|
|
590
|
+
* write commits, and the on-success clear is what closes that window.
|
|
591
|
+
*/
|
|
592
|
+
function seededService(): Harness {
|
|
593
|
+
const harness: Harness = buildService();
|
|
594
|
+
harness.internals.contextCache.set(CREDENTIAL_ID.toString(), null, 60_000);
|
|
595
|
+
expect(harness.internals.contextCache.size()).toBe(1);
|
|
596
|
+
return harness;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
test("onBeforeUpdate clears the cache", async () => {
|
|
600
|
+
const harness: Harness = seededService();
|
|
601
|
+
|
|
602
|
+
await harness.internals.onBeforeUpdate({
|
|
603
|
+
query: {},
|
|
604
|
+
data: {},
|
|
605
|
+
} as unknown as UpdateBy<Model>);
|
|
606
|
+
|
|
607
|
+
expect(harness.internals.contextCache.size()).toBe(0);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("onUpdateSuccess clears the cache again, after the write commits", async () => {
|
|
611
|
+
const harness: Harness = seededService();
|
|
612
|
+
|
|
613
|
+
await harness.internals.onUpdateSuccess(
|
|
614
|
+
{
|
|
615
|
+
updateBy: { query: {}, data: {} },
|
|
616
|
+
carryForward: null,
|
|
617
|
+
} as unknown as OnUpdate<Model>,
|
|
618
|
+
[CREDENTIAL_ID],
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
expect(harness.internals.contextCache.size()).toBe(0);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("onBeforeDelete clears the cache", async () => {
|
|
625
|
+
const harness: Harness = seededService();
|
|
626
|
+
|
|
627
|
+
await harness.internals.onBeforeDelete({
|
|
628
|
+
query: {},
|
|
629
|
+
} as unknown as DeleteBy<Model>);
|
|
630
|
+
|
|
631
|
+
expect(harness.internals.contextCache.size()).toBe(0);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("onDeleteSuccess clears the cache again", async () => {
|
|
635
|
+
const harness: Harness = seededService();
|
|
636
|
+
|
|
637
|
+
await harness.internals.onDeleteSuccess(
|
|
638
|
+
{
|
|
639
|
+
deleteBy: { query: {} },
|
|
640
|
+
carryForward: null,
|
|
641
|
+
} as unknown as OnDelete<Model>,
|
|
642
|
+
[CREDENTIAL_ID],
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
expect(harness.internals.contextCache.size()).toBe(0);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe("the lastConnectedAt heartbeat is throttled but never skipped", () => {
|
|
650
|
+
test("a first connect stamps lastConnectedAt", async () => {
|
|
651
|
+
const harness: Harness = buildService();
|
|
652
|
+
|
|
653
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
654
|
+
|
|
655
|
+
const call: Record<string, unknown> = lastCall(harness.updateWithoutHooks);
|
|
656
|
+
const data: Record<string, unknown> = call["data"] as Record<
|
|
657
|
+
string,
|
|
658
|
+
unknown
|
|
659
|
+
>;
|
|
660
|
+
|
|
661
|
+
expect(String(call["id"])).toBe(CREDENTIAL_ID.toString());
|
|
662
|
+
expect(data["lastConnectedAt"]).toBeInstanceOf(Date);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("a reconnect inside the throttle window writes nothing", async () => {
|
|
666
|
+
// Connect storms must not turn into a write per CONNECT on a hot row.
|
|
667
|
+
const harness: Harness = buildService();
|
|
668
|
+
cacheGetString.mockResolvedValue("1");
|
|
669
|
+
|
|
670
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
671
|
+
|
|
672
|
+
expect(harness.updateWithoutHooks.mock.calls).toHaveLength(0);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("the throttle key is set to expire, so the stamp resumes", async () => {
|
|
676
|
+
/*
|
|
677
|
+
* A throttle key with no expiry would stop lastConnectedAt updating for
|
|
678
|
+
* the lifetime of the Redis key - the liveness column would freeze at the
|
|
679
|
+
* device's first connect.
|
|
680
|
+
*/
|
|
681
|
+
const harness: Harness = buildService();
|
|
682
|
+
|
|
683
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
684
|
+
|
|
685
|
+
const options: Record<string, unknown> = cacheSetString.mock
|
|
686
|
+
.calls[0]![3] as Record<string, unknown>;
|
|
687
|
+
|
|
688
|
+
expect(options["expiresInSeconds"]).toBeGreaterThan(0);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("an unreachable Redis FAILS OPEN and the stamp still happens", async () => {
|
|
692
|
+
/*
|
|
693
|
+
* The throttle is an optimisation; lastConnectedAt is the UI's liveness
|
|
694
|
+
* column. Treating a Redis error as "already stamped" would make every
|
|
695
|
+
* device look dead for as long as Redis was down.
|
|
696
|
+
*/
|
|
697
|
+
const harness: Harness = buildService();
|
|
698
|
+
cacheGetString.mockRejectedValue(new Error("redis down"));
|
|
699
|
+
|
|
700
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
701
|
+
|
|
702
|
+
expect(harness.updateWithoutHooks.mock.calls).toHaveLength(1);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("a failed throttle WRITE does not lose the stamp either", async () => {
|
|
706
|
+
const harness: Harness = buildService();
|
|
707
|
+
cacheSetString.mockRejectedValue(new Error("redis down"));
|
|
708
|
+
|
|
709
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
710
|
+
|
|
711
|
+
expect(harness.updateWithoutHooks.mock.calls).toHaveLength(1);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("the stamp is a no-hooks update, so it cannot convoy on hooks", async () => {
|
|
715
|
+
/*
|
|
716
|
+
* Pinned by which method is called: a heartbeat going through the normal
|
|
717
|
+
* update path would run every hook and bump the row version on each
|
|
718
|
+
* CONNECT, which is the lock convoy this avoids.
|
|
719
|
+
*/
|
|
720
|
+
const harness: Harness = buildService();
|
|
721
|
+
const withHooks: Spy = jest.spyOn(
|
|
722
|
+
harness.service,
|
|
723
|
+
"updateOneById",
|
|
724
|
+
) as unknown as Spy;
|
|
725
|
+
withHooks.mockResolvedValue(undefined);
|
|
726
|
+
|
|
727
|
+
await harness.service.markConnected(CREDENTIAL_ID);
|
|
728
|
+
|
|
729
|
+
expect(harness.updateWithoutHooks.mock.calls).toHaveLength(1);
|
|
730
|
+
expect(withHooks.mock.calls).toHaveLength(0);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
describe("the expected-device set for a fleet", () => {
|
|
735
|
+
function enabledRows(ids: Array<string | undefined>): Array<Model> {
|
|
736
|
+
return ids.map((externalId: string | undefined): Model => {
|
|
737
|
+
const row: Model = new Model();
|
|
738
|
+
/*
|
|
739
|
+
* Assigned through the record rather than the property: externalId is
|
|
740
|
+
* deliberately `string | undefined` here - a row that came back without
|
|
741
|
+
* one is the case under test - and exactOptionalPropertyTypes refuses
|
|
742
|
+
* `undefined` on an optional property.
|
|
743
|
+
*/
|
|
744
|
+
(row as unknown as Record<string, unknown>)["externalId"] = externalId;
|
|
745
|
+
return row;
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
test("only ENABLED devices are expected", async () => {
|
|
750
|
+
/*
|
|
751
|
+
* A revoked device stays in the inventory as Offline (stale cleanup keeps
|
|
752
|
+
* its row) but must leave this set, or its silent-death alerting would
|
|
753
|
+
* keep firing against a device that was deliberately turned off.
|
|
754
|
+
*/
|
|
755
|
+
const harness: Harness = buildService();
|
|
756
|
+
|
|
757
|
+
await harness.service.getExpectedDeviceExternalIds({
|
|
758
|
+
projectId: PROJECT_ID,
|
|
759
|
+
iotFleetId: FLEET_ID,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const query: Record<string, unknown> = lastCall(harness.findBy)[
|
|
763
|
+
"query"
|
|
764
|
+
] as Record<string, unknown>;
|
|
765
|
+
|
|
766
|
+
expect(query["isEnabled"]).toBe(true);
|
|
767
|
+
expect(String(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
768
|
+
expect(String(query["iotFleetId"])).toBe(FLEET_ID.toString());
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test("ids come back VERBATIM, never canonicalized", async () => {
|
|
772
|
+
/*
|
|
773
|
+
* These are matched byte-exact against the device.id resource attribute.
|
|
774
|
+
* Trimming or lowercasing here would silently stop matching the device
|
|
775
|
+
* that is actually reporting.
|
|
776
|
+
*/
|
|
777
|
+
const harness: Harness = buildService();
|
|
778
|
+
harness.findBy.mockResolvedValue(
|
|
779
|
+
enabledRows(["Device-01", "device-01", " padded "]),
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
await expect(
|
|
783
|
+
harness.service.getExpectedDeviceExternalIds({
|
|
784
|
+
projectId: PROJECT_ID,
|
|
785
|
+
iotFleetId: FLEET_ID,
|
|
786
|
+
}),
|
|
787
|
+
).resolves.toEqual(["Device-01", "device-01", " padded "]);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test("a row with no device id is skipped rather than producing a blank", async () => {
|
|
791
|
+
/*
|
|
792
|
+
* A blank id in this set would inject an absent-series for a device that
|
|
793
|
+
* can never report, which alerts forever.
|
|
794
|
+
*/
|
|
795
|
+
const harness: Harness = buildService();
|
|
796
|
+
harness.findBy.mockResolvedValue(
|
|
797
|
+
enabledRows(["Device-01", undefined, "Device-02"]),
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
await expect(
|
|
801
|
+
harness.service.getExpectedDeviceExternalIds({
|
|
802
|
+
projectId: PROJECT_ID,
|
|
803
|
+
iotFleetId: FLEET_ID,
|
|
804
|
+
}),
|
|
805
|
+
).resolves.toEqual(["Device-01", "Device-02"]);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("a fleet with no registered devices is an empty set, not an error", async () => {
|
|
809
|
+
const harness: Harness = buildService();
|
|
810
|
+
harness.findBy.mockResolvedValue([]);
|
|
811
|
+
|
|
812
|
+
await expect(
|
|
813
|
+
harness.service.getExpectedDeviceExternalIds({
|
|
814
|
+
projectId: PROJECT_ID,
|
|
815
|
+
iotFleetId: FLEET_ID,
|
|
816
|
+
}),
|
|
817
|
+
).resolves.toEqual([]);
|
|
818
|
+
});
|
|
819
|
+
});
|