@oneuptime/common 12.0.23 → 12.0.25

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 (323) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  5. package/Models/DatabaseModels/Project.ts +68 -0
  6. package/Models/DatabaseModels/User.ts +68 -0
  7. package/Server/API/BaseAPI.ts +0 -24
  8. package/Server/API/SlackAPI.ts +0 -2
  9. package/Server/API/TelemetryAPI.ts +29 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  14. package/Server/Middleware/GzipRequestBody.ts +311 -0
  15. package/Server/Middleware/MultipartFormData.ts +87 -3
  16. package/Server/Middleware/SlackAuthorization.ts +96 -18
  17. package/Server/Services/DetectionRuleService.ts +73 -6
  18. package/Server/Services/Index.ts +205 -0
  19. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  20. package/Server/Services/NetworkDeviceService.ts +148 -0
  21. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  22. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  23. package/Server/Services/ProjectService.ts +29 -19
  24. package/Server/Services/SecurityEventService.ts +90 -13
  25. package/Server/Services/StatusPageResourceService.ts +223 -0
  26. package/Server/Services/TelemetryAttributeService.ts +17 -0
  27. package/Server/Services/UserService.ts +4 -6
  28. package/Server/Types/Markdown.ts +161 -67
  29. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  30. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  31. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  32. package/Server/Utils/Attribution.ts +4 -0
  33. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  34. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  35. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  36. package/Server/Utils/StartServer.ts +12 -32
  37. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  38. package/Server/Utils/Telemetry/LlmMetricSpend.ts +181 -0
  39. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  40. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  41. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  42. package/Server/Views/Partials/Head.ejs +2 -1
  43. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  44. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  45. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  46. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  47. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  48. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  49. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  50. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  51. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  52. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  53. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  54. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  55. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  56. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  57. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  58. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  59. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  60. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  61. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  62. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  63. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  64. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  65. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  66. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  67. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  68. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  69. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  70. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  71. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  72. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  73. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  74. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  75. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  76. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  77. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  78. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  79. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  80. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  81. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  83. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  84. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  85. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  86. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  87. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  88. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  89. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  90. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  91. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  92. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  93. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  94. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  95. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  96. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
  97. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
  98. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  99. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  100. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  101. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  102. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  103. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  104. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  105. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  106. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  107. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +584 -0
  108. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  109. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  110. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  111. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  112. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  113. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  114. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  115. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  116. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  117. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  118. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  119. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  120. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  121. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  122. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  123. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  124. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  125. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  126. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  138. package/Types/Telemetry/LlmConventions.ts +255 -0
  139. package/Types/Telemetry/LlmMetricConventions.ts +329 -0
  140. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  141. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  142. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  143. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  144. package/UI/Components/Detail/Detail.tsx +10 -0
  145. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  146. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  147. package/UI/Components/Forms/Types/Field.ts +8 -0
  148. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  149. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  150. package/UI/Components/Forms/Validation.ts +46 -0
  151. package/UI/Components/Input/Input.tsx +3 -0
  152. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  153. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  154. package/UI/Components/ModelTable/Column.ts +8 -0
  155. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  156. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  157. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  158. package/UI/Components/SideOver/SideOver.tsx +5 -5
  159. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  160. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  161. package/UI/Components/Toggle/Toggle.tsx +9 -1
  162. package/UI/Components/Types/FieldType.ts +1 -0
  163. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  164. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  165. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  166. package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
  167. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  168. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  170. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  172. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  174. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  176. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/User.js +76 -0
  178. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  179. package/build/dist/Server/API/BaseAPI.js +3 -19
  180. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  181. package/build/dist/Server/API/SlackAPI.js +0 -2
  182. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  183. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  184. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  193. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  194. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  195. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  196. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  197. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  198. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  199. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  200. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  201. package/build/dist/Server/Services/Index.js +204 -0
  202. package/build/dist/Server/Services/Index.js.map +1 -1
  203. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  204. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  205. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  206. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  210. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  211. package/build/dist/Server/Services/ProjectService.js +25 -45
  212. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  213. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  214. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  215. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  216. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  217. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  218. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  219. package/build/dist/Server/Services/UserService.js +2 -12
  220. package/build/dist/Server/Services/UserService.js.map +1 -1
  221. package/build/dist/Server/Types/Markdown.js +147 -61
  222. package/build/dist/Server/Types/Markdown.js.map +1 -1
  223. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  224. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  225. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  226. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  227. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  228. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  229. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  230. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  231. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  232. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  233. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  234. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  235. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  236. package/build/dist/Server/Utils/StartServer.js +12 -21
  237. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  238. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  239. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  240. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +151 -0
  241. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  242. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  243. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  244. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  245. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  246. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  247. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  248. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  249. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  250. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  251. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  252. package/build/dist/Types/Marketing/Attribution.js +10 -0
  253. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  254. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  255. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  256. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  257. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  258. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  259. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  260. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  261. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  262. package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -0
  263. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  264. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  265. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  266. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  267. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  268. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  269. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  270. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  271. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  272. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  273. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  274. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  275. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  276. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  277. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  278. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  279. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  280. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  281. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  282. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  283. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  284. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  285. package/build/dist/UI/Components/Input/Input.js +1 -1
  286. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  287. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  288. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  289. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  290. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  291. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  292. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  293. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  294. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  295. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  296. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  297. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  298. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  299. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  300. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  301. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  302. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  303. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  304. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  305. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  306. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  307. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  308. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  309. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  310. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  311. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +175 -0
  312. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  313. package/jest.config.json +2 -0
  314. package/package.json +1 -1
  315. package/tsconfig.json +2 -1
  316. package/Tests/UI/Components/Toast.test.tsx +0 -197
  317. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  318. package/UI/Components/Toast/Toast.tsx +0 -189
  319. package/UI/Components/Toast/ToastInit.tsx +0 -110
  320. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  321. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  322. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  323. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,298 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import timers from "timers";
