@oneuptime/common 12.0.9 → 12.0.11

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 (262) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Models/DatabaseModels/RunnerJob.ts +17 -2
  4. package/Server/API/LlmProviderAPI.ts +8 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
  8. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  9. package/Server/Services/NetworkDeviceService.ts +265 -12
  10. package/Server/Services/RunnerJobService.ts +24 -1
  11. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  12. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  13. package/Server/Utils/LLM/LLMService.ts +108 -19
  14. package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
  15. package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
  16. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
  17. package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
  18. package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
  19. package/Server/Utils/SSRFProtection.ts +5 -4
  20. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  21. package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
  22. package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
  23. package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
  24. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  25. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
  26. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  27. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  28. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  29. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  30. package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
  31. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
  32. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  33. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  34. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  35. package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
  36. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  37. package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
  38. package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
  39. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  40. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  41. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
  42. package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
  43. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
  44. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
  45. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
  46. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
  47. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
  48. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  49. package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
  50. package/Tests/Types/API/URLTelScheme.test.ts +261 -0
  51. package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
  52. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  53. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  54. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  55. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
  56. package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
  57. package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
  58. package/Tests/UI/Components/Button.test.tsx +279 -0
  59. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  60. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  61. package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
  62. package/Tests/UI/Components/Icon.test.tsx +57 -0
  63. package/Tests/UI/Components/List.test.tsx +20 -3
  64. package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
  65. package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
  66. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  67. package/Tests/UI/Components/Pagination.test.tsx +721 -146
  68. package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
  69. package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
  70. package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
  71. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
  72. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
  73. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
  74. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
  75. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
  76. package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
  77. package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  79. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  80. package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
  81. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  82. package/Types/API/Protocol.ts +9 -0
  83. package/Types/API/URL.ts +286 -20
  84. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  85. package/Types/CustomField/CustomFieldType.ts +28 -0
  86. package/Types/Monitor/MonitorType.ts +11 -0
  87. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  88. package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
  89. package/Types/Runbook/RunbookStepType.ts +23 -0
  90. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
  91. package/UI/Components/Button/Button.tsx +14 -22
  92. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  93. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  94. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  95. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  96. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  97. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  98. package/UI/Components/Dictionary/Dictionary.tsx +4 -39
  99. package/UI/Components/Icon/Icon.tsx +1 -1
  100. package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
  101. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
  102. package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
  103. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
  104. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
  105. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  106. package/UI/Components/Pagination/Pagination.tsx +373 -328
  107. package/UI/Components/Pagination/PaginationUtil.ts +263 -0
  108. package/UI/Components/Pagination/Screenshots/README.md +15 -0
  109. package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
  110. package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
  111. package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
  112. package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
  113. package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
  114. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
  115. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
  116. package/UI/Components/TextArea/TextArea.tsx +3 -0
  117. package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
  118. package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
  119. package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
  120. package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
  121. package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
  122. package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
  123. package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
  124. package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
  125. package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
  126. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
  127. package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
  128. package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
  129. package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
  130. package/Utils/API.ts +243 -36
  131. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  132. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  133. package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
  134. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  135. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  137. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
  139. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  140. package/build/dist/Server/API/LlmProviderAPI.js +9 -1
  141. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  146. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
  147. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  148. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  149. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  150. package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
  151. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  152. package/build/dist/Server/Services/RunnerJobService.js +20 -2
  153. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  154. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  155. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  156. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  157. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  158. package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
  159. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  160. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
  161. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  162. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
  163. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  164. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
  165. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  166. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
  167. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  168. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
  169. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
  170. package/build/dist/Server/Utils/SSRFProtection.js +5 -4
  171. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  172. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  173. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  174. package/build/dist/Types/API/Protocol.js +9 -0
  175. package/build/dist/Types/API/Protocol.js.map +1 -1
  176. package/build/dist/Types/API/URL.js +241 -18
  177. package/build/dist/Types/API/URL.js.map +1 -1
  178. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  179. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  180. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  181. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  182. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  183. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  184. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
  185. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
  186. package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
  187. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  188. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  189. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  190. package/build/dist/UI/Components/Button/Button.js +10 -16
  191. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  193. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  195. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  196. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  197. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  198. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  199. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  200. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  201. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  202. package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
  203. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  204. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  205. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  206. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
  207. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  208. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
  209. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  210. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
  211. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  212. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  213. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  214. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
  215. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  216. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  217. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  218. package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
  219. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  220. package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
  221. package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
  222. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
  226. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  227. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
  229. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
  231. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
  232. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
  233. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
  234. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
  235. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
  236. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
  237. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
  238. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
  239. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
  240. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
  241. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
  242. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
  243. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
  244. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
  245. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
  246. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
  247. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
  248. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
  249. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
  250. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
  251. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  252. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
  253. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
  254. package/build/dist/Utils/API.js +175 -34
  255. package/build/dist/Utils/API.js.map +1 -1
  256. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  257. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  258. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  259. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  260. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
  261. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
  262. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ import Protocol from "../../../Types/API/Protocol";
