@oneuptime/common 14.0.3 → 14.0.5

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.
Files changed (617) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  3. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  4. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  5. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  6. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  9. package/Models/DatabaseModels/Incident.ts +55 -0
  10. package/Models/DatabaseModels/Index.ts +16 -0
  11. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  13. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  14. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  18. package/Server/Middleware/MultipartFormData.ts +1 -1
  19. package/Server/Middleware/TelemetryIngest.ts +15 -8
  20. package/Server/Services/AlertReminderRuleService.ts +100 -24
  21. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  22. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  23. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  24. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  25. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  26. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  27. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  28. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  29. package/Server/Services/DatabaseServerService.ts +4546 -0
  30. package/Server/Services/DatabaseService.ts +14 -8
  31. package/Server/Services/DockerResourceService.ts +43 -2
  32. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  33. package/Server/Services/Index.ts +16 -0
  34. package/Server/Services/InventoryItemService.ts +113 -5
  35. package/Server/Services/IoTDeviceService.ts +145 -37
  36. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  37. package/Server/Services/PodmanResourceService.ts +43 -2
  38. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  39. package/Server/Services/ServiceService.ts +1 -1
  40. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  41. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  42. package/Server/Services/UserService.ts +14 -1
  43. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  44. package/Server/Types/Database/QueryHelper.ts +30 -1
  45. package/Server/Utils/AI/Toolbox/ResourceTools.ts +69 -1
  46. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  47. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +188 -6
  48. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  49. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2262 -755
  50. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  51. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  52. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  53. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  54. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  55. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  56. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  57. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  58. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  59. package/Server/Utils/StartServer.ts +26 -6
  60. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  61. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  62. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  63. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  64. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  65. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  66. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  67. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  68. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  69. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  70. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  71. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  72. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  73. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  74. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  75. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  76. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  77. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  78. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  79. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  80. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  81. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  82. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  83. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  84. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  85. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  86. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  87. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  88. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  89. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  90. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  91. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  92. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  93. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  94. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  95. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  96. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  97. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  98. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  99. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  100. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  101. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  102. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  103. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  104. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  105. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  106. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  107. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  108. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  109. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  110. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  111. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  112. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  113. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  114. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  115. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  116. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  117. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  118. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  119. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  120. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  121. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  122. package/Tests/ClassStrings.ts +178 -0
  123. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  124. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  125. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  126. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  127. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  128. package/Tests/RenderedMarkup.ts +518 -0
  129. package/Tests/ResponsiveVisibility.test.ts +490 -4
  130. package/Tests/ResponsiveVisibility.ts +505 -52
  131. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  133. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  134. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  135. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  136. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  137. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  138. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  139. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  140. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  141. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  142. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  143. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  144. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  145. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  146. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  147. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  148. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  149. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  150. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  151. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  152. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  153. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  154. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  155. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  156. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  157. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  158. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  159. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  160. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  161. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  162. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  163. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  164. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  165. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  166. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  167. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  168. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  169. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  170. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  171. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  172. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  175. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  176. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  177. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  178. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  179. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  180. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  181. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1719 -0
  182. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  183. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  184. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  185. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  186. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  187. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  188. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  189. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  190. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  191. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  192. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  193. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  194. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  195. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  196. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  197. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  198. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  199. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  200. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  201. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  202. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  203. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  204. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  205. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  206. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  207. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  208. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  209. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  210. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  211. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  212. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  213. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  214. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  215. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  216. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  217. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  218. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  219. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  220. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  221. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +298 -0
  222. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +271 -0
  223. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  224. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  225. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  226. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  227. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  228. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  229. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +181 -0
  230. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  231. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  232. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  233. package/Tests/UI/Components/Card.test.tsx +93 -2
  234. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  235. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  236. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  237. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  238. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  239. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  240. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  241. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  242. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  243. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  244. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  245. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  246. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  247. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  248. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  249. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  250. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
  251. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
  252. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  253. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  254. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  255. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  256. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  257. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  258. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  259. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  260. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  261. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  262. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  263. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  264. package/Tests/tsconfig.json +16 -0
  265. package/Types/AI/AIResourceContext.ts +7 -0
  266. package/Types/Dashboard/DashboardTemplates.ts +2 -1
  267. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  268. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  269. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  270. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  271. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  272. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  273. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  274. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  275. package/Types/Email/EmailTemplateType.ts +1 -0
  276. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  277. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  278. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  279. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  280. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  281. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  282. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  283. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  284. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  285. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  286. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  287. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  288. package/Types/Permission.ts +267 -0
  289. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  290. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  291. package/Types/Rules/RuleRun.ts +12 -0
  292. package/Types/Telemetry/EntityType.ts +23 -0
  293. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  294. package/Types/Telemetry/ServiceType.ts +8 -0
  295. package/UI/Components/Banner/Banner.tsx +1 -1
  296. package/UI/Components/Card/Card.tsx +1 -1
  297. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  298. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  299. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  300. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  301. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  302. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  303. package/UI/Components/Forms/BasicForm.tsx +1 -1
  304. package/UI/Components/Header/Header.tsx +1 -1
  305. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  306. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  307. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  308. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  309. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  310. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  311. package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
  312. package/UI/Components/Navbar/NavBar.tsx +9 -2
  313. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  314. package/UI/Components/Page/Page.tsx +1 -1
  315. package/UI/Components/Pagination/Pagination.tsx +3 -3
  316. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  317. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  318. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  319. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  320. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  321. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  322. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  323. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  324. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  325. package/UI/Styles/Theme.css +8 -1
  326. package/UI/Utils/Navigation.ts +89 -42
  327. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  328. package/UI/Utils/TelemetryService.ts +4 -3
  329. package/Utils/LabelRuleImportExport.ts +2 -0
  330. package/Utils/MetricUnitUtil.ts +103 -11
  331. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  332. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  333. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  334. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  335. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  336. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  337. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  338. package/Utils/Telemetry/EntityKey.ts +176 -0
  339. package/Utils/ValueFormatter.ts +16 -0
  340. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  341. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  342. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  343. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  344. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  345. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  346. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  347. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  348. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  349. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  350. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  351. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  352. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  353. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  354. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  355. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  356. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  357. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  358. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  359. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  360. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  361. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  362. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  363. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  364. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  365. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  366. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  367. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  368. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  369. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  370. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  371. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  372. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  373. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  374. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  375. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  376. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  377. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  378. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  379. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  380. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  381. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  382. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  383. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  384. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  385. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  386. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  387. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  388. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  389. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  390. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  391. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  392. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  393. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  394. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  395. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  396. package/build/dist/Server/Services/DatabaseService.js +8 -4
  397. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  398. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  399. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  400. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  401. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  402. package/build/dist/Server/Services/Index.js +16 -0
  403. package/build/dist/Server/Services/Index.js.map +1 -1
  404. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  405. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  406. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  407. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  408. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  409. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  410. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  411. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  412. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  413. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  414. package/build/dist/Server/Services/ServiceService.js +1 -1
  415. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  416. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  417. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  418. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  419. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  420. package/build/dist/Server/Services/UserService.js +13 -1
  421. package/build/dist/Server/Services/UserService.js.map +1 -1
  422. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  423. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  424. package/build/dist/Server/Types/Database/QueryHelper.js +26 -1
  425. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  426. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +46 -0
  427. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  428. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  429. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  430. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +134 -13
  431. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  432. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  433. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  434. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  435. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  436. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  437. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  438. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  439. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  440. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  441. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  442. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  443. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  444. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  445. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  446. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  447. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  448. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  449. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  450. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  451. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  452. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  453. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  454. package/build/dist/Server/Utils/StartServer.js +20 -4
  455. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  456. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  457. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  458. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  459. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  460. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  461. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  462. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  463. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  464. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  465. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  466. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  467. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  468. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  469. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  470. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  471. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  472. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  473. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  474. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  475. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  476. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  477. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  478. package/build/dist/Types/Dashboard/DashboardTemplates.js +2 -1
  479. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  480. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  481. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  482. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  483. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  484. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  485. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  486. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  487. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  488. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  489. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  490. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  491. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  492. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  493. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  494. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  495. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  496. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  497. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  498. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  499. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  500. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  501. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  502. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  503. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  504. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  505. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  506. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  507. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  508. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  509. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  510. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  511. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  512. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  513. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  514. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  515. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  516. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  517. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  518. package/build/dist/Types/Permission.js +234 -0
  519. package/build/dist/Types/Permission.js.map +1 -1
  520. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  521. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  522. package/build/dist/Types/Rules/RuleRun.js +4 -0
  523. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  524. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  525. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  526. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  527. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  528. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  529. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  530. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  531. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  532. package/build/dist/UI/Components/Card/Card.js +1 -1
  533. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  534. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  535. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  536. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  537. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  538. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  539. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  540. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  541. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  542. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  543. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  544. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  545. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  546. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  547. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  548. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  549. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  550. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  551. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  552. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  553. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  554. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  555. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  556. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  557. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  558. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  559. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  560. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
  561. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  562. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  563. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  564. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  565. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  566. package/build/dist/UI/Components/Page/Page.js +1 -1
  567. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  568. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  569. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  570. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  571. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  572. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  573. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  574. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  575. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  576. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  577. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  578. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  579. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  580. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  581. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  582. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  583. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  584. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  585. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  586. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  587. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  588. package/build/dist/UI/Utils/Navigation.js +66 -33
  589. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  590. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  591. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  592. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  593. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  594. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  595. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  596. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  597. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  598. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  599. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  600. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  601. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  602. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  603. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  604. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  605. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  606. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  607. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  608. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  609. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  610. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  611. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  612. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  613. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  614. package/build/dist/Utils/ValueFormatter.js +15 -0
  615. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  616. package/package.json +1 -1
  617. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,1943 @@