3
+
4
+ /*
5
+ * multer calls `setImmediate` internally (make-middleware.js and
6
+ * remove-uploaded-files.js), and Common's jest environment is jsdom, which
7
+ * does not expose it. A `@jest-environment node` docblock is not an option
8
+ * here - Common's shared jest.setup.ts touches `window`, which is why
9
+ * EsbuildConfig.test.ts spawns a subprocess instead - so lend jsdom the
10
+ * real one from Node.
11
+ */
12
+ if (
13
+ typeof (globalThis as unknown as { setImmediate?: unknown }).setImmediate !==
14
+ "function"
15
+ ) {
16
+ (globalThis as unknown as { setImmediate: unknown }).setImmediate =
17
+ timers.setImmediate;
18
+ }
19
+
20
+ /*
21
+ * The limits under test are 25 and 50 MiB, so the fixtures that probe them
22
+ * are 25 and 50 MiB of real multipart body parsed by the real busboy.
23
+ * That does not fit in jest's 5s default.
24
+ */
25
+ jest.setTimeout(180_000);
26
+
27
+ /*
28
+ * The multipart reader used by Pyroscope ingest and the SendGrid inbound
29
+ * email webhook.
30
+ *
31
+ * Both routes mount it BEFORE their auth check - Pyroscope before
32
+ * isAuthorizedServiceMiddleware, SendGrid before the webhook secret is
33
+ * verified - and it uses multer's memoryStorage, so every part it accepts
34
+ * is buffered in the process on behalf of a caller who has not proved
35
+ * anything yet.
36
+ *
37
+ * multer's defaults leave almost all of that unbounded: fileSize, files,
38
+ * fields and parts are all Infinity out of the box, and only fieldSize
39
+ * has a default at all. These tests pin the limits and the status code a
40
+ * breach produces, because the failure mode of getting this wrong is
41
+ * silent - an unbounded upload looks exactly like a working one until the
42
+ * pod dies.
43
+ */
44
+
45
+ import MultipartFormDataMiddleware, {
46
+ MAX_MULTIPART_FIELDS,
47
+ MAX_MULTIPART_FIELD_BYTES,
48
+ MAX_MULTIPART_FILES,
49
+ MAX_MULTIPART_FILE_BYTES,
50
+ } from "../../../Server/Middleware/MultipartFormData";
51
+ import {
52
+ ExpressRequest,
53
+ ExpressResponse,
54
+ NextFunction,
55
+ } from "../../../Server/Utils/Express";
56
+ import Exception from "../../../Types/Exception/Exception";
57
+ import ExceptionCode from "../../../Types/Exception/ExceptionCode";
58
+ import { Readable } from "stream";
59
+
60
+ const BOUNDARY: string = "----oneuptimetestboundary";
61
+
62
+ interface MultipartPart {
63
+ name: string;
64
+ value: Buffer;
65
+ filename?: string;
66
+ }
67
+
68
+ function buildMultipartBody(parts: Array<MultipartPart>): Buffer {
69
+ const chunks: Array<Buffer> = [];
70
+
71
+ for (const part of parts) {
72
+ const disposition: string = part.filename
73
+ ? `form-data; name="${part.name}"; filename="${part.filename}"`
74
+ : `form-data; name="${part.name}"`;
75
+
76
+ const header: string =
77
+ `--${BOUNDARY}\r\n` +
78
+ `Content-Disposition: ${disposition}\r\n` +
79
+ (part.filename ? "Content-Type: application/octet-stream\r\n" : "") +
80
+ "\r\n";
81
+
82
+ chunks.push(Buffer.from(header));
83
+ chunks.push(part.value);
84
+ chunks.push(Buffer.from("\r\n"));
85
+ }
86
+
87
+ chunks.push(Buffer.from(`--${BOUNDARY}--\r\n`));
88
+
89
+ return Buffer.concat(chunks as unknown as Array<Uint8Array>);
90
+ }
91
+
92
+ interface Outcome {
93
+ req: ExpressRequest;
94
+ error: unknown;
95
+ nextCalls: number;
96
+ }
97
+
98
+ /*
99
+ * Drive the middleware with a real multipart body over a real Readable, so
100
+ * busboy does the parsing it does in production rather than being faked.
101
+ */
102
+ function run(parts: Array<MultipartPart>): Promise<Outcome> {
103
+ const body: Buffer = buildMultipartBody(parts);
104
+
105
+ const req: Readable & {
106
+ headers: Record<string, string>;
107
+ body?: unknown;
108
+ files?: unknown;
109
+ } = Readable.from([body]) as never;
110
+
111
+ req.headers = {
112
+ "content-type": `multipart/form-data; boundary=${BOUNDARY}`,
113
+ "content-length": String(body.length),
114
+ };
115
+
116
+ const res: ExpressResponse = {
117
+ status: (): unknown => {
118
+ return { send: (): void => {}, json: (): void => {} };
119
+ },
120
+ headersSent: false,
121
+ } as unknown as ExpressResponse;
122
+
123
+ return new Promise<Outcome>((resolve: (value: Outcome) => void): void => {
124
+ let nextCalls: number = 0;
125
+
126
+ MultipartFormDataMiddleware(req as unknown as ExpressRequest, res, ((
127
+ err?: unknown,
128
+ ): void => {
129
+ nextCalls++;
130
+ resolve({
131
+ req: req as unknown as ExpressRequest,
132
+ error: err,
133
+ nextCalls: nextCalls,
134
+ });
135
+ }) as NextFunction);
136
+ });
137
+ }
138
+
139
+ function files(outcome: Outcome): Array<{ fieldname: string; buffer: Buffer }> {
140
+ return (outcome.req.files || []) as Array<{
141
+ fieldname: string;
142
+ buffer: Buffer;
143
+ }>;
144
+ }
145
+
146
+ describe("MultipartFormData - the limits are set where they were reasoned about", () => {
147
+ test("sizes match the app-wide 50 MiB body-parser number", () => {
148
+ expect(MAX_MULTIPART_FILE_BYTES).toBe(50 * 1024 * 1024);
149
+ expect(MAX_MULTIPART_FIELD_BYTES).toBe(25 * 1024 * 1024);
150
+ });
151
+
152
+ test("counts are bounded at all, which is the part multer leaves at Infinity", () => {
153
+ expect(MAX_MULTIPART_FILES).toBe(50);
154
+ expect(MAX_MULTIPART_FIELDS).toBe(200);
155
+ expect(Number.isFinite(MAX_MULTIPART_FILES)).toBe(true);
156
+ expect(Number.isFinite(MAX_MULTIPART_FIELDS)).toBe(true);
157
+ });
158
+ });
159
+
160
+ describe("MultipartFormData - normal bodies still parse", () => {
161
+ test("a single file arrives as a Buffer on req.files", async () => {
162
+ const outcome: Outcome = await run([
163
+ { name: "profile", value: Buffer.from("pprof-bytes"), filename: "p.pb" },
164
+ ]);
165
+
166
+ expect(outcome.error).toBeUndefined();
167
+ expect(files(outcome)).toHaveLength(1);
168
+ expect(files(outcome)[0]!.fieldname).toBe("profile");
169
+ expect(files(outcome)[0]!.buffer.toString()).toBe("pprof-bytes");
170
+ });
171
+
172
+ test("fields and files together, the SendGrid shape", async () => {
173
+ const outcome: Outcome = await run([
174
+ { name: "from", value: Buffer.from("someone@example.com") },
175
+ { name: "subject", value: Buffer.from("hello") },
176
+ { name: "attachment1", value: Buffer.from("PDF"), filename: "a.pdf" },
177
+ ]);
178
+
179
+ expect(outcome.error).toBeUndefined();
180
+ expect((outcome.req.body as Record<string, string>)["from"]).toBe(
181
+ "someone@example.com",
182
+ );
183
+ expect(files(outcome)).toHaveLength(1);
184
+ });
185
+
186
+ test("a body with no parts at all is fine", async () => {
187
+ const outcome: Outcome = await run([]);
188
+
189
+ expect(outcome.error).toBeUndefined();
190
+ expect(files(outcome)).toHaveLength(0);
191
+ });
192
+
193
+ test("a file right at the count limit is accepted", async () => {
194
+ const outcome: Outcome = await run(
195
+ Array.from({ length: MAX_MULTIPART_FILES }, (_v: unknown, i: number) => {
196
+ return {
197
+ name: `f${i}`,
198
+ value: Buffer.from("x"),
199
+ filename: `f${i}.bin`,
200
+ };
201
+ }),
202
+ );
203
+
204
+ expect(outcome.error).toBeUndefined();
205
+ expect(files(outcome)).toHaveLength(MAX_MULTIPART_FILES);
206
+ });
207
+
208
+ test("fields right at the count limit are accepted", async () => {
209
+ const outcome: Outcome = await run(
210
+ Array.from({ length: MAX_MULTIPART_FIELDS }, (_v: unknown, i: number) => {
211
+ return { name: `k${i}`, value: Buffer.from("v") };
212
+ }),
213
+ );
214
+
215
+ expect(outcome.error).toBeUndefined();
216
+ expect(
217
+ Object.keys(outcome.req.body as Record<string, unknown>),
218
+ ).toHaveLength(MAX_MULTIPART_FIELDS);
219
+ });
220
+ });
221
+
222
+ describe("MultipartFormData - breaches answer 413, not 500", () => {
223
+ test("one file over the size limit", async () => {
224
+ const outcome: Outcome = await run([
225
+ {
226
+ name: "big",
227
+ value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
228
+ filename: "big.bin",
229
+ },
230
+ ]);
231
+
232
+ expect(outcome.error).toBeInstanceOf(Exception);
233
+ expect((outcome.error as Exception).code).toBe(
234
+ ExceptionCode.PayloadTooLargeException,
235
+ );
236
+ expect((outcome.error as Exception).code).toBe(413);
237
+ expect((outcome.error as Exception).message).toContain("LIMIT_FILE_SIZE");
238
+ });
239
+
240
+ test("one file too many", async () => {
241
+ const outcome: Outcome = await run(
242
+ Array.from(
243
+ { length: MAX_MULTIPART_FILES + 1 },
244
+ (_v: unknown, i: number) => {
245
+ return {
246
+ name: `f${i}`,
247
+ value: Buffer.from("x"),
248
+ filename: `f${i}.bin`,
249
+ };
250
+ },
251
+ ),
252
+ );
253
+
254
+ expect((outcome.error as Exception).code).toBe(
255
+ ExceptionCode.PayloadTooLargeException,
256
+ );
257
+ expect((outcome.error as Exception).message).toContain("LIMIT_FILE_COUNT");
258
+ });
259
+
260
+ test("one field too many - thousands of tiny parts is the cheap version of this attack", async () => {
261
+ const outcome: Outcome = await run(
262
+ Array.from(
263
+ { length: MAX_MULTIPART_FIELDS + 1 },
264
+ (_v: unknown, i: number) => {
265
+ return { name: `k${i}`, value: Buffer.from("v") };
266
+ },
267
+ ),
268
+ );
269
+
270
+ expect((outcome.error as Exception).code).toBe(
271
+ ExceptionCode.PayloadTooLargeException,
272
+ );
273
+ expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_COUNT");
274
+ });
275
+
276
+ test("one field value over the size limit", async () => {
277
+ const outcome: Outcome = await run([
278
+ { name: "html", value: Buffer.alloc(MAX_MULTIPART_FIELD_BYTES + 1) },
279
+ ]);
280
+
281
+ expect((outcome.error as Exception).code).toBe(
282
+ ExceptionCode.PayloadTooLargeException,
283
+ );
284
+ expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_VALUE");
285
+ });
286
+
287
+ test("next() is called exactly once on a breach", async () => {
288
+ const outcome: Outcome = await run([
289
+ {
290
+ name: "big",
291
+ value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
292
+ filename: "big.bin",
293
+ },
294
+ ]);
295
+
296
+ expect(outcome.nextCalls).toBe(1);
297
+ });
298
+ });
@@ -1,5 +1,6 @@
1
- import { describe, expect, test, beforeEach } from "@jest/globals";
1
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
2
2
  import crypto from "crypto";