2
+ import URL from "../../../Types/API/URL";
3
+ import BadDataException from "../../../Types/Exception/BadDataException";
4
+ import { describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * A status page went blank because ONE footer link, saved years earlier by a
8
+ * looser validator as "https://tel:3136361710/", no longer parsed. The value
9
+ * was hydrated by the column transformer during findBy, so the throw escaped
10
+ * the query and 400'd the whole /master-page config endpoint — the page never
11
+ * got its config and sat on "Loading..." forever.
12
+ *
13
+ * The rule these tests pin: reads tolerate a value that no longer validates,
14
+ * writes never accept one. A single bad legacy row may degrade itself and
15
+ * nothing else.
16
+ */
17
+
18
+ interface Transformer {
19
+ to: (value: unknown) => unknown;
20
+ from: (value: unknown) => unknown;
21
+ }
22
+
23
+ const transformer: Transformer = URL.getDatabaseTransformer() as Transformer;
24
+
25
+ /*
26
+ * Values the old character-allowlist Hostname accepted and the current
27
+ * structural one does not. Every one of these can already be sitting in a
28
+ * customer's database.
29
+ */
30
+ const legacyUnparseableValues: Array<string> = [
31
+ // The value that actually took the status page down.
32
+ "https://tel:3136361710/",
33
+ // Ports that are not ports.
34
+ "https://host:99999999/",
35
+ "https://example.com:notaport/",
36
+ // Hosts that are not hosts.
37
+ "https://-leading-dash.example.com/",
38
+ "https://trailing-dash-.example.com/",
39
+ "https:// /",
40
+ "https://exa mple.com/",
41
+ ];
42
+
43
+ describe("URL — reads tolerate a value that no longer parses", () => {
44
+ test.each(legacyUnparseableValues)(
45
+ "fromStringLenient(%s) does not throw",
46
+ (raw: string) => {
47
+ expect(() => {
48
+ return URL.fromStringLenient(raw);
49
+ }).not.toThrow();
50
+ },
51
+ );
52
+
53
+ test.each(legacyUnparseableValues)(
54
+ "fromStringLenient(%s) flags the value as malformed",
55
+ (raw: string) => {
56
+ expect(URL.fromStringLenient(raw).isMalformed()).toBe(true);
57
+ },
58
+ );
59
+
60
+ test.each(legacyUnparseableValues)(
61
+ "fromStringLenient(%s) preserves the value verbatim",
62
+ (raw: string) => {
63
+ // The link still renders as exactly what it always was.
64
+ expect(URL.fromStringLenient(raw).toString()).toBe(raw);
65
+ },
66
+ );
67
+
68
+ test.each(legacyUnparseableValues)(
69
+ "the column transformer reads %s without throwing",
70
+ (raw: string) => {
71
+ let result: URL | null = null;
72
+
73
+ expect(() => {
74
+ result = transformer.from(raw) as URL;
75
+ }).not.toThrow();
76
+
77
+ expect(result).toBeInstanceOf(URL);
78
+ expect((result as unknown as URL).toString()).toBe(raw);
79
+ },
80
+ );
81
+
82
+ test("fromString stays strict — leniency is opt-in, on read paths only", () => {
83
+ for (const raw of legacyUnparseableValues) {
84
+ expect(() => {
85
+ return URL.fromString(raw);
86
+ }).toThrow(BadDataException);
87
+ }
88
+ });
89
+ });
90
+
91
+ describe("URL — a malformed value exposes no host", () => {
92
+ /*
93
+ * The security property that makes leniency safe: nothing can read a
94
+ * hostname off a malformed URL and turn it into a request target. The
95
+ * outbound guards (SSRFProtection) re-parse the raw string with the WHATWG
96
+ * parser and never trusted this type's parse anyway.
97
+ */
98
+ test.each(legacyUnparseableValues)(
99
+ "%s yields no hostname and no opaque value",
100
+ (raw: string) => {
101
+ const url: URL = URL.fromStringLenient(raw);
102
+
103
+ expect(url.hostname).toBeUndefined();
104
+ expect(url.opaqueValue).toBe("");
105
+ },
106
+ );
107
+
108
+ test("a malformed URL reports the https default protocol", () => {
109
+ expect(URL.fromStringLenient("https://tel:3136361710/").protocol).toBe(
110
+ Protocol.HTTPS,
111
+ );
112
+ });
113
+
114
+ test("removeQueryString on a malformed URL does not throw", () => {
115
+ const url: URL = URL.fromStringLenient("https://tel:3136361710/?a=1");
116
+
117
+ expect(() => {
118
+ return url.removeQueryString();
119
+ }).not.toThrow();
120
+ expect(url.removeQueryString().isMalformed()).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe("URL — writes stay strict", () => {
125
+ test.each(legacyUnparseableValues)(
126
+ "the column transformer refuses to write back %s",
127
+ (raw: string) => {
128
+ const malformed: URL = URL.fromStringLenient(raw);
129
+
130
+ expect(() => {
131
+ return transformer.to(malformed);
132
+ }).toThrow(BadDataException);
133
+ },
134
+ );
135
+
136
+ test("a client cannot invent a malformed URL and have it persisted", () => {
137
+ /*
138
+ * fromJSON is lenient so the browser can render a legacy link — but the
139
+ * value it produces is still refused on the way back into the database.
140
+ */
141
+ const fromClient: URL = URL.fromJSON({
142
+ _type: "URL",
143
+ value: "https://tel:3136361710/",
144
+ });
145
+
146
+ expect(fromClient.isMalformed()).toBe(true);
147
+ expect(() => {
148
+ return transformer.to(fromClient);
149
+ }).toThrow(BadDataException);
150
+ });
151
+
152
+ test("a raw malformed string is refused on write too", () => {
153
+ expect(() => {
154
+ return transformer.to("https://tel:3136361710/");
155
+ }).toThrow(BadDataException);
156
+ });
157
+
158
+ test("the write error names the offending value", () => {
159
+ expect(() => {
160
+ return transformer.to(URL.fromStringLenient("https://tel:3136361710/"));
161
+ }).toThrow("URL https://tel:3136361710/ is not in valid format.");
162
+ });
163
+
164
+ test("valid URLs still write normally", () => {
165
+ expect(transformer.to(URL.fromString("https://example.com/privacy"))).toBe(
166
+ "https://example.com/privacy",
167
+ );
168
+ expect(transformer.to(URL.fromString("tel:+13136361710"))).toBe(
169
+ "tel:+13136361710",
170
+ );
171
+ });
172
+ });
173
+
174
+ describe("URL — leniency does not touch valid values", () => {
175
+ const validValues: Array<string> = [
176
+ "https://example.com/privacy",
177
+ "https://docs.voice.izt.cloud/",
178
+ "http://localhost:5000/api/test",
179
+ "mailto:support@voice.izt.cloud",
180
+ "tel:+13136361710",
181
+ "sms:+15555550123",
182
+ "wss://localhost:5000/api/test",
183
+ ];
184
+
185
+ test.each(validValues)("%s is not flagged malformed", (raw: string) => {
186
+ expect(URL.fromStringLenient(raw).isMalformed()).toBe(false);
187
+ expect((transformer.from(raw) as URL).isMalformed()).toBe(false);
188
+ });
189
+
190
+ test.each(validValues)("%s parses identically either way", (raw: string) => {
191
+ expect(URL.fromStringLenient(raw).toString()).toBe(
192
+ URL.fromString(raw).toString(),
193
+ );
194
+ });
195
+
196
+ test("an empty stored value is still null, not a malformed URL", () => {
197
+ expect(transformer.from("")).toBeNull();
198
+ expect(transformer.from(null)).toBeNull();
199
+ });
200
+
201
+ test("undefined is still passed through untouched on write", () => {
202
+ // A column the caller never set must stay undefined so its DEFAULT applies.
203
+ expect(transformer.to(undefined)).toBeUndefined();
204
+ });
205
+ });
206
+
207
+ /*
208
+ * The end-to-end shape of the outage: a status page's footer links, one of
209
+ * which is the bad legacy row. Reading the set must yield every link.
210
+ */
211
+ describe("URL — one bad link no longer takes down the page that lists it", () => {
212
+ const storedFooterLinks: Array<{ title: string; link: string }> = [
213
+ { title: "Phone", link: "https://tel:3136361710/" },
214
+ { title: "Email", link: "mailto:support@voice.izt.cloud" },
215
+ { title: "Docs", link: "https://docs.voice.izt.cloud/" },
216
+ { title: "Portal", link: "https://portal.voice.izt.cloud/" },
217
+ ];
218
+
219
+ test("hydrating the whole set does not throw", () => {
220
+ expect(() => {
221
+ return storedFooterLinks.map((row: { link: string }) => {
222
+ return transformer.from(row.link);
223
+ });
224
+ }).not.toThrow();
225
+ });
226
+
227
+ test("every link survives, including the malformed one", () => {
228
+ const hydrated: Array<URL> = storedFooterLinks.map(
229
+ (row: { link: string }) => {
230
+ return transformer.from(row.link) as URL;
231
+ },
232
+ );
233
+
234
+ expect(hydrated).toHaveLength(4);
235
+ expect(
236
+ hydrated.map((url: URL) => {
237
+ return url.toString();
238
+ }),
239
+ ).toEqual([
240
+ "https://tel:3136361710/",
241
+ "mailto:support@voice.izt.cloud",
242
+ "https://docs.voice.izt.cloud/",
243
+ "https://portal.voice.izt.cloud/",
244
+ ]);
245
+ });
246
+
247
+ test("only the bad row is degraded", () => {
248
+ const hydrated: Array<URL> = storedFooterLinks.map(
249
+ (row: { link: string }) => {
250
+ return transformer.from(row.link) as URL;
251
+ },
252
+ );
253
+
254
+ expect(
255
+ hydrated.map((url: URL) => {
256
+ return url.isMalformed();
257
+ }),
258
+ ).toEqual([true, false, false, false]);
259
+ });
260
+
261
+ test("the whole set still serialises to JSON for the browser", () => {
262
+ expect(() => {
263
+ return storedFooterLinks.map((row: { link: string }) => {
264
+ return (transformer.from(row.link) as URL).toJSON();
265
+ });
266
+ }).not.toThrow();
267
+ });
268
+
269
+ /*
270
+ * Had the fix stopped at the server, the 400 would simply have become a
271
+ * client-side crash when the browser revived the same value.
272
+ */
273
+ test("and the browser can revive every one of them", () => {
274
+ const revived: Array<URL> = storedFooterLinks.map(
275
+ (row: { link: string }) => {
276
+ return URL.fromJSON((transformer.from(row.link) as URL).toJSON());
277
+ },
278
+ );
279
+
280
+ expect(
281
+ revived.map((url: URL) => {
282
+ return url.toString();
283
+ }),
284
+ ).toEqual([
285
+ "https://tel:3136361710/",
286
+ "mailto:support@voice.izt.cloud",
287
+ "https://docs.voice.izt.cloud/",
288
+ "https://portal.voice.izt.cloud/",
289
+ ]);
290
+ });
291
+
292
+ /*
293
+ * And the correct value going forward: the same phone link, typed as a real
294
+ * tel: URI, is valid on write and needs no leniency at all.
295
+ */
296
+ test("the same link stored correctly needs no leniency", () => {
297
+ const stored: unknown = transformer.to(URL.fromString("tel:+13136361710"));
298
+ expect(stored).toBe("tel:+13136361710");
299
+ expect((transformer.from(stored) as URL).isMalformed()).toBe(false);
300
+ });
301
+ });
@@ -0,0 +1,261 @@
1
+ import Protocol from "../../../Types/API/Protocol";
2
+ import URL from "../../../Types/API/URL";
3
+ import BadDataException from "../../../Types/Exception/BadDataException";
4
+ import { describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * "tel:" and "sms:" are opaque schemes: what follows the colon is a phone
8
+ * number, not an authority. Before they were known to the parser, a status
9
+ * page footer link of "tel:3136361710" fell through to the https default,
10
+ * was read as host "tel" + port "3136361710", and toString() normalised it
11
+ * on save into "https://tel:3136361710/". These tests pin the parse, the
12
+ * round-trip, and the rule that nothing in an opaque payload may look like an
13
+ * authority or a path.
14
+ */
15
+
16
+ describe("URL — tel: and sms: are parsed as opaque schemes", () => {
17
+ const validTelUrls: Array<[string, string]> = [
18
+ // [input, expected opaque value]
19
+ ["tel:+13136361710", "+13136361710"],
20
+ ["tel:3136361710", "3136361710"],
21
+ ["tel:+1-313-636-1710", "+1-313-636-1710"],
22
+ ["tel:+1.313.636.1710", "+1.313.636.1710"],
23
+ // RFC 3966 allows a number to open on a visual separator.
24
+ ["tel:(313)636-1710", "(313)636-1710"],
25
+ ["tel:+1(313)636-1710", "+1(313)636-1710"],
26
+ ["tel:-5550123", "-5550123"],
27
+ ["tel:911", "911"],
28
+ ["tel:+442071838750", "+442071838750"],
29
+ // RFC 3966 parameters.
30
+ ["tel:3136361710;phone-context=+1", "3136361710;phone-context=+1"],
31
+ ["tel:+13136361710;ext=42", "+13136361710;ext=42"],
32
+ ];
33
+
34
+ test.each(validTelUrls)(
35
+ "parses %s as a tel: URL with no hostname",
36
+ (input: string, expectedValue: string) => {
37
+ const url: URL = URL.fromString(input);
38
+
39
+ expect(url.protocol).toBe(Protocol.TEL);
40
+ expect(url.opaqueValue).toBe(expectedValue);
41
+ expect(url.isMalformed()).toBe(false);
42
+ // An opaque URL has no authority — nothing can read a host off it.
43
+ expect(url.hostname).toBeUndefined();
44
+ },
45
+ );
46
+
47
+ test.each(validTelUrls)(
48
+ "round-trips %s unchanged through toString()",
49
+ (input: string) => {
50
+ expect(URL.fromString(input).toString()).toBe(input);
51
+ },
52
+ );
53
+
54
+ test("parses sms: the same way", () => {
55
+ const url: URL = URL.fromString("sms:+15555550123");
56
+
57
+ expect(url.protocol).toBe(Protocol.SMS);
58
+ expect(url.opaqueValue).toBe("+15555550123");
59
+ expect(url.toString()).toBe("sms:+15555550123");
60
+ });
61
+
62
+ test("sms: accepts several comma-separated recipients", () => {
63
+ const url: URL = URL.fromString("sms:+15555550123,+15555550124");
64
+
65
+ expect(url.opaqueValue).toBe("+15555550123,+15555550124");
66
+ expect(url.toString()).toBe("sms:+15555550123,+15555550124");
67
+ });
68
+
69
+ test("sms: keeps a body query string", () => {
70
+ const url: URL = URL.fromString("sms:+15555550123?body=hello");
71
+
72
+ expect(url.protocol).toBe(Protocol.SMS);
73
+ expect(url.opaqueValue).toBe("+15555550123");
74
+ expect(url.getQueryParam("body")).toBe("hello");
75
+ expect(url.toString()).toBe("sms:+15555550123?body=hello");
76
+ });
77
+
78
+ test("scheme match is case-insensitive, per RFC 3986", () => {
79
+ expect(URL.fromString("TEL:+13136361710").protocol).toBe(Protocol.TEL);
80
+ expect(URL.fromString("Tel:+13136361710").opaqueValue).toBe("+13136361710");
81
+ expect(URL.fromString("SMS:+15555550123").protocol).toBe(Protocol.SMS);
82
+ });
83
+
84
+ test("isOpaqueProtocol identifies exactly tel: and sms:", () => {
85
+ expect(URL.isOpaqueProtocol(Protocol.TEL)).toBe(true);
86
+ expect(URL.isOpaqueProtocol(Protocol.SMS)).toBe(true);
87
+ expect(URL.isOpaqueProtocol(Protocol.HTTPS)).toBe(false);
88
+ expect(URL.isOpaqueProtocol(Protocol.HTTP)).toBe(false);
89
+ expect(URL.isOpaqueProtocol(Protocol.MAIL)).toBe(false);
90
+ expect(URL.isOpaqueProtocol(Protocol.WS)).toBe(false);
91
+ expect(URL.isOpaqueProtocol(Protocol.WSS)).toBe(false);
92
+ expect(URL.isOpaqueProtocol(Protocol.MONGO_DB)).toBe(false);
93
+ });
94
+
95
+ test("the constructor also takes the opaque path", () => {
96
+ const url: URL = new URL(Protocol.TEL, "+13136361710");
97
+
98
+ expect(url.opaqueValue).toBe("+13136361710");
99
+ expect(url.hostname).toBeUndefined();
100
+ expect(url.toString()).toBe("tel:+13136361710");
101
+ });
102
+
103
+ test("isHttps() is false for an opaque URL", () => {
104
+ expect(URL.fromString("tel:+13136361710").isHttps()).toBe(false);
105
+ expect(URL.fromString("sms:+15555550123").isHttps()).toBe(false);
106
+ });
107
+ });
108
+
109
+ /*
110
+ * The regression this whole change exists for: the value a customer typed
111
+ * must survive a save/read cycle instead of being rewritten into a bogus
112
+ * https URL that a later, stricter Hostname validator then refuses to read.
113
+ */
114
+ describe("URL — tel: is no longer mangled into an https URL", () => {
115
+ test("tel:3136361710 does not become https://tel:3136361710/", () => {
116
+ const url: URL = URL.fromString("tel:3136361710");
117
+
118
+ expect(url.toString()).toBe("tel:3136361710");
119
+ expect(url.toString()).not.toBe("https://tel:3136361710/");
120
+ expect(url.protocol).not.toBe(Protocol.HTTPS);
121
+ });
122
+
123
+ test("the value survives the database round-trip", () => {
124
+ const transformer: {
125
+ to: (value: unknown) => unknown;
126
+ from: (value: unknown) => unknown;
127
+ } = URL.getDatabaseTransformer() as {
128
+ to: (value: unknown) => unknown;
129
+ from: (value: unknown) => unknown;
130
+ };
131
+
132
+ const stored: unknown = transformer.to(URL.fromString("tel:3136361710"));
133
+ expect(stored).toBe("tel:3136361710");
134
+
135
+ const readBack: URL = transformer.from(stored) as URL;
136
+ expect(readBack.toString()).toBe("tel:3136361710");
137
+ expect(readBack.isMalformed()).toBe(false);
138
+ });
139
+
140
+ test("the value survives the JSON round-trip to the browser", () => {
141
+ const url: URL = URL.fromString("tel:+13136361710");
142
+ const revived: URL = URL.fromJSON(url.toJSON());
143
+
144
+ expect(revived.toString()).toBe("tel:+13136361710");
145
+ expect(revived.protocol).toBe(Protocol.TEL);
146
+ expect(revived.isMalformed()).toBe(false);
147
+ });
148
+ });
149
+
150
+ describe("URL — an opaque payload may not smuggle an authority or path", () => {
151
+ const invalidOpaquePayloads: Array<string> = [
152
+ // Empty.
153
+ "tel:",
154
+ "sms:",
155
+ // Not a number at all.
156
+ "tel:abc",
157
+ "tel:not-a-phone",
158
+ // Path traversal / a path of any kind.
159
+ "tel:../../etc/passwd",
160
+ "tel:1/../../admin",
161
+ "tel:+1555/path",
162
+ // An authority smuggled behind the number.
163
+ "tel:1@evil.example.com",
164
+ "tel:+15555550123@169.254.169.254",
165
+ "sms:1@evil.example.com",
166
+ // A fragment.
167
+ "tel:1#fragment",
168
+ "tel:+15555550123#.office.com",
169
+ // Whitespace.
170
+ "tel:555 0123",
171
+ "tel: ",
172
+ // Separators only, with no digit anywhere.
173
+ "tel:(",
174
+ "tel:()-.",
175
+ "tel:+",
176
+ // Protocol-relative smuggling.
177
+ "tel://evil.example.com",
178
+ "sms://evil.example.com",
179
+ ];
180
+
181
+ test.each(invalidOpaquePayloads)("rejects %s", (input: string) => {
182
+ expect(() => {
183
+ return URL.fromString(input);
184
+ }).toThrow(BadDataException);
185
+ });
186
+
187
+ test("rejects a payload longer than 256 characters", () => {
188
+ expect(() => {
189
+ return URL.fromString("tel:+" + "1".repeat(300));
190
+ }).toThrow(BadDataException);
191
+ });
192
+
193
+ test("rejects a parameter that is not a well-formed key=value", () => {
194
+ expect(() => {
195
+ return URL.fromString("tel:+15555550123;evil=a/b");
196
+ }).toThrow(BadDataException);
197
+ expect(() => {
198
+ return URL.fromString("tel:+15555550123;a@b");
199
+ }).toThrow(BadDataException);
200
+ });
201
+
202
+ test("the error names the offending value", () => {
203
+ expect(() => {
204
+ return URL.fromString("tel:abc");
205
+ }).toThrow("Phone number abc is not in valid format.");
206
+ });
207
+
208
+ /*
209
+ * A dotted quad is a syntactically valid local number, so it parses — but it
210
+ * stays a tel: URL with no hostname, so it can never become the target of an
211
+ * HTTP request the way "https://169.254.169.254" would.
212
+ */
213
+ test("a dotted quad stays a tel: URL and exposes no host", () => {
214
+ const url: URL = URL.fromString("tel:169.254.169.254");
215
+
216
+ expect(url.protocol).toBe(Protocol.TEL);
217
+ expect(url.hostname).toBeUndefined();
218
+ expect(url.toString()).toBe("tel:169.254.169.254");
219
+ });
220
+ });
221
+
222
+ describe("URL — adding opaque schemes did not disturb the existing ones", () => {
223
+ const untouched: Array<[string, Protocol]> = [
224
+ ["https://example.com/api/test", Protocol.HTTPS],
225
+ ["http://example.com/api/test", Protocol.HTTP],
226
+ ["ws://localhost:5000/api/test", Protocol.WS],
227
+ ["wss://localhost:5000/api/test", Protocol.WSS],
228
+ ["mongodb://localhost:27017/test", Protocol.MONGO_DB],
229
+ ["mailto:support@example.com", Protocol.MAIL],
230
+ ];
231
+
232
+ test.each(untouched)(
233
+ "%s still parses as %s",
234
+ (input: string, expected: Protocol) => {
235
+ const url: URL = URL.fromString(input);
236
+ expect(url.protocol).toBe(expected);
237
+ expect(url.opaqueValue).toBe("");
238
+ },
239
+ );
240
+
241
+ test.each(untouched)("%s still round-trips", (input: string) => {
242
+ expect(URL.fromString(input).toString()).toBe(input);
243
+ });
244
+
245
+ test("query strings on http URLs are unaffected", () => {
246
+ const url: URL = URL.fromString("https://example.com/api?a=1&b=2");
247
+ expect(url.getQueryParam("a")).toBe("1");
248
+ expect(url.getQueryParam("b")).toBe("2");
249
+ expect(url.toString()).toBe("https://example.com/api?a=1&b=2");
250
+ });
251
+
252
+ /*
253
+ * "telephone.example.com" starts with "tel" but is not the tel: scheme —
254
+ * prefix matching must be on "tel:", not "tel".
255
+ */
256
+ test("a host that merely starts with tel is not treated as a tel: URL", () => {
257
+ const url: URL = URL.fromString("https://telephone.example.com/x");
258
+ expect(url.protocol).toBe(Protocol.HTTPS);
259
+ expect(url.hostname.toString()).toBe("telephone.example.com");
260
+ });
261
+ });