1
+ import {
2
+ getDatabaseSystemDisplayName,
3
+ getDatabaseSystemFamily,
4
+ getDefaultDatabasePort,
5
+ normalizeDatabaseSystem,
6
+ trimTrailingCharacter,
7
+ } from "./DatabaseSystem";
8
+
9
+ /*
10
+ * Database SERVER endpoints — the identity heart of the Databases product.
11
+ *
12
+ * Every path that sees a database (a DB client span, a `db.client.*`
13
+ * datapoint, a collector DB receiver batch, a Kubernetes workload, a
14
+ * user-typed alias) turns what it saw into one canonical `DatabaseEndpoint`
15
+ * here, and the endpoint (never the engine) is what gets keyed
16
+ * (`EntityKey.keyForDatabaseEndpoint`) and stored (`DatabaseServerEndpoint`).
17
+ * The ingest side and the read side MUST agree byte for byte, so all of the
18
+ * rules live in this one pure module:
19
+ *
20
+ * - one host spelling: lowercase, no trailing dot, IPv6 without brackets
21
+ * and compressed, IPv4-mapped IPv6 unwrapped;
22
+ * - one port: the explicit port, else the engine default (semconv omits
23
+ * `server.port` when it is the default) — except for a SQL Server named
24
+ * instance, which listens on a port of its own and is identified by its
25
+ * name instead (`sql1.corp\inst01`);
26
+ * - loopback and host-relative names are never an identity for application
27
+ * spans (a sidecar proxy or a laptop), and are the collector's own host
28
+ * only for a non-ephemeral collector;
29
+ * - Kubernetes service DNS is expanded to its FQDN, and names that only
30
+ * resolve inside one cluster or network (cluster-local DNS, the private
31
+ * DNS zones, private and link-local IPs) are qualified with the caller's
32
+ * cluster, because every cluster has its own `db.prod.svc.cluster.local`
33
+ * and its own 10.0.0.5.
34
+ *
35
+ * Isomorphic, dependency-light and total: nothing here throws, every
36
+ * function returns null (or "local") for input it cannot make sense of —
37
+ * scrubbed values such as "[REDACTED]" or "***.***.***.***" included.
38
+ */
39
+
40
+ export interface DatabaseEndpoint {
41
+ /*
42
+ * Canonical lowercase host; IPv6 without brackets; IPv4-mapped v6
43
+ * unwrapped. A SQL Server named instance reached without a port is part
44
+ * of it — `sql1.corp\inst01` — so it is part of every key and identifier
45
+ * built from the endpoint (see splitDatabaseHostInstance).
46
+ */
47
+ host: string;
48
+ // Null only when unknown AND the engine has no default port (or a named instance).
49
+ port: number | null;
50
+ // Qualifier: only on cluster-local / private-network hosts.
51
+ kubernetesClusterName?: string | undefined;
52
+ }
53
+
54
+ export interface ParsedHostAndPort {
55
+ host: string;
56
+ port: number | null;
57
+ // A SQL Server named instance (`host\instance`), lowercased; absent when none.
58
+ instance?: string | undefined;
59
+ }
60
+
61
+ export interface DatabaseCallerContext {
62
+ kubernetesNamespace?: string | null | undefined;
63
+ kubernetesClusterName?: string | null | undefined;
64
+ hostName?: string | null | undefined;
65
+ // See isEphemeralCaller.
66
+ isEphemeral: boolean;
67
+ /*
68
+ * True when the caller is known to run in Kubernetes although neither
69
+ * its namespace nor its cluster was kept: the client-span discovery query
70
+ * keeps only this flag for the addresses whose endpoint depends on WHETHER
71
+ * the caller runs in Kubernetes but not on which namespace. Ingest leaves
72
+ * it unset — there it is derived from the namespace and cluster
73
+ * (isKubernetesDatabaseCaller).
74
+ */
75
+ runsInKubernetes?: boolean | undefined;
76
+ }
77
+
78
+ export type DatabaseEndpointPurpose = "client-call" | "collector";
79
+
80
+ export type DatabaseEndpointScope = "global" | "local";
81
+
82
+ export type DatabaseWorkloadPlatform = "kubernetes" | "docker" | "podman";
83
+
84
+ /*
85
+ * DNS suffixes that only resolve inside one network: `.local` (mDNS, and the
86
+ * `cluster.local` Kubernetes domain), `.internal` (ICANN's private-use TLD —
87
+ * `ec2.internal`, `compute.internal`, GCE's `c.<project>.internal`),
88
+ * `.home.arpa` (RFC 8375) and `.localdomain` (the placeholder domain of a
89
+ * machine with none). The same name in two VPCs or clusters can be two
90
+ * different servers, so they are qualified like private IPs.
91
+ */
92
+ export const NETWORK_SCOPED_NAME_SUFFIXES: ReadonlyArray<string> = [
93
+ ".local",
94
+ ".internal",
95
+ ".home.arpa",
96
+ ".localdomain",
97
+ ];
98
+
99
+ /*
100
+ * The attributes a SQL Server client span may name its instance in, most
101
+ * specific first: the legacy `db.mssql.instance_name`, then the instance
102
+ * half of `db.namespace` (`instance|database` for a named instance).
103
+ */
104
+ export const DATABASE_INSTANCE_NAME_ATTRIBUTE: string =
105
+ "db.mssql.instance_name";
106
+ export const DATABASE_NAMESPACE_ATTRIBUTE: string = "db.namespace";
107
+ export const DATABASE_INSTANCE_ATTRIBUTES: ReadonlyArray<string> = [
108
+ DATABASE_INSTANCE_NAME_ATTRIBUTE,
109
+ DATABASE_NAMESPACE_ATTRIBUTE,
110
+ ];
111
+
112
+ const SQL_SERVER_SYSTEM: string = "microsoft.sql_server";
113
+
114
+ // The default instance: reached on the engine's default port, named by none.
115
+ const SQL_SERVER_DEFAULT_INSTANCE: string = "mssqlserver";
116
+
117
+ /*
118
+ * SQL Server instance names: at most 16 letters, digits, "_" and "$"
119
+ * (lowercased here). "#" is refused: it would end the address as a URL
120
+ * fragment, and the name could not round-trip.
121
+ */
122
+ const SQL_SERVER_INSTANCE_REGEX: RegExp = /^[a-z0-9_$-]{1,16}$/;
123
+
124
+ // `scheme://`, including the JDBC `jdbc:postgresql://` spelling.
125
+ const URL_SCHEME_REGEX: RegExp = /^(?:jdbc:)?[a-z][a-z0-9+.-]*:\/\//i;
126
+
127
+ /*
128
+ * Where the address part of a value ends: a path, a query, a fragment, or
129
+ * the `;property=value` list of JDBC SQL Server URLs.
130
+ */
131
+ const ADDRESS_END_REGEX: RegExp = /[/?#;]/;
132
+
133
+ /*
134
+ * ADO.NET / SQL Server `tcp:host,1433` protocol prefix. Not stripped when
135
+ * what follows is only digits, so a host literally named "tcp" with a port
136
+ * ("tcp:5432") is still read as host:port.
137
+ */
138
+ const SQL_SERVER_TCP_PREFIX_REGEX: RegExp = /^tcp:(?!\d+$)/i;
139
+
140
+ /*
141
+ * SQL Server spellings of "this machine": `(local)`, `.` and LocalDB. They
142
+ * are mapped to "localhost" so the loopback rule treats them like any other
143
+ * loopback address.
144
+ */
145
+ const LOCAL_MACHINE_ALIASES: ReadonlySet<string> = new Set<string>([
146
+ "(local)",
147
+ ".",
148
+ "(localdb)",
149
+ ]);
150
+
151
+ /*
152
+ * "localhost" and the /etc/hosts names distributions give the loopback
153
+ * address. Not `*.localhost`, but they resolve to 127.0.0.1 / ::1 on the
154
+ * caller's own machine all the same.
155
+ */
156
+ const LOOPBACK_HOST_NAMES: ReadonlySet<string> = new Set<string>([
157
+ "localhost",
158
+ "localhost.localdomain",
159
+ "localhost4",
160
+ "localhost4.localdomain4",
161
+ "localhost6",
162
+ "localhost6.localdomain6",
163
+ "ip6-localhost",
164
+ "ip6-loopback",
165
+ ]);
166
+
167
+ /*
168
+ * Names that resolve to the machine the caller runs on (Docker Desktop,
169
+ * Podman, kind), so like loopback they mean something different for every
170
+ * caller. Besides these, every `*.docker.internal` name and every
171
+ * `host.<tool>.internal` name (minikube, k3d, Lima, OrbStack, Rancher
172
+ * Desktop) is host-relative too (isHostRelativeDatabaseHost).
173
+ */
174
+ const HOST_RELATIVE_NAMES: ReadonlySet<string> = new Set<string>([
175
+ "host.docker.internal",
176
+ "host.containers.internal",
177
+ "gateway.docker.internal",
178
+ "docker.for.mac.localhost",
179
+ "kubernetes.docker.internal",
180
+ ]);
181
+
182
+ const DOCKER_DESKTOP_SUFFIX: string = ".docker.internal";
183
+
184
+ const EPHEMERAL_CLOUD_PLATFORMS: ReadonlySet<string> = new Set<string>([
185
+ "aws_ecs",
186
+ "aws_lambda",
187
+ "aws_app_runner",
188
+ "gcp_cloud_run",
189
+ "gcp_cloud_functions",
190
+ "gcp_app_engine",
191
+ "azure_container_apps",
192
+ "azure_container_instances",
193
+ "azure_functions",
194
+ ]);
195
+
196
+ const KUBERNETES_IDENTITY_ATTRIBUTES: ReadonlyArray<string> = [
197
+ "k8s.pod.name",
198
+ "k8s.namespace.name",
199
+ "k8s.cluster.name",
200
+ "k8s.node.name",
201
+ ];
202
+
203
+ const KUBERNETES_CLUSTER_DOMAIN_SUFFIX: string = ".svc.cluster.local";
204
+
205
+ /*
206
+ * `<a>.<b>.svc` or `<a>.<b>.svc.<any cluster domain>` (a Service), and the
207
+ * three-label `<pod>.<svc>.<ns>.svc…` form (a StatefulSet member behind a
208
+ * headless Service).
209
+ */
210
+ const KUBERNETES_SERVICE_DNS_REGEX: RegExp =
211
+ /^((?:[a-z0-9_-]+\.){1,2}[a-z0-9_-]+)\.svc(?:\..+)?$/;
212
+
213
+ /*
214
+ * Second labels that make a two-label name a private DNS zone rather than a
215
+ * Kubernetes `<service>.<namespace>`: `db.internal` from a pod is far more
216
+ * likely a VPC name than a Service in a namespace called "internal". Such a
217
+ * name is still network-scoped (NETWORK_SCOPED_NAME_SUFFIXES).
218
+ */
219
+ const NON_NAMESPACE_TOP_LABELS: ReadonlySet<string> = new Set<string>([
220
+ "local",
221
+ "internal",
222
+ "localdomain",
223
+ ]);
224
+
225
+ // RFC 1123 label, as Kubernetes validates namespace / Service / pod names.
226
+ const DNS_LABEL_REGEX: RegExp = /^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$/;
227
+
228
+ const HOSTNAME_LABEL_REGEX: RegExp = /^[a-z0-9_-]{1,63}$/;
229
+
230
+ const DIGITS_ONLY_REGEX: RegExp = /^\d+$/;
231
+
232
+ const IPV6_CHARACTERS_REGEX: RegExp = /^[0-9a-f:.]+$/;
233
+
234
+ const IPV4_REGEX: RegExp = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
235
+
236
+ const IPV6_GROUP_REGEX: RegExp = /^[0-9a-f]{1,4}$/;
237
+
238
+ // A character a bare user name never has: a dot, colon, bracket or backslash.
239
+ const NOT_A_BARE_WORD_REGEX: RegExp = /[.:[\]\\]/;
240
+
241
+ // The member number of an Atlas host: `<cluster>-shard-<NN>-<NN>`.
242
+ const ATLAS_MEMBER_NUMBER_REGEX: RegExp = /^\d{2}-\d{2}$/;
243
+ const ATLAS_MEMBER_MARKER: string = "-shard-";
244
+ const ATLAS_DOMAIN: string = "mongodb.net";
245
+
246
+ const DISPLAY_NAME_MAX_LENGTH: number = 100;
247
+
248
+ // How much of a rejected value a message repeats back.
249
+ const MAX_ECHOED_VALUE_LENGTH: number = 100;
250
+
251
+ // ---- low-level parsing ---------------------------------------------------
252
+
253
+ function parseIpv4Octets(text: string): Array<number> | null {
254
+ const match: RegExpExecArray | null = IPV4_REGEX.exec(text);
255
+ if (!match) {
256
+ return null;
257
+ }
258
+ const octets: Array<number> = [
259
+ Number(match[1]),
260
+ Number(match[2]),
261
+ Number(match[3]),
262
+ Number(match[4]),
263
+ ];
264
+ for (const octet of octets) {
265
+ if (octet > 255) {
266
+ return null;
267
+ }
268
+ }
269
+ return octets;
270
+ }
271
+
272
+ function parseIpv4(text: string): string | null {
273
+ const octets: Array<number> | null = parseIpv4Octets(text);
274
+ return octets ? octets.join(".") : null;
275
+ }
276
+
277
+ /** The eight 16-bit groups of an IPv6 literal (zone id stripped), or null. */
278
+ function parseIpv6Groups(text: string): Array<number> | null {
279
+ let value: string = text.trim().toLowerCase();
280
+
281
+ const zoneIndex: number = value.indexOf("%");
282
+ if (zoneIndex >= 0) {
283
+ value = value.substring(0, zoneIndex);
284
+ }
285
+
286
+ if (!value.includes(":") || !IPV6_CHARACTERS_REGEX.test(value)) {
287
+ return null;
288
+ }
289
+
290
+ // An embedded dotted IPv4 tail ("::ffff:10.0.0.5") becomes two groups.
291
+ const lastColon: number = value.lastIndexOf(":");
292
+ const tail: string = value.substring(lastColon + 1);
293
+ if (tail.includes(".")) {
294
+ const octets: Array<number> | null = parseIpv4Octets(tail);
295
+ if (!octets) {
296
+ return null;
297
+ }
298
+ value = `${value.substring(0, lastColon + 1)}${(
299
+ (octets[0]! << 8) |
300
+ octets[1]!
301
+ ).toString(16)}:${((octets[2]! << 8) | octets[3]!).toString(16)}`;
302
+ }
303
+
304
+ const doubleColonCount: number = value.split("::").length - 1;
305
+ if (doubleColonCount > 1) {
306
+ return null;
307
+ }
308
+
309
+ let parts: Array<string>;
310
+ if (doubleColonCount === 1) {
311
+ const halves: Array<string> = value.split("::");
312
+ const head: Array<string> = halves[0] ? halves[0].split(":") : [];
313
+ const rest: Array<string> = halves[1] ? halves[1].split(":") : [];
314
+ const missing: number = 8 - head.length - rest.length;
315
+ if (missing < 1) {
316
+ return null;
317
+ }
318
+ parts = [...head, ...new Array<string>(missing).fill("0"), ...rest];
319
+ } else {
320
+ parts = value.split(":");
321
+ }
322
+
323
+ if (parts.length !== 8) {
324
+ return null;
325
+ }
326
+
327
+ const groups: Array<number> = [];
328
+ for (const part of parts) {
329
+ if (!IPV6_GROUP_REGEX.test(part)) {
330
+ return null;
331
+ }
332
+ groups.push(parseInt(part, 16));
333
+ }
334
+ return groups;
335
+ }
336
+
337
+ /*
338
+ * Canonical text form of an IPv6 literal (RFC 5952: lowercase, no leading
339
+ * zeros, the longest run of two or more zero groups compressed to "::"), or
340
+ * the dotted IPv4 address for an IPv4-mapped one (::ffff:a.b.c.d).
341
+ */
342
+ function canonicalizeIpv6(text: string): string | null {
343
+ const groups: Array<number> | null = parseIpv6Groups(text);
344
+ if (!groups) {
345
+ return null;
346
+ }
347
+
348
+ const isIpv4Mapped: boolean =
349
+ groups[0] === 0 &&
350
+ groups[1] === 0 &&
351
+ groups[2] === 0 &&
352
+ groups[3] === 0 &&
353
+ groups[4] === 0 &&
354
+ groups[5] === 0xffff;
355
+ if (isIpv4Mapped) {
356
+ return [
357
+ groups[6]! >> 8,
358
+ groups[6]! & 0xff,
359
+ groups[7]! >> 8,
360
+ groups[7]! & 0xff,
361
+ ].join(".");
362
+ }
363
+
364
+ let bestStart: number = -1;
365
+ let bestLength: number = 0;
366
+ let runStart: number = -1;
367
+ for (let index: number = 0; index <= groups.length; index++) {
368
+ if (index < groups.length && groups[index] === 0) {
369
+ if (runStart < 0) {
370
+ runStart = index;
371
+ }
372
+ continue;
373
+ }
374
+ if (runStart >= 0) {
375
+ const runLength: number = index - runStart;
376
+ if (runLength > bestLength) {
377
+ bestStart = runStart;
378
+ bestLength = runLength;
379
+ }
380
+ runStart = -1;
381
+ }
382
+ }
383
+
384
+ const hex: Array<string> = groups.map((group: number): string => {
385
+ return group.toString(16);
386
+ });
387
+
388
+ if (bestLength < 2) {
389
+ return hex.join(":");
390
+ }
391
+
392
+ const head: string = hex.slice(0, bestStart).join(":");
393
+ const tail: string = hex.slice(bestStart + bestLength).join(":");
394
+ return `${head}::${tail}`;
395
+ }
396
+
397
+ function isValidHostname(host: string): boolean {
398
+ if (host.length === 0 || host.length > 253) {
399
+ return false;
400
+ }
401
+ const labels: Array<string> = host.split(".");
402
+ for (const label of labels) {
403
+ if (!HOSTNAME_LABEL_REGEX.test(label)) {
404
+ return false;
405
+ }
406
+ }
407
+ /*
408
+ * A top-level label is never all digits (RFC 3696 §2), so "999.1.1.1" or
409
+ * "12345" is a mangled address, not a name.
410
+ */
411
+ return !DIGITS_ONLY_REGEX.test(labels[labels.length - 1]!);
412
+ }
413
+
414
+ function toValidPort(value: unknown): number | null {
415
+ let numeric: number;
416
+ if (typeof value === "number") {
417
+ numeric = value;
418
+ } else if (typeof value === "string") {
419
+ const trimmed: string = value.trim();
420
+ if (!DIGITS_ONLY_REGEX.test(trimmed)) {
421
+ return null;
422
+ }
423
+ numeric = Number(trimmed);
424
+ } else {
425
+ return null;
426
+ }
427
+ return Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535
428
+ ? numeric
429
+ : null;
430
+ }
431
+
432
+ function canonicalClusterName(value: unknown): string {
433
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
434
+ }
435
+
436
+ function canonicalDnsLabel(value: unknown): string | null {
437
+ if (typeof value !== "string") {
438
+ return null;
439
+ }
440
+ const label: string = value.trim().toLowerCase();
441
+ return DNS_LABEL_REGEX.test(label) ? label : null;
442
+ }
443
+
444
+ function isNonBlankText(value: unknown): boolean {
445
+ return typeof value === "string" && value.trim().length > 0;
446
+ }
447
+
448
+ /*
449
+ * A SQL Server instance name as it becomes part of an identity: trimmed,
450
+ * lowercased, validated; the default instance (MSSQLSERVER) is no instance.
451
+ * `undefined` for an invalid name, null for none.
452
+ */
453
+ function canonicalInstanceName(value: unknown): string | null | undefined {
454
+ if (value === null || value === undefined) {
455
+ return null;
456
+ }
457
+ if (typeof value !== "string") {
458
+ return undefined;
459
+ }
460
+ const instance: string = value.trim().toLowerCase();
461
+ if (!instance) {
462
+ return null;
463
+ }
464
+ if (!SQL_SERVER_INSTANCE_REGEX.test(instance)) {
465
+ return undefined;
466
+ }
467
+ return instance === SQL_SERVER_DEFAULT_INSTANCE ? null : instance;
468
+ }
469
+
470
+ /**
471
+ * A host as stored in a DatabaseEndpoint, split into the network host and
472
+ * the SQL Server named instance it may carry (`sql1.corp\inst01` →
473
+ * `sql1.corp` + `inst01`; everything else → the host + "").
474
+ */
475
+ export function splitDatabaseHostInstance(host: string): {
476
+ host: string;
477
+ instance: string;
478
+ } {
479
+ const value: string = typeof host === "string" ? host : "";
480
+ const index: number = value.indexOf("\\");
481
+ return index >= 0
482
+ ? { host: value.substring(0, index), instance: value.substring(index + 1) }
483
+ : { host: value, instance: "" };
484
+ }
485
+
486
+ function joinHostInstance(host: string, instance: string | null): string {
487
+ return instance ? `${host}\\${instance}` : host;
488
+ }
489
+
490
+ /*
491
+ * Where the address in a raw value is: the URL authority (scheme, userinfo
492
+ * and path dropped) or the value up to its path / query / JDBC properties,
493
+ * `tcp:` prefix dropped. May still hold a host list. Null for nothing.
494
+ */
495
+ function extractAddressText(raw: unknown): string | null {
496
+ if (typeof raw !== "string") {
497
+ return null;
498
+ }
499
+
500
+ let value: string = raw.trim();
501
+ if (!value || value.startsWith("/")) {
502
+ return null;
503
+ }
504
+
505
+ const schemeMatch: RegExpExecArray | null = URL_SCHEME_REGEX.exec(value);
506
+ if (schemeMatch) {
507
+ const afterScheme: string = value.substring(schemeMatch[0].length);
508
+ const authorityEnd: number = afterScheme.search(ADDRESS_END_REGEX);
509
+ let authority: string =
510
+ authorityEnd >= 0 ? afterScheme.substring(0, authorityEnd) : afterScheme;
511
+ const userInfoEnd: number = authority.lastIndexOf("@");
512
+ if (userInfoEnd >= 0) {
513
+ authority = authority.substring(userInfoEnd + 1);
514
+ }
515
+ value = authority;
516
+ } else {
517
+ value = value.replace(SQL_SERVER_TCP_PREFIX_REGEX, "");
518
+ const pathStart: number = value.search(ADDRESS_END_REGEX);
519
+ if (pathStart >= 0) {
520
+ value = value.substring(0, pathStart);
521
+ }
522
+ }
523
+
524
+ value = value.trim();
525
+ return value ? value : null;
526
+ }
527
+
528
+ /*
529
+ * One host of an address: `host`, `host:port`, `[v6]:port`, bare IPv6,
530
+ * `host\instance[:port]` and the SQL Server local-machine spellings.
531
+ * `portText` is a port found beside it (the `,1433` of SQL Server); a port
532
+ * outside 1..65535 is dropped, or — with `strictPort` — rejects the value.
533
+ */
534
+ function parseSingleHost(
535
+ text: string,
536
+ initialPortText: string | null,
537
+ strictPort: boolean,
538
+ ): ParsedHostAndPort | null {
539
+ let value: string = text.trim();
540
+ let portText: string | null = initialPortText;
541
+
542
+ // "host\instance" (optionally "host\instance:port"): a SQL Server named instance.
543
+ let instance: string | null = null;
544
+ const backslashIndex: number = value.indexOf("\\");
545
+ if (backslashIndex >= 0) {
546
+ let instanceText: string = value.substring(backslashIndex + 1).trim();
547
+ value = value.substring(0, backslashIndex).trim();
548
+ const colonIndex: number = instanceText.indexOf(":");
549
+ if (colonIndex >= 0) {
550
+ if (portText === null) {
551
+ portText = instanceText.substring(colonIndex + 1);
552
+ }
553
+ instanceText = instanceText.substring(0, colonIndex);
554
+ }
555
+ const canonical: string | null | undefined =
556
+ canonicalInstanceName(instanceText);
557
+ if (canonical === undefined) {
558
+ return null;
559
+ }
560
+ instance = canonical;
561
+ }
562
+
563
+ if (!value) {
564
+ return null;
565
+ }
566
+
567
+ let host: string;
568
+
569
+ if (LOCAL_MACHINE_ALIASES.has(value.toLowerCase())) {
570
+ host = "localhost";
571
+ } else if (value.startsWith("[")) {
572
+ const closing: number = value.indexOf("]");
573
+ if (closing < 0) {
574
+ return null;
575
+ }
576
+ const rest: string = value.substring(closing + 1);
577
+ if (rest) {
578
+ if (!rest.startsWith(":")) {
579
+ return null;
580
+ }
581
+ if (portText === null) {
582
+ portText = rest.substring(1);
583
+ }
584
+ }
585
+ const ipv6: string | null = canonicalizeIpv6(value.substring(1, closing));
586
+ if (!ipv6) {
587
+ // "[REDACTED]", "[HASHED:ab12cd34]" and other bracketed non-addresses.
588
+ return null;
589
+ }
590
+ host = ipv6;
591
+ } else {
592
+ const colonCount: number = (value.match(/:/g) || []).length;
593
+ if (colonCount >= 2) {
594
+ const ipv6: string | null = canonicalizeIpv6(value);
595
+ if (!ipv6) {
596
+ return null;
597
+ }
598
+ host = ipv6;
599
+ } else {
600
+ let hostText: string = value;
601
+ if (colonCount === 1) {
602
+ const colonIndex: number = value.indexOf(":");
603
+ hostText = value.substring(0, colonIndex);
604
+ if (portText === null) {
605
+ portText = value.substring(colonIndex + 1);
606
+ }
607
+ }
608
+
609
+ hostText = trimTrailingCharacter(hostText.trim().toLowerCase(), ".");
610
+ if (!hostText) {
611
+ return null;
612
+ }
613
+
614
+ const ipv4: string | null = parseIpv4(hostText);
615
+ if (ipv4) {
616
+ host = ipv4;
617
+ } else if (isValidHostname(hostText)) {
618
+ host = hostText;
619
+ } else {
620
+ return null;
621
+ }
622
+ }
623
+ }
624
+
625
+ let port: number | null = null;
626
+ if (portText !== null && portText.trim() !== "") {
627
+ const trimmedPort: string = portText.trim();
628
+ if (!DIGITS_ONLY_REGEX.test(trimmedPort)) {
629
+ return null;
630
+ }
631
+ port = toValidPort(trimmedPort);
632
+ if (port === null && strictPort) {
633
+ return null;
634
+ }
635
+ }
636
+
637
+ return instance ? { host, port, instance } : { host, port };
638
+ }
639
+
640
+ // Canonical order of the hosts of a list: host, then instance, then port.
641
+ function compareParsedHosts(
642
+ a: ParsedHostAndPort,
643
+ b: ParsedHostAndPort,
644
+ ): number {
645
+ if (a.host !== b.host) {
646
+ return a.host < b.host ? -1 : 1;
647
+ }
648
+ const instanceA: string = a.instance || "";
649
+ const instanceB: string = b.instance || "";
650
+ if (instanceA !== instanceB) {
651
+ return instanceA < instanceB ? -1 : 1;
652
+ }
653
+ return (a.port ?? 0) - (b.port ?? 0);
654
+ }
655
+
656
+ function parseAddressHosts(
657
+ raw: unknown,
658
+ strictPort: boolean,
659
+ ): Array<ParsedHostAndPort> | null {
660
+ const address: string | null = extractAddressText(raw);
661
+ if (address === null) {
662
+ return null;
663
+ }
664
+
665
+ const parts: Array<string> = address.split(",");
666
+
667
+ // "host,1433": SQL Server's port separator, not a list.
668
+ if (parts.length === 2 && DIGITS_ONLY_REGEX.test(parts[1]!.trim())) {
669
+ const single: ParsedHostAndPort | null = parseSingleHost(
670
+ parts[0]!,
671
+ parts[1]!.trim(),
672
+ strictPort,
673
+ );
674
+ return single ? [single] : null;
675
+ }
676
+
677
+ const hosts: Array<ParsedHostAndPort> = [];
678
+ const seen: Set<string> = new Set<string>();
679
+ for (const part of parts) {
680
+ if (!part.trim()) {
681
+ continue;
682
+ }
683
+ const parsed: ParsedHostAndPort | null = parseSingleHost(
684
+ part,
685
+ null,
686
+ strictPort,
687
+ );
688
+ // A list is one logical server only when every host in it is readable.
689
+ if (!parsed) {
690
+ return null;
691
+ }
692
+ const key: string = `${parsed.host}\\${parsed.instance || ""}:${
693
+ parsed.port ?? ""
694
+ }`;
695
+ if (!seen.has(key)) {
696
+ seen.add(key);
697
+ hosts.push(parsed);
698
+ }
699
+ }
700
+
701
+ if (hosts.length === 0) {
702
+ return null;
703
+ }
704
+
705
+ hosts.sort(compareParsedHosts);
706
+ return hosts;
707
+ }
708
+
709
+ /**
710
+ * Parse a raw address attribute / connection string into a canonical host
711
+ * and an optional port. Accepts `host`, `host:port`, `[v6]:port`, bare IPv6
712
+ * (zone id stripped), URLs (`scheme://user:pw@host:port/db?x` — the userinfo
713
+ * split on the LAST "@" before the first "/"; JDBC `;key=value` properties
714
+ * dropped), host lists and the SQL Server forms `tcp:host,1433`,
715
+ * `host\instance` (the instance returned beside the host; MSSQLSERVER, the
716
+ * default instance, is no instance), `(local)` and `.`.
717
+ *
718
+ * A host list (`m1:27017,m2:27017`) names ONE logical server, so it yields
719
+ * the same host whatever order a client lists the members in: the first in
720
+ * canonical order (see parseHostAndPortList). Every listed host must parse.
721
+ *
722
+ * The host must then be a valid IPv4, IPv6 or RFC 1123 hostname — anything
723
+ * else (scrubbed values, "*", whitespace, a unix socket path) is null. A
724
+ * port outside 1..65535 is dropped (null); a non-numeric port rejects the
725
+ * whole value.
726
+ */
727
+ export function parseHostAndPort(raw: unknown): ParsedHostAndPort | null {
728
+ const hosts: Array<ParsedHostAndPort> | null = parseAddressHosts(raw, false);
729
+ return hosts && hosts.length > 0 ? hosts[0]! : null;
730
+ }
731
+
732
+ /**
733
+ * Every host a raw address names, deduplicated and in canonical order —
734
+ * one entry for a single host, several for a host list — or null when the
735
+ * value (or any host in its list) is unparseable. The first entry is what
736
+ * parseHostAndPort returns.
737
+ */
738
+ export function parseHostAndPortList(
739
+ raw: unknown,
740
+ ): Array<ParsedHostAndPort> | null {
741
+ return parseAddressHosts(raw, false);
742
+ }
743
+
744
+ // ---- host classification -------------------------------------------------
745
+
746
+ /** True for an IPv4 or IPv6 literal (brackets and a `\instance` tolerated). */
747
+ export function isIpLiteralHost(host: string): boolean {
748
+ if (typeof host !== "string") {
749
+ return false;
750
+ }
751
+ const value: string = splitDatabaseHostInstance(host.trim())
752
+ .host.trim()
753
+ .replace(/^\[(.*)\]$/, "$1");
754
+ if (!value) {
755
+ return false;
756
+ }
757
+ return parseIpv4(value) !== null || parseIpv6Groups(value) !== null;
758
+ }
759
+
760
+ /**
761
+ * localhost (and the distributions' /etc/hosts aliases such as
762
+ * localhost.localdomain), *.localhost, 127/8, ::1, the unspecified addresses
763
+ * (:: and 0.0.0.0) and their IPv4-mapped IPv6 spellings.
764
+ */
765
+ export function isLoopbackDatabaseHost(host: string): boolean {
766
+ if (typeof host !== "string") {
767
+ return false;
768
+ }
769
+ let value: string = splitDatabaseHostInstance(
770
+ host.trim().toLowerCase(),
771
+ ).host.trim();
772
+ if (!value) {
773
+ return false;
774
+ }
775
+ if (LOCAL_MACHINE_ALIASES.has(value)) {
776
+ return true;
777
+ }
778
+ if (value.includes(":")) {
779
+ value = canonicalizeIpv6(value.replace(/^\[(.*)\]$/, "$1")) || value;
780
+ }
781
+ value = trimTrailingCharacter(value, ".");
782
+ if (LOOPBACK_HOST_NAMES.has(value) || value.endsWith(".localhost")) {
783
+ return true;
784
+ }
785
+ if (value === "::1" || value === "::" || value === "0.0.0.0") {
786
+ return true;
787
+ }
788
+ const octets: Array<number> | null = parseIpv4Octets(value);
789
+ return octets !== null && octets[0] === 127;
790
+ }
791
+
792
+ /*
793
+ * `host.<tool>.internal`: minikube, k3d, Lima, OrbStack, Rancher Desktop —
794
+ * the per-machine gateway name of each local Kubernetes / container tool.
795
+ * Checked with string operations (the tool label is one RFC 1123 label).
796
+ */
797
+ function isHostGatewayName(value: string): boolean {
798
+ const labels: Array<string> = value.split(".");
799
+ return (
800
+ labels.length === 3 &&
801
+ labels[0] === "host" &&
802
+ labels[2] === "internal" &&
803
+ DNS_LABEL_REGEX.test(labels[1]!)
804
+ );
805
+ }
806
+
807
+ /**
808
+ * host.docker.internal and the other "the machine I run on" names: every
809
+ * `*.docker.internal` name, every `host.<tool>.internal` name (minikube,
810
+ * k3d, Lima, OrbStack, Rancher Desktop) and Podman's
811
+ * host.containers.internal.
812
+ */
813
+ export function isHostRelativeDatabaseHost(host: string): boolean {
814
+ if (typeof host !== "string") {
815
+ return false;
816
+ }
817
+ const value: string = trimTrailingCharacter(
818
+ splitDatabaseHostInstance(host.trim().toLowerCase()).host.trim(),
819
+ ".",
820
+ );
821
+ return (
822
+ HOST_RELATIVE_NAMES.has(value) ||
823
+ value.endsWith(DOCKER_DESKTOP_SUFFIX) ||
824
+ isHostGatewayName(value)
825
+ );
826
+ }
827
+
828
+ function stripBracketsAndInstance(host: string): string {
829
+ return splitDatabaseHostInstance(host.trim())
830
+ .host.trim()
831
+ .replace(/^\[(.*)\]$/, "$1");
832
+ }
833
+
834
+ /**
835
+ * RFC 1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10) and IPv6 ULA
836
+ * (fc00::/7) — ranges that are reused by every cluster and VPC, so an
837
+ * address in them only identifies something together with where it was
838
+ * seen from.
839
+ */
840
+ export function isPrivateIpHost(host: string): boolean {
841
+ if (typeof host !== "string") {
842
+ return false;
843
+ }
844
+ const value: string = stripBracketsAndInstance(host);
845
+
846
+ const octets: Array<number> | null = parseIpv4Octets(value);
847
+ if (octets) {
848
+ const first: number = octets[0]!;
849
+ const second: number = octets[1]!;
850
+ return (
851
+ first === 10 ||
852
+ (first === 172 && second >= 16 && second <= 31) ||
853
+ (first === 192 && second === 168) ||
854
+ (first === 100 && second >= 64 && second <= 127)
855
+ );
856
+ }
857
+
858
+ const canonical: string | null = value.includes(":")
859
+ ? canonicalizeIpv6(value)
860
+ : null;
861
+ if (!canonical) {
862
+ return false;
863
+ }
864
+ if (!canonical.includes(":")) {
865
+ // IPv4-mapped: classify the unwrapped IPv4 address.
866
+ return isPrivateIpHost(canonical);
867
+ }
868
+ const groups: Array<number> | null = parseIpv6Groups(canonical);
869
+ return groups !== null && (groups[0]! & 0xfe00) === 0xfc00;
870
+ }
871
+
872
+ /**
873
+ * Link-local addresses — IPv4 169.254/16 and IPv6 fe80::/10 (zone id
874
+ * dropped). Every network link has its own, so one never identifies a
875
+ * server on its own (always LOCAL scope, see getDatabaseEndpointScope).
876
+ */
877
+ export function isLinkLocalIpHost(host: string): boolean {
878
+ if (typeof host !== "string") {
879
+ return false;
880
+ }
881
+ const value: string = stripBracketsAndInstance(host);
882
+
883
+ const octets: Array<number> | null = parseIpv4Octets(value);
884
+ if (octets) {
885
+ return octets[0] === 169 && octets[1] === 254;
886
+ }
887
+
888
+ const canonical: string | null = value.includes(":")
889
+ ? canonicalizeIpv6(value)
890
+ : null;
891
+ if (!canonical) {
892
+ return false;
893
+ }
894
+ if (!canonical.includes(":")) {
895
+ return isLinkLocalIpHost(canonical);
896
+ }
897
+ const groups: Array<number> | null = parseIpv6Groups(canonical);
898
+ return groups !== null && (groups[0]! & 0xffc0) === 0xfe80;
899
+ }
900
+
901
+ function isSingleLabelName(host: string): boolean {
902
+ return !host.includes(".") && !isIpLiteralHost(host);
903
+ }
904
+
905
+ /**
906
+ * True for a host that only resolves inside one Kubernetes cluster or
907
+ * private network — cluster-local DNS and the other private DNS zones
908
+ * (NETWORK_SCOPED_NAME_SUFFIXES), private IPs and link-local IPs — and so
909
+ * is qualified with the caller's cluster (`…@<cluster>`), or LOCAL scope
910
+ * without one. A `\instance` is ignored.
911
+ */
912
+ export function isClusterScopedDatabaseHost(host: string): boolean {
913
+ if (typeof host !== "string") {
914
+ return false;
915
+ }
916
+ const value: string = trimTrailingCharacter(
917
+ splitDatabaseHostInstance(host.trim().toLowerCase()).host.trim(),
918
+ ".",
919
+ );
920
+ if (!value) {
921
+ return false;
922
+ }
923
+ for (const suffix of NETWORK_SCOPED_NAME_SUFFIXES) {
924
+ if (value.endsWith(suffix)) {
925
+ return true;
926
+ }
927
+ }
928
+ return isPrivateIpHost(value) || isLinkLocalIpHost(value);
929
+ }
930
+
931
+ /**
932
+ * True for a Kubernetes Service DNS name — `<service>.<namespace>.svc`, with
933
+ * or without its cluster domain (`.svc.cluster.local`), or
934
+ * `<pod>.<service>.<namespace>.svc…` for a StatefulSet member — which only a
935
+ * pod inside one cluster can resolve. The same test the endpoint parser
936
+ * expands such names with. Case and a trailing dot are ignored, as is a
937
+ * `\instance`; a port or `@cluster` is not part of a host.
938
+ */
939
+ export function isKubernetesServiceDnsHost(host: string): boolean {
940
+ if (typeof host !== "string") {
941
+ return false;
942
+ }
943
+ const value: string = trimTrailingCharacter(
944
+ splitDatabaseHostInstance(host.trim().toLowerCase()).host.trim(),
945
+ ".",
946
+ );
947
+ return value.length > 0 && KUBERNETES_SERVICE_DNS_REGEX.test(value);
948
+ }
949
+
950
+ /**
951
+ * True when the caller runs in Kubernetes as far as its context tells: it
952
+ * has a namespace or a cluster (or the discovery query said so, see
953
+ * DatabaseCallerContext.runsInKubernetes).
954
+ */
955
+ export function isKubernetesDatabaseCaller(
956
+ caller: DatabaseCallerContext | null | undefined,
957
+ ): boolean {
958
+ if (!caller) {
959
+ return false;
960
+ }
961
+ return (
962
+ caller.runsInKubernetes === true ||
963
+ isNonBlankText(caller.kubernetesNamespace) ||
964
+ canonicalClusterName(caller.kubernetesClusterName).length > 0
965
+ );
966
+ }
967
+
968
+ /*
969
+ * `<pod>-<ordinal>.<service>`: a two-label name whose first label is a
970
+ * StatefulSet pod (`mongo-0`, `redis-node-2`) and whose second is the
971
+ * headless Service of the same release (`mongo`, `mongo-headless`,
972
+ * `redis-headless` — the same first dash-separated word) — a member in the
973
+ * caller's own namespace, not a Service in another one.
974
+ */
975
+ function isStatefulSetMemberShortName(labels: Array<string>): boolean {
976
+ const pod: string = labels[0]!;
977
+ const dash: number = pod.lastIndexOf("-");
978
+ if (dash <= 0 || dash === pod.length - 1) {
979
+ return false;
980
+ }
981
+ if (!DIGITS_ONLY_REGEX.test(pod.substring(dash + 1))) {
982
+ return false;
983
+ }
984
+ const release: string = pod.split("-")[0]!;
985
+ return release.length > 0 && labels[1]!.split("-")[0] === release;
986
+ }
987
+
988
+ /*
989
+ * Rule 3 (Kubernetes DNS): any `<a>.<b>.svc[.<domain>]` (and the member form
990
+ * `<pod>.<svc>.<ns>.svc[.<domain>]`) becomes `….svc.cluster.local`, so a
991
+ * custom cluster domain and the short `.svc` form key like the FQDN; a
992
+ * single-label name from a caller in a known namespace becomes that
993
+ * namespace's Service FQDN. From a caller that runs in Kubernetes, a
994
+ * two-label name is what the pod's resolver search path makes of it:
995
+ * `<service>.<namespace>` → `<service>.<namespace>.svc.cluster.local`, and
996
+ * a StatefulSet member of the caller's own namespace
997
+ * (`mongo-0.mongo-headless`) → `mongo-0.mongo-headless.<ns>.svc.cluster.local`
998
+ * (or, with no namespace known, the Service reading). Two-label names from
999
+ * anything else — a VM, a laptop — are left alone: there they are domains.
1000
+ */
1001
+ function applyKubernetesDnsRules(
1002
+ host: string,
1003
+ kubernetesNamespace: string | null | undefined,
1004
+ runsInKubernetes: boolean,
1005
+ ): string {
1006
+ const serviceMatch: RegExpExecArray | null =
1007
+ KUBERNETES_SERVICE_DNS_REGEX.exec(host);
1008
+ if (serviceMatch) {
1009
+ return `${serviceMatch[1]}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`;
1010
+ }
1011
+
1012
+ const namespace: string | null = canonicalDnsLabel(kubernetesNamespace);
1013
+
1014
+ if (isSingleLabelName(host)) {
1015
+ if (namespace) {
1016
+ return `${host}.${namespace}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`;
1017
+ }
1018
+ return host;
1019
+ }
1020
+
1021
+ if (!runsInKubernetes) {
1022
+ return host;
1023
+ }
1024
+
1025
+ const labels: Array<string> = host.split(".");
1026
+ if (
1027
+ labels.length !== 2 ||
1028
+ !DNS_LABEL_REGEX.test(labels[0]!) ||
1029
+ !DNS_LABEL_REGEX.test(labels[1]!) ||
1030
+ NON_NAMESPACE_TOP_LABELS.has(labels[1]!)
1031
+ ) {
1032
+ return host;
1033
+ }
1034
+
1035
+ if (namespace && isStatefulSetMemberShortName(labels)) {
1036
+ return `${host}.${namespace}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`;
1037
+ }
1038
+
1039
+ return `${host}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`;
1040
+ }
1041
+
1042
+ function buildEndpoint(
1043
+ host: string,
1044
+ port: number | null,
1045
+ kubernetesClusterName: string,
1046
+ ): DatabaseEndpoint {
1047
+ return kubernetesClusterName
1048
+ ? { host, port, kubernetesClusterName }
1049
+ : { host, port };
1050
+ }
1051
+
1052
+ // ---- caller context ------------------------------------------------------
1053
+
1054
+ /*
1055
+ * Resource attributes are FLAT semconv keys; the `resource.`-prefixed
1056
+ * spelling used by stored telemetry rows is accepted as a fallback so a
1057
+ * caller holding a stored attribute map gets the same answer.
1058
+ */
1059
+ function readResourceAttribute(
1060
+ attributes: Record<string, unknown> | null | undefined,
1061
+ key: string,
1062
+ ): string | null {
1063
+ if (!attributes || typeof attributes !== "object") {
1064
+ return null;
1065
+ }
1066
+ let value: unknown = attributes[key];
1067
+ if (value === undefined || value === null || value === "") {
1068
+ value = attributes[`resource.${key}`];
1069
+ }
1070
+ if (typeof value === "string") {
1071
+ const trimmed: string = value.trim();
1072
+ return trimmed.length > 0 ? trimmed : null;
1073
+ }
1074
+ if (typeof value === "number" || typeof value === "boolean") {
1075
+ return String(value);
1076
+ }
1077
+ return null;
1078
+ }
1079
+
1080
+ /**
1081
+ * True when the resource that made a call is not a stable machine: anything
1082
+ * in Kubernetes, in a container, on a FaaS or managed-container platform, or
1083
+ * with no `os.type` at all (no `system` resource detector ran, so
1084
+ * `host.name` is whatever the SDK guessed — usually a pod or container
1085
+ * name). Its `host.name` must never become a database's identity.
1086
+ */
1087
+ export function isEphemeralCaller(
1088
+ resourceAttributes: Record<string, unknown>,
1089
+ ): boolean {
1090
+ const attributes: Record<string, unknown> =
1091
+ resourceAttributes && typeof resourceAttributes === "object"
1092
+ ? resourceAttributes
1093
+ : {};
1094
+
1095
+ for (const key of KUBERNETES_IDENTITY_ATTRIBUTES) {
1096
+ if (readResourceAttribute(attributes, key)) {
1097
+ return true;
1098
+ }
1099
+ }
1100
+
1101
+ if (
1102
+ readResourceAttribute(attributes, "container.id") ||
1103
+ readResourceAttribute(attributes, "container.runtime")
1104
+ ) {
1105
+ return true;
1106
+ }
1107
+
1108
+ for (const key of Object.keys(attributes)) {
1109
+ const bareKey: string = key.startsWith("resource.")
1110
+ ? key.substring("resource.".length)
1111
+ : key;
1112
+ if (bareKey.startsWith("faas.") && readResourceAttribute(attributes, key)) {
1113
+ return true;
1114
+ }
1115
+ }
1116
+
1117
+ const cloudPlatform: string | null = readResourceAttribute(
1118
+ attributes,
1119
+ "cloud.platform",
1120
+ );
1121
+ if (
1122
+ cloudPlatform &&
1123
+ EPHEMERAL_CLOUD_PLATFORMS.has(cloudPlatform.toLowerCase())
1124
+ ) {
1125
+ return true;
1126
+ }
1127
+
1128
+ return readResourceAttribute(attributes, "os.type") === null;
1129
+ }
1130
+
1131
+ /** The parts of a caller's resource that endpoint canonicalization needs. */
1132
+ export function buildDatabaseCallerContext(
1133
+ resourceAttributes: Record<string, unknown>,
1134
+ ): DatabaseCallerContext {
1135
+ return {
1136
+ kubernetesNamespace: readResourceAttribute(
1137
+ resourceAttributes,
1138
+ "k8s.namespace.name",
1139
+ ),
1140
+ kubernetesClusterName: readResourceAttribute(
1141
+ resourceAttributes,
1142
+ "k8s.cluster.name",
1143
+ ),
1144
+ hostName: readResourceAttribute(resourceAttributes, "host.name"),
1145
+ isEphemeral: isEphemeralCaller(resourceAttributes),
1146
+ };
1147
+ }
1148
+
1149
+ /**
1150
+ * The SQL Server named instance a client span names beside its address —
1151
+ * `db.mssql.instance_name`, else the instance half of a `db.namespace` of
1152
+ * the form `instance|database` — or null. Only for SQL Server: other
1153
+ * engines give `db.namespace` other meanings. Returned raw (untrimmed);
1154
+ * canonicalizeDatabaseEndpoint validates it. Pass the result as its
1155
+ * `instance`; anything that memoizes on the attributes it reads should key
1156
+ * on this derived instance (as DatabaseCallEntityKeys does) or include
1157
+ * DATABASE_INSTANCE_ATTRIBUTES.
1158
+ */
1159
+ export function readDatabaseInstanceName(input: {
1160
+ system: unknown;
1161
+ getAttribute: (key: string) => unknown;
1162
+ }): string | null {
1163
+ if (!input || typeof input.getAttribute !== "function") {
1164
+ return null;
1165
+ }
1166
+ if (normalizeDatabaseSystem(input.system) !== SQL_SERVER_SYSTEM) {
1167
+ return null;
1168
+ }
1169
+
1170
+ const toText: (value: unknown) => string | null = (
1171
+ value: unknown,
1172
+ ): string | null => {
1173
+ if (typeof value === "string") {
1174
+ return value === "" ? null : value;
1175
+ }
1176
+ if (typeof value === "number") {
1177
+ return String(value);
1178
+ }
1179
+ return null;
1180
+ };
1181
+
1182
+ const instanceName: string | null = toText(
1183
+ input.getAttribute(DATABASE_INSTANCE_NAME_ATTRIBUTE),
1184
+ );
1185
+ if (instanceName !== null) {
1186
+ return instanceName;
1187
+ }
1188
+
1189
+ const namespace: string | null = toText(
1190
+ input.getAttribute(DATABASE_NAMESPACE_ATTRIBUTE),
1191
+ );
1192
+ if (namespace !== null) {
1193
+ const separator: number = namespace.indexOf("|");
1194
+ if (separator >= 0) {
1195
+ return namespace.substring(0, separator);
1196
+ }
1197
+ }
1198
+
1199
+ return null;
1200
+ }
1201
+
1202
+ // ---- canonicalization ----------------------------------------------------
1203
+
1204
+ /**
1205
+ * Canonicalize what a caller saw into a database endpoint, or null when it
1206
+ * names no identifiable server. Rules, in order:
1207
+ *
1208
+ * 1. parse the address; port = explicit port ?? parsed port ?? engine
1209
+ * default ?? null. A SQL Server named instance (`host\instance` in the
1210
+ * address, else `instance`, see readDatabaseInstanceName) is kept as
1211
+ * part of the host only while no port is known — a port already names
1212
+ * the instance — and then takes no default port;
1213
+ * 2. loopback / host-relative: for a client call → null (a per-caller
1214
+ * sidecar, proxy or dev laptop); for a collector → the collector's own
1215
+ * host.name when the collector is not ephemeral, else null;
1216
+ * 3. Kubernetes DNS expansion (see applyKubernetesDnsRules);
1217
+ * 4. cluster-local / private-network names and private IPs are qualified
1218
+ * with the caller's `k8s.cluster.name` when it has one.
1219
+ */
1220
+ export function canonicalizeDatabaseEndpoint(input: {
1221
+ system: string;
1222
+ address: string | null | undefined;
1223
+ port?: string | number | null | undefined;
1224
+ instance?: string | null | undefined;
1225
+ caller: DatabaseCallerContext;
1226
+ purpose: DatabaseEndpointPurpose;
1227
+ }): DatabaseEndpoint | null {
1228
+ if (!input) {
1229
+ return null;
1230
+ }
1231
+
1232
+ const parsed: ParsedHostAndPort | null = parseHostAndPort(input.address);
1233
+ if (!parsed) {
1234
+ return null;
1235
+ }
1236
+
1237
+ const caller: DatabaseCallerContext | null = input.caller || null;
1238
+ const knownPort: number | null = toValidPort(input.port) ?? parsed.port;
1239
+
1240
+ let instance: string | null = null;
1241
+ if (knownPort === null) {
1242
+ const reported: string | null | undefined = canonicalInstanceName(
1243
+ input.instance,
1244
+ );
1245
+ instance = parsed.instance || (reported ? reported : null);
1246
+ }
1247
+
1248
+ const port: number | null =
1249
+ knownPort ??
1250
+ (instance ? null : getDefaultDatabasePort(input.system)) ??
1251
+ null;
1252
+
1253
+ let host: string = parsed.host;
1254
+
1255
+ if (isLoopbackDatabaseHost(host) || isHostRelativeDatabaseHost(host)) {
1256
+ if (input.purpose !== "collector") {
1257
+ return null;
1258
+ }
1259
+ if (!caller || caller.isEphemeral || !caller.hostName) {
1260
+ return null;
1261
+ }
1262
+ const callerHost: ParsedHostAndPort | null = parseHostAndPort(
1263
+ caller.hostName,
1264
+ );
1265
+ if (
1266
+ !callerHost ||
1267
+ isLoopbackDatabaseHost(callerHost.host) ||
1268
+ isHostRelativeDatabaseHost(callerHost.host)
1269
+ ) {
1270
+ return null;
1271
+ }
1272
+ host = callerHost.host;
1273
+ }
1274
+
1275
+ host = applyKubernetesDnsRules(
1276
+ host,
1277
+ caller?.kubernetesNamespace,
1278
+ isKubernetesDatabaseCaller(caller),
1279
+ );
1280
+
1281
+ const clusterName: string = isClusterScopedDatabaseHost(host)
1282
+ ? canonicalClusterName(caller?.kubernetesClusterName)
1283
+ : "";
1284
+
1285
+ return buildEndpoint(joinHostInstance(host, instance), port, clusterName);
1286
+ }
1287
+
1288
+ /**
1289
+ * "local" for an endpoint that only resolves inside one network or cluster
1290
+ * and carries nothing to say which: a single-label name, cluster-local or
1291
+ * private-zone DNS or a private IP without a cluster qualifier, a link-local
1292
+ * IP (per link, qualified or not) and loopback / host-relative names,
1293
+ * defensively. Local endpoints are still keyed (it is what the span says)
1294
+ * but never create a DatabaseServer row and never auto-match an alias.
1295
+ */
1296
+ export function getDatabaseEndpointScope(
1297
+ endpoint: DatabaseEndpoint,
1298
+ ): DatabaseEndpointScope {
1299
+ const host: string =
1300
+ endpoint && typeof endpoint.host === "string"
1301
+ ? splitDatabaseHostInstance(endpoint.host.trim().toLowerCase()).host
1302
+ : "";
1303
+
1304
+ if (!host) {
1305
+ return "local";
1306
+ }
1307
+ if (isLoopbackDatabaseHost(host) || isHostRelativeDatabaseHost(host)) {
1308
+ return "local";
1309
+ }
1310
+ if (isSingleLabelName(host) || isLinkLocalIpHost(host)) {
1311
+ return "local";
1312
+ }
1313
+
1314
+ const hasCluster: boolean =
1315
+ canonicalClusterName(endpoint.kubernetesClusterName).length > 0;
1316
+ if (!hasCluster && isClusterScopedDatabaseHost(host)) {
1317
+ return "local";
1318
+ }
1319
+
1320
+ return "global";
1321
+ }
1322
+
1323
+ /**
1324
+ * "host:port", "host" (unknown port), "[v6]:port", "host\instance"; plus
1325
+ * "@cluster" when qualified. The inverse is parseDatabaseEndpointString.
1326
+ * The cluster part is kept verbatim apart from trim + lowercase (EKS names
1327
+ * are ARNs containing ":" and "/").
1328
+ */
1329
+ export function formatDatabaseEndpoint(endpoint: DatabaseEndpoint): string {
1330
+ const value: { host: string; instance: string } = splitDatabaseHostInstance(
1331
+ endpoint && typeof endpoint.host === "string"
1332
+ ? endpoint.host.trim().toLowerCase()
1333
+ : "",
1334
+ );
1335
+ const hostPart: string = `${
1336
+ value.host.includes(":") ? `[${value.host}]` : value.host
1337
+ }${value.instance ? `\\${value.instance}` : ""}`;
1338
+ const port: number | null = toValidPort(endpoint?.port);
1339
+ const portPart: string = port !== null ? `:${port}` : "";
1340
+ const clusterName: string = canonicalClusterName(
1341
+ endpoint?.kubernetesClusterName,
1342
+ );
1343
+ return `${hostPart}${portPart}${clusterName ? `@${clusterName}` : ""}`;
1344
+ }
1345
+
1346
+ type EndpointStringProblem =
1347
+ | "empty"
1348
+ | "invalid"
1349
+ | "port-out-of-range"
1350
+ | "loopback"
1351
+ | "user-info"
1352
+ | "qualifier-needs-namespace"
1353
+ | "qualifier-not-allowed";
1354
+
1355
+ interface InterpretedEndpointString {
1356
+ endpoint: DatabaseEndpoint | null;
1357
+ problem: EndpointStringProblem | null;
1358
+ // The text before an "@", and after it, when the value had one.
1359
+ beforeAt: string;
1360
+ afterAt: string;
1361
+ // The canonical host the qualifier was refused for.
1362
+ refusedHost: string;
1363
+ }
1364
+
1365
+ /*
1366
+ * `user@host`, not `host@cluster`: the part after the "@" reads as a server
1367
+ * (it has a port or a dot, or is an IP) while the part before it does not
1368
+ * read as an address that takes a qualifier (one with a port, an IP, a
1369
+ * cluster-local or private-zone name, a `.svc` name) — `admin@10.0.0.5:5432`,
1370
+ * `john.doe@db.example.com`. Cluster names such as `prod-eu`, and ARNs
1371
+ * (which never parse as a host), never look like a server.
1372
+ */
1373
+ function looksLikeUserInfo(beforeAt: string, afterAt: string): boolean {
1374
+ if (!beforeAt) {
1375
+ return false;
1376
+ }
1377
+
1378
+ const server: ParsedHostAndPort | null = parseHostAndPort(afterAt);
1379
+ if (
1380
+ !server ||
1381
+ !(
1382
+ server.port !== null ||
1383
+ server.host.includes(".") ||
1384
+ server.host.includes(":") ||
1385
+ isIpLiteralHost(server.host)
1386
+ )
1387
+ ) {
1388
+ return false;
1389
+ }
1390
+
1391
+ // A bare word ("admin") is a user name.
1392
+ if (!NOT_A_BARE_WORD_REGEX.test(beforeAt)) {
1393
+ return true;
1394
+ }
1395
+
1396
+ const address: ParsedHostAndPort | null = parseHostAndPort(beforeAt);
1397
+ if (!address) {
1398
+ return true;
1399
+ }
1400
+ return (
1401
+ address.port === null &&
1402
+ !isIpLiteralHost(address.host) &&
1403
+ !isClusterScopedDatabaseHost(address.host) &&
1404
+ !KUBERNETES_SERVICE_DNS_REGEX.test(address.host)
1405
+ );
1406
+ }
1407
+
1408
+ function interpretDatabaseEndpointString(
1409
+ value: unknown,
1410
+ context: {
1411
+ system: string;
1412
+ kubernetesClusterName?: string | null | undefined;
1413
+ kubernetesNamespace?: string | null | undefined;
1414
+ port?: number | null | undefined;
1415
+ },
1416
+ strictPort: boolean,
1417
+ ): InterpretedEndpointString {
1418
+ const result: InterpretedEndpointString = {
1419
+ endpoint: null,
1420
+ problem: null,
1421
+ beforeAt: "",
1422
+ afterAt: "",
1423
+ refusedHost: "",
1424
+ };
1425
+
1426
+ if (typeof value !== "string" || !value.trim()) {
1427
+ result.problem = "empty";
1428
+ return result;
1429
+ }
1430
+
1431
+ const trimmed: string = value.trim();
1432
+ let addressPart: string = trimmed;
1433
+ let explicitClusterName: string = "";
1434
+
1435
+ if (!URL_SCHEME_REGEX.test(trimmed)) {
1436
+ const atIndex: number = trimmed.lastIndexOf("@");
1437
+ if (atIndex >= 0) {
1438
+ addressPart = trimmed.substring(0, atIndex);
1439
+ result.beforeAt = addressPart.trim();
1440
+ result.afterAt = trimmed.substring(atIndex + 1).trim();
1441
+ explicitClusterName = canonicalClusterName(result.afterAt);
1442
+ }
1443
+ }
1444
+
1445
+ if (
1446
+ explicitClusterName &&
1447
+ looksLikeUserInfo(result.beforeAt, result.afterAt)
1448
+ ) {
1449
+ result.problem = "user-info";
1450
+ return result;
1451
+ }
1452
+
1453
+ const hosts: Array<ParsedHostAndPort> | null = parseAddressHosts(
1454
+ addressPart,
1455
+ strictPort,
1456
+ );
1457
+ const parsed: ParsedHostAndPort | null =
1458
+ hosts && hosts.length > 0 ? hosts[0]! : null;
1459
+ if (!parsed) {
1460
+ result.problem =
1461
+ strictPort && parseHostAndPort(addressPart)
1462
+ ? "port-out-of-range"
1463
+ : "invalid";
1464
+ return result;
1465
+ }
1466
+ if (
1467
+ isLoopbackDatabaseHost(parsed.host) ||
1468
+ isHostRelativeDatabaseHost(parsed.host)
1469
+ ) {
1470
+ result.problem = "loopback";
1471
+ return result;
1472
+ }
1473
+
1474
+ const knownPort: number | null = toValidPort(context?.port) ?? parsed.port;
1475
+ const instance: string | null =
1476
+ knownPort === null && parsed.instance ? parsed.instance : null;
1477
+ const port: number | null =
1478
+ knownPort ??
1479
+ (instance ? null : getDefaultDatabasePort(context?.system)) ??
1480
+ null;
1481
+
1482
+ const rowClusterName: string = canonicalClusterName(
1483
+ context?.kubernetesClusterName,
1484
+ );
1485
+
1486
+ /*
1487
+ * An explicit "@cluster" says the name was seen from inside that
1488
+ * cluster, so it is read the way a pod there reads it.
1489
+ */
1490
+ const runsInKubernetes: boolean =
1491
+ explicitClusterName.length > 0 ||
1492
+ rowClusterName.length > 0 ||
1493
+ isNonBlankText(context?.kubernetesNamespace);
1494
+
1495
+ const host: string = applyKubernetesDnsRules(
1496
+ parsed.host,
1497
+ context?.kubernetesNamespace,
1498
+ runsInKubernetes,
1499
+ );
1500
+
1501
+ const clusterScoped: boolean = isClusterScopedDatabaseHost(host);
1502
+
1503
+ if (explicitClusterName && !clusterScoped) {
1504
+ result.problem = isSingleLabelName(host)
1505
+ ? "qualifier-needs-namespace"
1506
+ : "qualifier-not-allowed";
1507
+ result.refusedHost = host;
1508
+ return result;
1509
+ }
1510
+
1511
+ result.endpoint = buildEndpoint(
1512
+ joinHostInstance(host, instance),
1513
+ port,
1514
+ clusterScoped ? explicitClusterName || rowClusterName : "",
1515
+ );
1516
+ return result;
1517
+ }
1518
+
1519
+ /**
1520
+ * Inverse of formatDatabaseEndpoint, for stored values AND user-typed
1521
+ * aliases. The cluster qualifier is split on the LAST "@" and is only valid
1522
+ * on a host that resolves inside one cluster or network
1523
+ * (isClusterScopedDatabaseHost, after the Kubernetes DNS rules): anything
1524
+ * else with an "@" — `admin@10.0.0.5:5432`, `db.example.com@prod` — is
1525
+ * null, never a silently dropped qualifier or a user name read as a host.
1526
+ * URL forms are not split: their "@" is userinfo. Kubernetes DNS rules use
1527
+ * the row's namespace for a single-label name, and read a two-label name as
1528
+ * `<service>.<namespace>` when the row (or the qualifier) is a Kubernetes
1529
+ * one; an unqualified cluster-scoped host takes the row's cluster, and the
1530
+ * port defaults from the engine. Loopback and host-relative names are null —
1531
+ * an alias must name a server.
1532
+ */
1533
+ export function parseDatabaseEndpointString(
1534
+ value: unknown,
1535
+ context: {
1536
+ system: string;
1537
+ kubernetesClusterName?: string | null | undefined;
1538
+ kubernetesNamespace?: string | null | undefined;
1539
+ },
1540
+ ): DatabaseEndpoint | null {
1541
+ return interpretDatabaseEndpointString(value, context, false).endpoint;
1542
+ }
1543
+
1544
+ export interface ManualDatabaseEndpoint {
1545
+ // The canonical endpoint, or null when the value was rejected.
1546
+ endpoint: DatabaseEndpoint | null;
1547
+ // Why it was rejected, worded for the person who typed it; null when accepted.
1548
+ error: string | null;
1549
+ /*
1550
+ * Set when the accepted endpoint only resolves inside one Kubernetes
1551
+ * cluster or private network and names no cluster (LOCAL scope):
1552
+ * applications whose telemetry reports `k8s.cluster.name` are keyed
1553
+ * `<endpoint>@<cluster>` instead and will not match it. Advice — the
1554
+ * caller decides whether to show it or to refuse the value.
1555
+ */
1556
+ clusterQualifierHint: string | null;
1557
+ }
1558
+
1559
+ function echo(value: string): string {
1560
+ return value.length > MAX_ECHOED_VALUE_LENGTH
1561
+ ? `${value.substring(0, MAX_ECHOED_VALUE_LENGTH)}…`
1562
+ : value;
1563
+ }
1564
+
1565
+ const CLUSTER_QUALIFIER_EXAMPLE: string =
1566
+ "pg.shop.svc.cluster.local:5432@prod-eu";
1567
+
1568
+ /**
1569
+ * The endpoint a person typed (the create form of a manually added
1570
+ * database, an alias on its Endpoints tab), with a message that says what
1571
+ * to change when it cannot be used:
1572
+ *
1573
+ * - `user@host` (`admin@10.0.0.5:5432`) is refused — the user name does
1574
+ * not belong in an endpoint;
1575
+ * - `@<cluster>` names the Kubernetes cluster of an address that only
1576
+ * resolves inside one (a cluster-local name, a `<service>.<namespace>`
1577
+ * short name, a private-zone name, a private IP) and is refused on
1578
+ * anything else;
1579
+ * - loopback and host-relative names are refused;
1580
+ * - `port` (the form's own port field) replaces any port in the address,
1581
+ * and must be 1..65535, as must a port typed in the address.
1582
+ *
1583
+ * Accepted values are canonicalized exactly like parseDatabaseEndpointString.
1584
+ * `knownClusterNames` (the project's Kubernetes cluster names) are offered
1585
+ * in the hint for an unqualified cluster-scoped endpoint.
1586
+ */
1587
+ export function parseManualDatabaseEndpoint(
1588
+ value: unknown,
1589
+ context: {
1590
+ system: string;
1591
+ port?: unknown;
1592
+ kubernetesClusterName?: string | null | undefined;
1593
+ kubernetesNamespace?: string | null | undefined;
1594
+ knownClusterNames?: Array<string> | null | undefined;
1595
+ },
1596
+ ): ManualDatabaseEndpoint {
1597
+ const rejected: (error: string) => ManualDatabaseEndpoint = (
1598
+ error: string,
1599
+ ): ManualDatabaseEndpoint => {
1600
+ return { endpoint: null, error: error, clusterQualifierHint: null };
1601
+ };
1602
+
1603
+ const typed: string = typeof value === "string" ? value.trim() : "";
1604
+
1605
+ const hasPortField: boolean =
1606
+ context?.port !== undefined &&
1607
+ context?.port !== null &&
1608
+ !(typeof context.port === "string" && context.port.trim() === "");
1609
+ let portField: number | null = null;
1610
+ if (hasPortField) {
1611
+ portField = toValidPort(context.port);
1612
+ if (portField === null) {
1613
+ return rejected(
1614
+ "Server port must be a whole number between 1 and 65535.",
1615
+ );
1616
+ }
1617
+ }
1618
+
1619
+ const interpreted: InterpretedEndpointString =
1620
+ interpretDatabaseEndpointString(
1621
+ value,
1622
+ {
1623
+ system: context?.system || "",
1624
+ kubernetesClusterName: context?.kubernetesClusterName,
1625
+ kubernetesNamespace: context?.kubernetesNamespace,
1626
+ port: portField,
1627
+ },
1628
+ !hasPortField,
1629
+ );
1630
+
1631
+ switch (interpreted.problem) {
1632
+ case "empty":
1633
+ return rejected(
1634
+ "Server address is required. Enter the host name or IP address applications use to reach this database, for example orders-db.example.com:5432.",
1635
+ );
1636
+ case "user-info":
1637
+ return rejected(
1638
+ `"${echo(typed)}" looks like user@host. Remove "${echo(
1639
+ interpreted.beforeAt,
1640
+ )}@": the database user does not belong in an endpoint. "@" is only used after an address that resolves inside one Kubernetes cluster, to name that cluster, for example ${CLUSTER_QUALIFIER_EXAMPLE}.`,
1641
+ );
1642
+ case "qualifier-needs-namespace":
1643
+ return rejected(
1644
+ `"${echo(interpreted.refusedHost)}" is a single-label name, which only means one Service once its namespace is known. Enter it with the namespace, for example ${echo(
1645
+ interpreted.refusedHost,
1646
+ )}.<namespace>@${echo(interpreted.afterAt)}.`,
1647
+ );
1648
+ case "qualifier-not-allowed":
1649
+ return rejected(
1650
+ `"${echo(interpreted.refusedHost)}" resolves the same way everywhere, so it cannot take a Kubernetes cluster qualifier. Remove "@${echo(
1651
+ interpreted.afterAt,
1652
+ )}". A cluster is only named for an address that resolves inside one cluster or private network: a cluster-local name such as pg.shop.svc.cluster.local, a <service>.<namespace> short name, a .internal or .local name, or a private IP address.`,
1653
+ );
1654
+ case "loopback":
1655
+ return rejected(
1656
+ `"${echo(typed)}" is a loopback or host-local address, such as localhost or host.docker.internal. Every application reaches its own, so it cannot identify one database. Enter the host name or IP address the database is reached at from other machines.`,
1657
+ );
1658
+ case "port-out-of-range":
1659
+ return rejected(
1660
+ `"${echo(typed)}" has a port outside 1-65535. Enter a port between 1 and 65535.`,
1661
+ );
1662
+ case "invalid":
1663
+ return rejected(
1664
+ `"${echo(typed)}" is not a valid host[:port] endpoint. Enter a host name or IP address with an optional port, for example orders-db.example.com:5432. A SQL Server named instance is written host\\instance, and an address that resolves inside one Kubernetes cluster can name that cluster: ${CLUSTER_QUALIFIER_EXAMPLE}.`,
1665
+ );
1666
+ default:
1667
+ break;
1668
+ }
1669
+
1670
+ const endpoint: DatabaseEndpoint | null = interpreted.endpoint;
1671
+ if (!endpoint) {
1672
+ return rejected(`"${echo(typed)}" is not a valid host[:port] endpoint.`);
1673
+ }
1674
+
1675
+ let clusterQualifierHint: string | null = null;
1676
+ if (
1677
+ !endpoint.kubernetesClusterName &&
1678
+ isClusterScopedDatabaseHost(endpoint.host)
1679
+ ) {
1680
+ const formatted: string = formatDatabaseEndpoint(endpoint);
1681
+ const clusters: Array<string> = Array.from(
1682
+ new Set<string>(
1683
+ (Array.isArray(context?.knownClusterNames)
1684
+ ? context.knownClusterNames
1685
+ : []
1686
+ )
1687
+ .map(canonicalClusterName)
1688
+ .filter((name: string): boolean => {
1689
+ return name.length > 0;
1690
+ }),
1691
+ ),
1692
+ );
1693
+ clusterQualifierHint = `${formatted} only resolves inside one Kubernetes cluster or private network. Applications that report their cluster (k8s.cluster.name) are matched to ${formatted}@<cluster name> instead, so name the cluster the way they report it${
1694
+ clusters.length > 0
1695
+ ? `, for example ${formatted}@${clusters[0]} (this project's clusters: ${clusters.join(", ")})`
1696
+ : ""
1697
+ }.`;
1698
+ }
1699
+
1700
+ return {
1701
+ endpoint: endpoint,
1702
+ error: null,
1703
+ clusterQualifierHint: clusterQualifierHint,
1704
+ };
1705
+ }
1706
+
1707
+ // ---- managed cluster members ---------------------------------------------
1708
+
1709
+ /**
1710
+ * The cluster a managed-service MEMBER host belongs to, as a host name, or
1711
+ * null for anything else. Drivers of replicated databases report the member
1712
+ * that served each call, so without this every member of one cluster would
1713
+ * become a database of its own. Today: MongoDB Atlas —
1714
+ * `cluster0-shard-00-01.ab1cd.mongodb.net` → `cluster0.ab1cd.mongodb.net`
1715
+ * (the cluster's SRV name). A `\instance` or a port plays no part.
1716
+ */
1717
+ export function getDatabaseClusterHost(host: string): string | null {
1718
+ if (typeof host !== "string") {
1719
+ return null;
1720
+ }
1721
+ const value: string = splitDatabaseHostInstance(
1722
+ host.trim().toLowerCase(),
1723
+ ).host;
1724
+ const labels: Array<string> = value.split(".");
1725
+ if (
1726
+ labels.length !== 4 ||
1727
+ `${labels[2]}.${labels[3]}` !== ATLAS_DOMAIN ||
1728
+ !DNS_LABEL_REGEX.test(labels[1]!)
1729
+ ) {
1730
+ return null;
1731
+ }
1732
+ const member: string = labels[0]!;
1733
+ const marker: number = member.lastIndexOf(ATLAS_MEMBER_MARKER);
1734
+ if (marker <= 0) {
1735
+ return null;
1736
+ }
1737
+ if (
1738
+ !ATLAS_MEMBER_NUMBER_REGEX.test(
1739
+ member.substring(marker + ATLAS_MEMBER_MARKER.length),
1740
+ )
1741
+ ) {
1742
+ return null;
1743
+ }
1744
+ const cluster: string = member.substring(0, marker);
1745
+ return DNS_LABEL_REGEX.test(cluster)
1746
+ ? `${cluster}.${labels[1]}.${ATLAS_DOMAIN}`
1747
+ : null;
1748
+ }
1749
+
1750
+ // ---- row identity + naming -----------------------------------------------
1751
+
1752
+ /*
1753
+ * Row identifiers start with the engine FAMILY (getDatabaseSystemFamily),
1754
+ * never the engine: a fork keys like the engine it forks ("mariadb" as
1755
+ * "mysql", "valkey" as "redis"). Which of the two a database is gets
1756
+ * decided — and corrected — over its lifetime (a span says "mysql", the
1757
+ * image then says "mariadb", a version string refines "mysql" to "tidb"),
1758
+ * and that must never re-key the row or mint a second one. Engines of
1759
+ * different families still key apart. An unknown engine is its own family.
1760
+ */
1761
+ function identifierEngine(system: unknown): string {
1762
+ return getDatabaseSystemFamily(system) || "";
1763
+ }
1764
+
1765
+ /**
1766
+ * `${family}|${formatted endpoint}` — the identifier of an endpoint row,
1767
+ * keyed on the engine family (see identifierEngine).
1768
+ */
1769
+ export function buildDatabaseServerIdentifier(
1770
+ system: string,
1771
+ endpoint: DatabaseEndpoint,
1772
+ ): string {
1773
+ return `${identifierEngine(system)}|${formatDatabaseEndpoint(endpoint)}`;
1774
+ }
1775
+
1776
+ /**
1777
+ * The identifier of a workload-detected row, lowercased and keyed on the
1778
+ * engine family (see identifierEngine), so a workload re-classified within
1779
+ * its family (Redis → Valkey, MySQL → MariaDB) keeps its identifier:
1780
+ * kubernetes → `${family}|kubernetes:${cluster}/${namespace}/${kind}/${name}`
1781
+ * docker / podman → `${family}|${platform}:${host}/${name}` — no kind, so
1782
+ * compose labels that appear later cannot change the identity.
1783
+ */
1784
+ export function buildWorkloadDatabaseServerIdentifier(input: {
1785
+ system: string;
1786
+ platform: DatabaseWorkloadPlatform;
1787
+ parentName: string;
1788
+ namespace?: string | null | undefined;
1789
+ workloadKind?: string | null | undefined;
1790
+ workloadName: string;
1791
+ }): string {
1792
+ const segment: (value: unknown) => string = (value: unknown): string => {
1793
+ return typeof value === "string" ? value.trim() : "";
1794
+ };
1795
+
1796
+ const system: string = identifierEngine(input.system);
1797
+ const platform: string = segment(input.platform);
1798
+ const parentName: string = segment(input.parentName);
1799
+ const workloadName: string = segment(input.workloadName);
1800
+
1801
+ if (platform === "kubernetes") {
1802
+ return `${system}|kubernetes:${parentName}/${segment(
1803
+ input.namespace,
1804
+ )}/${segment(input.workloadKind)}/${workloadName}`.toLowerCase();
1805
+ }
1806
+
1807
+ return `${system}|${platform}:${parentName}/${workloadName}`.toLowerCase();
1808
+ }
1809
+
1810
+ /**
1811
+ * "PostgreSQL db.prod:5432", "Redis cache/redis-master", "MongoDB mongo" —
1812
+ * the engine then the endpoint (no "@cluster") or the workload. At most 100
1813
+ * characters.
1814
+ */
1815
+ export function buildDatabaseServerDisplayName(input: {
1816
+ system: string;
1817
+ endpoint?: DatabaseEndpoint | null | undefined;
1818
+ namespace?: string | null | undefined;
1819
+ workloadName?: string | null | undefined;
1820
+ }): string {
1821
+ const engine: string = getDatabaseSystemDisplayName(input?.system);
1822
+
1823
+ let target: string = "";
1824
+ if (input?.endpoint && typeof input.endpoint.host === "string") {
1825
+ target = formatDatabaseEndpoint({
1826
+ host: input.endpoint.host,
1827
+ port: input.endpoint.port,
1828
+ });
1829
+ } else {
1830
+ const workloadName: string =
1831
+ typeof input?.workloadName === "string" ? input.workloadName.trim() : "";
1832
+ const namespace: string =
1833
+ typeof input?.namespace === "string" ? input.namespace.trim() : "";
1834
+ if (workloadName) {
1835
+ target = namespace ? `${namespace}/${workloadName}` : workloadName;
1836
+ }
1837
+ }
1838
+
1839
+ const name: string = target ? `${engine} ${target}` : engine;
1840
+ return name.length > DISPLAY_NAME_MAX_LENGTH
1841
+ ? name.substring(0, DISPLAY_NAME_MAX_LENGTH).trimEnd()
1842
+ : name;
1843
+ }
1844
+
1845
+ /**
1846
+ * The endpoint aliases of a Kubernetes-detected database, formatted:
1847
+ * - each Service × port: `${svc}.${ns}.svc.cluster.local:${port}@${cluster}`;
1848
+ * - each StatefulSet / operator member behind a headless Service:
1849
+ * `${pod}.${svc}.${ns}.svc.cluster.local:${port}@${cluster}`;
1850
+ * - the unqualified twin of each, ONLY when `includeUnqualified` (the
1851
+ * worker sets it when the project has exactly one Kubernetes cluster, so
1852
+ * an unqualified name cannot belong to anything else).
1853
+ * Ports are the engine default plus the declared container ports. No
1854
+ * two-label `${svc}.${ns}` forms are needed: a pod calling `svc.ns` is
1855
+ * canonicalized to the Service FQDN (applyKubernetesDnsRules), which is
1856
+ * already here. Invalid DNS labels are skipped. Deduped, in a stable order.
1857
+ * With a blank cluster name only the unqualified form can be produced, so
1858
+ * it is.
1859
+ */
1860
+ export function buildKubernetesDatabaseAliases(input: {
1861
+ system: string;
1862
+ namespace: string;
1863
+ clusterName: string;
1864
+ serviceNames: Array<string>;
1865
+ podServiceNames?:
1866
+ | Array<{ podName: string; serviceName: string }>
1867
+ | null
1868
+ | undefined;
1869
+ ports: Array<number>;
1870
+ includeUnqualified: boolean;
1871
+ }): Array<string> {
1872
+ if (!input) {
1873
+ return [];
1874
+ }
1875
+
1876
+ const namespace: string | null = canonicalDnsLabel(input.namespace);
1877
+ if (!namespace) {
1878
+ return [];
1879
+ }
1880
+
1881
+ const clusterName: string = canonicalClusterName(input.clusterName);
1882
+
1883
+ const ports: Array<number | null> = [];
1884
+ const candidatePorts: Array<unknown> = [
1885
+ getDefaultDatabasePort(input.system),
1886
+ ...(Array.isArray(input.ports) ? input.ports : []),
1887
+ ];
1888
+ for (const candidate of candidatePorts) {
1889
+ const port: number | null = toValidPort(candidate);
1890
+ if (port !== null && !ports.includes(port)) {
1891
+ ports.push(port);
1892
+ }
1893
+ }
1894
+ if (ports.length === 0) {
1895
+ ports.push(null);
1896
+ }
1897
+
1898
+ const aliases: Array<string> = [];
1899
+ const seen: Set<string> = new Set<string>();
1900
+ const push: (alias: string) => void = (alias: string): void => {
1901
+ if (!seen.has(alias)) {
1902
+ seen.add(alias);
1903
+ aliases.push(alias);
1904
+ }
1905
+ };
1906
+
1907
+ const addHost: (host: string) => void = (host: string): void => {
1908
+ if (!isValidHostname(host)) {
1909
+ return;
1910
+ }
1911
+ for (const port of ports) {
1912
+ if (clusterName) {
1913
+ push(formatDatabaseEndpoint(buildEndpoint(host, port, clusterName)));
1914
+ }
1915
+ if (!clusterName || input.includeUnqualified) {
1916
+ push(formatDatabaseEndpoint(buildEndpoint(host, port, "")));
1917
+ }
1918
+ }
1919
+ };
1920
+
1921
+ for (const serviceName of Array.isArray(input.serviceNames)
1922
+ ? input.serviceNames
1923
+ : []) {
1924
+ const service: string | null = canonicalDnsLabel(serviceName);
1925
+ if (service) {
1926
+ addHost(`${service}.${namespace}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`);
1927
+ }
1928
+ }
1929
+
1930
+ for (const member of Array.isArray(input.podServiceNames)
1931
+ ? input.podServiceNames
1932
+ : []) {
1933
+ const podName: string | null = canonicalDnsLabel(member?.podName);
1934
+ const serviceName: string | null = canonicalDnsLabel(member?.serviceName);
1935
+ if (podName && serviceName) {
1936
+ addHost(
1937
+ `${podName}.${serviceName}.${namespace}${KUBERNETES_CLUSTER_DOMAIN_SUFFIX}`,
1938
+ );
1939
+ }
1940
+ }
1941
+
1942
+ return aliases;
1943
+ }