3
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
3
4
  import SlackAuthorization from "../../../Server/Middleware/SlackAuthorization";
4
5
  import Response from "../../../Server/Utils/Response";
5
6
  import {
@@ -8,6 +9,7 @@ import {
8
9
  OneUptimeRequest,
9
10
  } from "../../../Server/Utils/Express";
10
11
  import BadDataException from "../../../Types/Exception/BadDataException";
12
+ import ServiceUnavailableException from "../../../Types/Exception/ServiceUnavailableException";
11
13
 
12
14
  const SIGNING_SECRET: string = "slack-app-signing-secret";
13
15
 
@@ -28,6 +30,16 @@ jest.mock("../../../Server/Utils/Response", () => {
28
30
  __esModule: true,
29
31
  default: {
30
32
  sendErrorResponse: jest.fn(),
33
+ sendTextResponse: jest.fn(),
34
+ },
35
+ };
36
+ });
37
+
38
+ jest.mock("../../../Server/Infrastructure/GlobalCache", () => {
39
+ return {
40
+ __esModule: true,
41
+ default: {
42
+ setStringIfNotExists: jest.fn(),
31
43
  },
32
44
  };
33
45
  });
@@ -41,11 +53,16 @@ jest.mock("../../../Server/Utils/Response", () => {
41
53
  * have to come back as the intended 400.
42
54
  */
43
55
  describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
44
- const TIMESTAMP: string = "1700000000";
56
+ const NOW_IN_SECONDS: number = 1_800_000_000;
57
+ const TIMESTAMP: string = NOW_IN_SECONDS.toString();
45
58
  const RAW_BODY: string = "token=abc&team_id=T123&command=%2Foneuptime";
46
59
 
47
60
  const sendErrorResponse: jest.Mock =
48
61
  Response.sendErrorResponse as unknown as jest.Mock;
62
+ const sendTextResponse: jest.Mock =
63
+ Response.sendTextResponse as unknown as jest.Mock;
64
+ const setStringIfNotExists: jest.Mock =
65
+ GlobalCache.setStringIfNotExists as unknown as jest.Mock;
49
66
 
50
67
  const res: ExpressResponse = {} as ExpressResponse;
51
68
  let next: NextFunction;
@@ -62,15 +79,19 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
62
79
  type BuildRequestFunction = (
63
80
  signature: string | undefined,
64
81
  body?: string,
82
+ timestamp?: string | null,
65
83
  ) => OneUptimeRequest;
66
84
 
67
85
  const buildRequest: BuildRequestFunction = (
68
86
  signature: string | undefined,
69
87
  body: string = RAW_BODY,
88
+ timestamp: string | null = TIMESTAMP,
70
89
  ): OneUptimeRequest => {
71
- const headers: Record<string, string> = {
72
- "x-slack-request-timestamp": TIMESTAMP,
73
- };
90
+ const headers: Record<string, string> = {};
91
+
92
+ if (timestamp !== null) {
93
+ headers["x-slack-request-timestamp"] = timestamp;
94
+ }
74
95
 
75
96
  if (signature !== undefined) {
76
97
  headers["x-slack-signature"] = signature;
@@ -81,9 +102,15 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
81
102
 
82
103
  beforeEach(() => {
83
104
  jest.clearAllMocks();
105
+ jest.spyOn(Date, "now").mockReturnValue(NOW_IN_SECONDS * 1000);
106
+ setStringIfNotExists.mockResolvedValue(true);
84
107
  next = jest.fn();
85
108
  });
86
109
 
110
+ afterEach(() => {
111
+ jest.restoreAllMocks();
112
+ });
113
+
87
114
  test("calls next when the signature matches the timestamp and body", async () => {
88
115
  const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
89
116
 
@@ -91,6 +118,12 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
91
118
 
92
119
  expect(next).toHaveBeenCalled();
93
120
  expect(sendErrorResponse).not.toHaveBeenCalled();
121
+ expect(setStringIfNotExists).toHaveBeenCalledWith(
122
+ "slack-request-replay",
123
+ sign(TIMESTAMP, RAW_BODY).slice("v0=".length),
124
+ TIMESTAMP,
125
+ { expiresInSeconds: 5 * 60 + 1 },
126
+ );
94
127
  });
95
128
 
96
129
  test("rejects a well-formed signature computed over a different body", async () => {
@@ -107,6 +140,212 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
107
140
  res,
108
141
  new BadDataException("Slack Signature Verification Failed."),
109
142
  );
143
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
144
+ });
145
+
146
+ test("accepts a request exactly five minutes old", async () => {
147
+ const timestamp: string = (NOW_IN_SECONDS - 5 * 60).toString();
148
+ const req: OneUptimeRequest = buildRequest(
149
+ sign(timestamp, RAW_BODY),
150
+ RAW_BODY,
151
+ timestamp,
152
+ );
153
+
154
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
155
+
156
+ expect(next).toHaveBeenCalled();
157
+ expect(sendErrorResponse).not.toHaveBeenCalled();
158
+ });
159
+
160
+ test("accepts a request exactly five minutes in the future and covers the full replay window", async () => {
161
+ const timestamp: string = (NOW_IN_SECONDS + 5 * 60).toString();
162
+ const req: OneUptimeRequest = buildRequest(
163
+ sign(timestamp, RAW_BODY),
164
+ RAW_BODY,
165
+ timestamp,
166
+ );
167
+
168
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
169
+
170
+ expect(next).toHaveBeenCalled();
171
+ expect(setStringIfNotExists).toHaveBeenCalledWith(
172
+ "slack-request-replay",
173
+ sign(timestamp, RAW_BODY).slice("v0=".length),
174
+ timestamp,
175
+ { expiresInSeconds: 10 * 60 + 1 },
176
+ );
177
+ });
178
+
179
+ test("rejects a correctly signed request older than five minutes", async () => {
180
+ const timestamp: string = (NOW_IN_SECONDS - 5 * 60 - 1).toString();
181
+ const req: OneUptimeRequest = buildRequest(
182
+ sign(timestamp, RAW_BODY),
183
+ RAW_BODY,
184
+ timestamp,
185
+ );
186
+
187
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
188
+
189
+ expect(next).not.toHaveBeenCalled();
190
+ expect(sendErrorResponse).toHaveBeenCalledWith(
191
+ req,
192
+ res,
193
+ new BadDataException("Slack Signature Verification Failed."),
194
+ );
195
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
196
+ });
197
+
198
+ test("rejects a correctly signed request more than five minutes in the future", async () => {
199
+ const timestamp: string = (NOW_IN_SECONDS + 5 * 60 + 1).toString();
200
+ const req: OneUptimeRequest = buildRequest(
201
+ sign(timestamp, RAW_BODY),
202
+ RAW_BODY,
203
+ timestamp,
204
+ );
205
+
206
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
207
+
208
+ expect(next).not.toHaveBeenCalled();
209
+ expect(sendErrorResponse).toHaveBeenCalledWith(
210
+ req,
211
+ res,
212
+ new BadDataException("Slack Signature Verification Failed."),
213
+ );
214
+ });
215
+
216
+ test("acknowledges a duplicate without invoking the Slack handler", async () => {
217
+ setStringIfNotExists.mockResolvedValue(false);
218
+ const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
219
+
220
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
221
+
222
+ expect(next).not.toHaveBeenCalled();
223
+ expect(sendErrorResponse).not.toHaveBeenCalled();
224
+ expect(sendTextResponse).toHaveBeenCalledWith(req, res, "");
225
+ });
226
+
227
+ test("uses the canonical digest so signature casing cannot bypass replay detection", async () => {
228
+ const lowercaseSignature: string = sign(TIMESTAMP, RAW_BODY);
229
+ const uppercaseSignature: string = `v0=${lowercaseSignature
230
+ .slice("v0=".length)
231
+ .toUpperCase()}`;
232
+ const firstRequest: OneUptimeRequest = buildRequest(lowercaseSignature);
233
+ const replayRequest: OneUptimeRequest = buildRequest(uppercaseSignature);
234
+
235
+ setStringIfNotExists
236
+ .mockResolvedValueOnce(true)
237
+ .mockResolvedValueOnce(false);
238
+
239
+ await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
240
+ await SlackAuthorization.isAuthorizedSlackRequest(replayRequest, res, next);
241
+
242
+ const canonicalDigest: string = lowercaseSignature.slice("v0=".length);
243
+ expect(setStringIfNotExists).toHaveBeenNthCalledWith(
244
+ 1,
245
+ "slack-request-replay",
246
+ canonicalDigest,
247
+ TIMESTAMP,
248
+ { expiresInSeconds: 5 * 60 + 1 },
249
+ );
250
+ expect(setStringIfNotExists).toHaveBeenNthCalledWith(
251
+ 2,
252
+ "slack-request-replay",
253
+ canonicalDigest,
254
+ TIMESTAMP,
255
+ { expiresInSeconds: 5 * 60 + 1 },
256
+ );
257
+ expect(next).toHaveBeenCalledTimes(1);
258
+ expect(sendTextResponse).toHaveBeenCalledWith(replayRequest, res, "");
259
+ });
260
+
261
+ test("allows repeated URL verification requests to reach their idempotent handler", async () => {
262
+ const signature: string = sign(TIMESTAMP, RAW_BODY);
263
+ const firstRequest: OneUptimeRequest = buildRequest(signature);
264
+ const secondRequest: OneUptimeRequest = buildRequest(signature);
265
+
266
+ Object.assign(firstRequest, {
267
+ route: { path: "/slack/events" },
268
+ body: { type: "url_verification" },
269
+ });
270
+ Object.assign(secondRequest, {
271
+ route: { path: "/slack/events" },
272
+ body: { type: "url_verification" },
273
+ });
274
+
275
+ await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
276
+ await SlackAuthorization.isAuthorizedSlackRequest(secondRequest, res, next);
277
+
278
+ expect(next).toHaveBeenCalledTimes(2);
279
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
280
+ });
281
+
282
+ test("allows repeated options loads to reach their idempotent handler", async () => {
283
+ const signature: string = sign(TIMESTAMP, RAW_BODY);
284
+ const firstRequest: OneUptimeRequest = buildRequest(signature);
285
+ const secondRequest: OneUptimeRequest = buildRequest(signature);
286
+
287
+ Object.assign(firstRequest, {
288
+ route: { path: "/slack/options-load" },
289
+ });
290
+ Object.assign(secondRequest, {
291
+ route: { path: "/slack/options-load" },
292
+ });
293
+
294
+ await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
295
+ await SlackAuthorization.isAuthorizedSlackRequest(secondRequest, res, next);
296
+
297
+ expect(next).toHaveBeenCalledTimes(2);
298
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
299
+ });
300
+
301
+ test("fails closed when distributed replay protection is unavailable", async () => {
302
+ setStringIfNotExists.mockRejectedValue(new Error("cache unavailable"));
303
+ const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
304
+
305
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
306
+
307
+ expect(next).not.toHaveBeenCalled();
308
+ expect(sendErrorResponse).toHaveBeenCalledWith(
309
+ req,
310
+ res,
311
+ new ServiceUnavailableException(
312
+ "Slack request verification is temporarily unavailable.",
313
+ ),
314
+ );
315
+ });
316
+
317
+ const malformedTimestamps: Array<[string, string | null]> = [
318
+ ["absent", null],
319
+ ["empty", ""],
320
+ ["non-numeric", "not-a-timestamp"],
321
+ ["partially numeric", `${TIMESTAMP}abc`],
322
+ ["fractional", `${TIMESTAMP}.5`],
323
+ ["negative", "-1"],
324
+ ["outside the safe integer range", "999999999999999999999"],
325
+ ];
326
+
327
+ describe("rejects a malformed timestamp", () => {
328
+ test.each(malformedTimestamps)(
329
+ "%s",
330
+ async (_label: string, timestamp: string | null) => {
331
+ const signedTimestamp: string = timestamp || TIMESTAMP;
332
+ const req: OneUptimeRequest = buildRequest(
333
+ sign(signedTimestamp, RAW_BODY),
334
+ RAW_BODY,
335
+ timestamp,
336
+ );
337
+
338
+ await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
339
+
340
+ expect(next).not.toHaveBeenCalled();
341
+ expect(sendErrorResponse).toHaveBeenCalledWith(
342
+ req,
343
+ res,
344
+ new BadDataException("Slack Signature Verification Failed."),
345
+ );
346
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
347
+ },
348
+ );
110
349
  });
111
350
 
112
351
  /*
@@ -143,7 +382,25 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
143
382
  res,
144
383
  new BadDataException("Slack Signature Verification Failed."),
145
384
  );
385
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
146
386
  },
147
387
  );
148
388
  });
389
+
390
+ test("normalizes an array signature header without throwing", async () => {
391
+ const req: OneUptimeRequest = buildRequest(undefined);
392
+ req.headers["x-slack-signature"] = ["not-a-signature"];
393
+
394
+ await expect(
395
+ SlackAuthorization.isAuthorizedSlackRequest(req, res, next),
396
+ ).resolves.toBeUndefined();
397
+
398
+ expect(next).not.toHaveBeenCalled();
399
+ expect(sendErrorResponse).toHaveBeenCalledWith(
400
+ req,
401
+ res,
402
+ new BadDataException("Slack Signature Verification Failed."),
403
+ );
404
+ expect(setStringIfNotExists).not.toHaveBeenCalled();
405
+ });
149
406
  });