@oneuptime/common 12.0.6 → 12.0.7

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 (424) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +1 -1
  2. package/Models/DatabaseModels/DatabaseBaseModel/FileModel.ts +2 -3
  3. package/Models/DatabaseModels/DeletedProject.ts +595 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Monitor.ts +111 -0
  6. package/Models/DatabaseModels/Project.ts +3 -3
  7. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -1
  8. package/Server/API/FileAPI.ts +15 -2
  9. package/Server/API/MicrosoftTeamsAPI.ts +6 -1
  10. package/Server/API/ProjectAPI.ts +214 -0
  11. package/Server/API/StatusPageAPI.ts +39 -6
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.ts +50 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.ts +67 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.ts +39 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIAgentService.ts +20 -0
  17. package/Server/Services/DashboardDomainService.ts +73 -28
  18. package/Server/Services/DatabaseService.ts +1 -0
  19. package/Server/Services/DeletedProjectService.ts +138 -0
  20. package/Server/Services/FileService.ts +61 -0
  21. package/Server/Services/IncidentEpisodePublicNoteService.ts +18 -0
  22. package/Server/Services/IncidentPublicNoteService.ts +18 -0
  23. package/Server/Services/Index.ts +2 -0
  24. package/Server/Services/MonitorService.ts +311 -0
  25. package/Server/Services/ProbeService.ts +20 -0
  26. package/Server/Services/ProjectService.ts +297 -1
  27. package/Server/Services/PushNotificationService.ts +71 -0
  28. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +18 -0
  29. package/Server/Services/StatusPageAnnouncementService.ts +53 -0
  30. package/Server/Services/StatusPageDomainService.ts +73 -28
  31. package/Server/Services/TeamMemberService.ts +117 -12
  32. package/Server/Services/TelemetryIngestionKeyService.ts +1 -0
  33. package/Server/Services/UserWebhookService.ts +34 -109
  34. package/Server/Services/WorkspaceNotificationRuleService.ts +69 -0
  35. package/Server/Services/WorkspaceProjectAuthTokenService.ts +37 -1
  36. package/Server/Types/Database/DeleteById.ts +2 -0
  37. package/Server/Types/Database/DeleteOneBy.ts +6 -0
  38. package/Server/Types/Domain.ts +41 -0
  39. package/Server/Types/Workflow/Components/API/Delete.ts +2 -1
  40. package/Server/Types/Workflow/Components/API/Get.ts +2 -1
  41. package/Server/Types/Workflow/Components/API/Patch.ts +2 -1
  42. package/Server/Types/Workflow/Components/API/Post.ts +2 -1
  43. package/Server/Types/Workflow/Components/API/Put.ts +2 -1
  44. package/Server/Types/Workflow/Components/API/Utils.ts +41 -2
  45. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +25 -19
  46. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +6 -5
  47. package/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.ts +32 -14
  48. package/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.ts +31 -13
  49. package/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.ts +29 -20
  50. package/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.ts +28 -21
  51. package/Server/Types/Workflow/Components/BaseModel/LogComponentError.ts +89 -0
  52. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +208 -0
  53. package/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts +11 -0
  54. package/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.ts +41 -18
  55. package/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.ts +45 -19
  56. package/Server/Types/Workflow/Components/Discord/SendMessageToChannel.ts +16 -9
  57. package/Server/Types/Workflow/Components/IncomingWebhookUtils.ts +66 -0
  58. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +15 -9
  59. package/Server/Types/Workflow/Components/Slack/SendMessageToChannel.ts +17 -0
  60. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
  61. package/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.ts +171 -0
  62. package/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.ts +3 -3
  63. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +3 -2
  64. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +1 -1
  65. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +6 -6
  66. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +43 -5
  67. package/Server/Utils/DataSource/EgressGuard.ts +176 -16
  68. package/Server/Utils/DataSource/HttpFetch.ts +6 -45
  69. package/Server/Utils/Execute.ts +8 -0
  70. package/Server/Utils/InlineImageAccessTokenSync.ts +23 -0
  71. package/Server/Utils/LLM/LLMService.ts +57 -12
  72. package/Server/Utils/Monitor/MonitorAlert.ts +40 -0
  73. package/Server/Utils/Monitor/MonitorDependencySuppression.ts +192 -0
  74. package/Server/Utils/Monitor/MonitorIncident.ts +40 -0
  75. package/Server/Utils/Monitor/MonitorResource.ts +36 -0
  76. package/Server/Utils/Response.ts +142 -6
  77. package/Server/Utils/SSRFProtection.ts +273 -60
  78. package/Server/Utils/StartServer.ts +8 -0
  79. package/Server/Utils/VM/VMRunner.ts +88 -1
  80. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +555 -49
  81. package/Server/Utils/Workspace/Slack/Slack.ts +25 -2
  82. package/Server/Utils/Workspace/WorkspaceBase.ts +6 -0
  83. package/Tests/App/Dashboard/DangerZone.test.tsx +334 -0
  84. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +539 -0
  85. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +678 -0
  86. package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +706 -0
  87. package/Tests/App/Dashboard/DashboardVariablesModal.test.tsx +508 -0
  88. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +470 -0
  89. package/Tests/App/StatusPage/StatusPageDetailPagesNotFound.test.tsx +308 -0
  90. package/Tests/Models/DatabaseModels/DeletedProjectModel.test.ts +251 -0
  91. package/Tests/Models/DatabaseModels/SavedViewJSONColumnTuples.test.ts +115 -0
  92. package/Tests/Server/API/FileAPIAccessControl.test.ts +316 -0
  93. package/Tests/Server/API/ProjectAPI.test.ts +299 -1
  94. package/Tests/Server/API/ProjectAPIAdjustBalance.test.ts +459 -0
  95. package/Tests/Server/API/ProjectAPIAdjustBalanceBillingDisabled.test.ts +177 -0
  96. package/Tests/Server/Services/DatabaseServiceDeletionReason.test.ts +123 -0
  97. package/Tests/Server/Services/DeletedProjectService.test.ts +415 -0
  98. package/Tests/Server/Services/DeletedProjectServiceSelfHosted.test.ts +70 -0
  99. package/Tests/Server/Services/DomainSubdomainHooks.test.ts +94 -0
  100. package/Tests/Server/Services/FileIsPublicBooleanMigration.test.ts +244 -0
  101. package/Tests/Server/Services/FileServiceMimeType.test.ts +89 -0
  102. package/Tests/Server/Services/MonitorServiceDependencyValidation.test.ts +702 -0
  103. package/Tests/Server/Services/ProjectServiceAdjustBalance.test.ts +615 -0
  104. package/Tests/Server/Services/ProjectServiceDeleteAudit.test.ts +406 -0
  105. package/Tests/Server/Services/PublishTimeImageVisibility.test.ts +450 -0
  106. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +669 -0
  107. package/Tests/Server/Services/UserWebhookSSRF.test.ts +174 -0
  108. package/Tests/Server/Services/WebPushEndpointAllowlist.test.ts +133 -0
  109. package/Tests/Server/Services/WorkspaceProjectAuthTokenRepoint.test.ts +618 -0
  110. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +26 -1
  111. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +215 -0
  112. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +742 -0
  113. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +256 -0
  114. package/Tests/Server/Types/Workflow/Components/ModelArguments.test.ts +371 -0
  115. package/Tests/Server/Utils/AI/CodeAgentCommandGuard.test.ts +204 -0
  116. package/Tests/Server/Utils/AI/CodeAgentWriteLimits.test.ts +213 -0
  117. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +52 -5
  118. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +3 -0
  119. package/Tests/Server/Utils/AI/LLMServiceEgressGuard.test.ts +315 -0
  120. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +3 -0
  121. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +17 -5
  122. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +14 -1
  123. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +35 -0
  124. package/Tests/Server/Utils/AI/StubLLMEgressGuard.ts +25 -0
  125. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +62 -7
  126. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +69 -0
  127. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +330 -0
  128. package/Tests/Server/Utils/GitHubCreatePullRequestWithToken.test.ts +393 -0
  129. package/Tests/Server/Utils/InlineImageAccessTokenSync.test.ts +202 -0
  130. package/Tests/Server/Utils/Monitor/MonitorDependencySuppression.test.ts +811 -0
  131. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +604 -0
  132. package/Tests/Server/Utils/ResponseSendFileResponse.test.ts +192 -0
  133. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +406 -0
  134. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +194 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +1169 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +2 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +1605 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTenantResolution.test.ts +1218 -0
  139. package/Tests/Types/AI/AIChatMessageStatus.test.ts +79 -0
  140. package/Tests/Types/AI/AIInsightStatus.test.ts +40 -0
  141. package/Tests/Types/API/HostnameAuthorityParsing.test.ts +157 -0
  142. package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +27 -0
  143. package/Tests/Types/DataSource/DataSourceLimits.test.ts +71 -0
  144. package/Tests/Types/DataSource/DataSourceType.test.ts +181 -0
  145. package/Tests/Types/DomainSubdomainValidation.test.ts +90 -0
  146. package/Tests/Types/JSONFunctions.test.ts +252 -1
  147. package/Tests/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.test.ts +61 -0
  148. package/Tests/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.test.ts +62 -0
  149. package/Tests/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.test.ts +68 -0
  150. package/Tests/Types/Monitor/SnmpMonitor/SnmpVersion.test.ts +51 -0
  151. package/Tests/UI/Components/404.test.tsx +1 -1
  152. package/Tests/UI/Components/Alert.test.tsx +1 -1
  153. package/Tests/UI/Components/Badge.test.tsx +1 -1
  154. package/Tests/UI/Components/BasicForm.test.tsx +39 -10
  155. package/Tests/UI/Components/Button.test.tsx +1 -1
  156. package/Tests/UI/Components/Card.test.tsx +1 -1
  157. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +1 -1
  158. package/Tests/UI/Components/ColorViewer.test.tsx +1 -1
  159. package/Tests/UI/Components/ComponentsModal.test.tsx +7 -1
  160. package/Tests/UI/Components/DictionaryOfStrings.test.tsx +1 -1
  161. package/Tests/UI/Components/Dropdown.test.tsx +1 -1
  162. package/Tests/UI/Components/FilePicker.test.tsx +1 -1
  163. package/Tests/UI/Components/HiddenText.test.tsx +1 -1
  164. package/Tests/UI/Components/Input.test.tsx +1 -1
  165. package/Tests/UI/Components/Loader.test.tsx +1 -1
  166. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +1 -1
  167. package/Tests/UI/Components/MasterPage.test.tsx +1 -1
  168. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +489 -0
  169. package/Tests/UI/Components/ModelDelete.test.tsx +214 -0
  170. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1 -1
  171. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +1 -1
  172. package/Tests/UI/Components/ModelTable/BaseModelTableSearchLabels.test.tsx +1 -1
  173. package/Tests/UI/Components/ModelTable/BaseModelTableUrlState.test.tsx +1 -1
  174. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1 -1
  175. package/Tests/UI/Components/NavBar.test.tsx +1 -1
  176. package/Tests/UI/Components/OperatorSelectorKeyboard.test.tsx +1 -1
  177. package/Tests/UI/Components/Pagination.test.tsx +1 -1
  178. package/Tests/UI/Components/Pill.test.tsx +1 -1
  179. package/Tests/UI/Components/Probe.test.tsx +1 -1
  180. package/Tests/UI/Components/ProgressBar.test.tsx +1 -1
  181. package/Tests/UI/Components/RadioButtons.test.tsx +1 -1
  182. package/Tests/UI/Components/SideMenuItem.test.tsx +1 -1
  183. package/Tests/UI/Components/SideOver.test.tsx +500 -6
  184. package/Tests/UI/Components/Tabs.test.tsx +1 -1
  185. package/Tests/UI/Components/TelemetrySearchBar.test.tsx +1 -1
  186. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +1 -1
  187. package/Tests/UI/Components/TextArea.test.tsx +1 -1
  188. package/Tests/UI/Components/Toast.test.tsx +1 -1
  189. package/Tests/UI/Components/Toggle.test.tsx +1 -1
  190. package/Tests/UI/Components/TopSection.test.tsx +1 -1
  191. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +1 -1
  192. package/Tests/UI/Rum/ReplayScrubber.test.tsx +1 -1
  193. package/Tests/UI/Rum/ReplayStage.test.tsx +1 -1
  194. package/Tests/UI/Telemetry/TelemetrySnapshotWindowAlert.test.tsx +1 -1
  195. package/Tests/UI/Utils/LocalStorageArrayRoundTrip.test.ts +72 -0
  196. package/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts +19 -2
  197. package/Tests/Utils/Dashboard/ModelQueryVariableInterpolation.test.ts +44 -0
  198. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +288 -5
  199. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +42 -0
  200. package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +27 -0
  201. package/Tests/Utils/LegacySerializedArray.test.ts +139 -0
  202. package/Tests/Utils/Monitor/MonitorDependencyRule.test.ts +190 -0
  203. package/Tests/Utils/Realtime.test.ts +78 -0
  204. package/Tests/Utils/Telemetry/SavedViewFilters.test.ts +246 -0
  205. package/Tests/__mocks__/i18next-browser-languagedetector.js +21 -0
  206. package/Types/AI/FixPullRequestCiStatus.ts +1 -1
  207. package/Types/AI/FixVerificationStatus.ts +2 -2
  208. package/Types/API/Hostname.ts +92 -4
  209. package/Types/API/URL.ts +43 -25
  210. package/Types/Billing/BalanceAdjustmentType.ts +14 -0
  211. package/Types/Billing/ProjectBalanceType.ts +13 -0
  212. package/Types/Domain.ts +26 -0
  213. package/Types/File/MimeType.ts +11 -0
  214. package/Types/JSONFunctions.ts +15 -0
  215. package/Types/Telemetry/TelemetrySavedViewState.ts +9 -1
  216. package/Types/Workflow/Components/BaseModel.ts +92 -28
  217. package/Typings/Index.d.ts +12 -0
  218. package/UI/Components/GanttChart/ChartContainer.tsx +4 -3
  219. package/UI/Components/Modal/ConfirmModal.tsx +10 -5
  220. package/UI/Components/Modal/Modal.tsx +27 -47
  221. package/UI/Components/ModelDelete/ModelDelete.tsx +23 -6
  222. package/UI/Components/SideOver/SideOver.tsx +195 -68
  223. package/UI/Utils/PageScrollLock.ts +66 -0
  224. package/Utils/Dashboard/HtmlWidgetDocument.ts +4 -2
  225. package/Utils/Dashboard/VariableInterpolation.ts +19 -1
  226. package/Utils/LegacySerializedArray.ts +60 -0
  227. package/Utils/Monitor/MonitorDependencyRule.ts +88 -0
  228. package/Utils/Telemetry/SavedViewFilters.ts +88 -0
  229. package/build/dist/Models/DatabaseModels/AIInsight.js +1 -1
  230. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  231. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js +2 -3
  232. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js.map +1 -1
  233. package/build/dist/Models/DatabaseModels/DeletedProject.js +640 -0
  234. package/build/dist/Models/DatabaseModels/DeletedProject.js.map +1 -0
  235. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  236. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  237. package/build/dist/Models/DatabaseModels/Monitor.js +106 -0
  238. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  239. package/build/dist/Models/DatabaseModels/Project.js +3 -3
  240. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  241. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  242. package/build/dist/Server/API/FileAPI.js +14 -2
  243. package/build/dist/Server/API/FileAPI.js.map +1 -1
  244. package/build/dist/Server/API/MicrosoftTeamsAPI.js +6 -1
  245. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  246. package/build/dist/Server/API/ProjectAPI.js +157 -4
  247. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  248. package/build/dist/Server/API/StatusPageAPI.js +27 -3
  249. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  250. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js +34 -0
  251. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js.map +1 -0
  252. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js +30 -0
  253. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js.map +1 -0
  254. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js +20 -0
  255. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js.map +1 -0
  256. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  257. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  258. package/build/dist/Server/Services/AIAgentService.js +17 -0
  259. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  260. package/build/dist/Server/Services/DashboardDomainService.js +55 -18
  261. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  262. package/build/dist/Server/Services/DatabaseService.js +1 -0
  263. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  264. package/build/dist/Server/Services/DeletedProjectService.js +124 -0
  265. package/build/dist/Server/Services/DeletedProjectService.js.map +1 -0
  266. package/build/dist/Server/Services/FileService.js +52 -0
  267. package/build/dist/Server/Services/FileService.js.map +1 -1
  268. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +10 -1
  269. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  270. package/build/dist/Server/Services/IncidentPublicNoteService.js +10 -1
  271. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  272. package/build/dist/Server/Services/Index.js +2 -0
  273. package/build/dist/Server/Services/Index.js.map +1 -1
  274. package/build/dist/Server/Services/MonitorService.js +232 -0
  275. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  276. package/build/dist/Server/Services/ProbeService.js +17 -0
  277. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  278. package/build/dist/Server/Services/ProjectService.js +228 -6
  279. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  280. package/build/dist/Server/Services/PushNotificationService.js +56 -0
  281. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  282. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +10 -1
  283. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  284. package/build/dist/Server/Services/StatusPageAnnouncementService.js +54 -0
  285. package/build/dist/Server/Services/StatusPageAnnouncementService.js.map +1 -1
  286. package/build/dist/Server/Services/StatusPageDomainService.js +55 -18
  287. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  288. package/build/dist/Server/Services/TeamMemberService.js +97 -6
  289. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  290. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +1 -0
  291. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  292. package/build/dist/Server/Services/UserWebhookService.js +31 -87
  293. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  294. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +65 -0
  295. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  296. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +23 -0
  297. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  298. package/build/dist/Server/Types/Domain.js +37 -0
  299. package/build/dist/Server/Types/Domain.js.map +1 -1
  300. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +2 -1
  301. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  302. package/build/dist/Server/Types/Workflow/Components/API/Get.js +2 -1
  303. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  304. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +2 -1
  305. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  306. package/build/dist/Server/Types/Workflow/Components/API/Post.js +2 -1
  307. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  308. package/build/dist/Server/Types/Workflow/Components/API/Put.js +2 -1
  309. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  310. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +35 -2
  311. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  312. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +15 -11
  313. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  314. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +2 -3
  315. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  316. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js +21 -10
  317. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js.map +1 -1
  318. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js +21 -10
  319. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js.map +1 -1
  320. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js +18 -13
  321. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js.map +1 -1
  322. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js +18 -13
  323. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js.map +1 -1
  324. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js +43 -0
  325. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js.map +1 -0
  326. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +116 -0
  327. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -0
  328. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js +7 -0
  329. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js.map +1 -1
  330. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js +24 -13
  331. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js.map +1 -1
  332. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js +27 -13
  333. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js.map +1 -1
  334. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js +14 -6
  335. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js.map +1 -1
  336. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js +42 -0
  337. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js.map +1 -0
  338. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +15 -6
  339. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  340. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js +10 -2
  341. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js.map +1 -1
  342. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
  343. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js +141 -0
  344. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js.map +1 -1
  345. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  346. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +1 -1
  347. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +6 -6
  348. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -1
  349. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +39 -5
  350. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  351. package/build/dist/Server/Utils/DataSource/EgressGuard.js +94 -10
  352. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  353. package/build/dist/Server/Utils/DataSource/HttpFetch.js +1 -19
  354. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
  355. package/build/dist/Server/Utils/Execute.js +2 -2
  356. package/build/dist/Server/Utils/Execute.js.map +1 -1
  357. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js +13 -0
  358. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js.map +1 -1
  359. package/build/dist/Server/Utils/LLM/LLMService.js +43 -12
  360. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  361. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +44 -24
  362. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  363. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js +158 -0
  364. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js.map +1 -0
  365. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +50 -30
  366. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  367. package/build/dist/Server/Utils/Monitor/MonitorResource.js +32 -0
  368. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  369. package/build/dist/Server/Utils/Response.js +112 -5
  370. package/build/dist/Server/Utils/Response.js.map +1 -1
  371. package/build/dist/Server/Utils/SSRFProtection.js +219 -53
  372. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  373. package/build/dist/Server/Utils/StartServer.js +7 -0
  374. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  375. package/build/dist/Server/Utils/VM/VMRunner.js +73 -1
  376. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  377. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +445 -43
  378. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  379. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +18 -1
  380. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  381. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  382. package/build/dist/Types/AI/FixPullRequestCiStatus.js +1 -1
  383. package/build/dist/Types/AI/FixVerificationStatus.js +2 -2
  384. package/build/dist/Types/API/Hostname.js +73 -4
  385. package/build/dist/Types/API/Hostname.js.map +1 -1
  386. package/build/dist/Types/API/URL.js +42 -24
  387. package/build/dist/Types/API/URL.js.map +1 -1
  388. package/build/dist/Types/Billing/BalanceAdjustmentType.js +15 -0
  389. package/build/dist/Types/Billing/BalanceAdjustmentType.js.map +1 -0
  390. package/build/dist/Types/Billing/ProjectBalanceType.js +14 -0
  391. package/build/dist/Types/Billing/ProjectBalanceType.js.map +1 -0
  392. package/build/dist/Types/Domain.js +22 -0
  393. package/build/dist/Types/Domain.js.map +1 -1
  394. package/build/dist/Types/File/MimeType.js +11 -0
  395. package/build/dist/Types/File/MimeType.js.map +1 -1
  396. package/build/dist/Types/JSONFunctions.js +14 -0
  397. package/build/dist/Types/JSONFunctions.js.map +1 -1
  398. package/build/dist/Types/Workflow/Components/BaseModel.js +83 -28
  399. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  400. package/build/dist/UI/Components/GanttChart/ChartContainer.js +4 -3
  401. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
  402. package/build/dist/UI/Components/Modal/ConfirmModal.js +3 -1
  403. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  404. package/build/dist/UI/Components/Modal/Modal.js +24 -34
  405. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  406. package/build/dist/UI/Components/ModelDelete/ModelDelete.js +11 -6
  407. package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
  408. package/build/dist/UI/Components/SideOver/SideOver.js +105 -30
  409. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  410. package/build/dist/UI/Utils/PageScrollLock.js +52 -0
  411. package/build/dist/UI/Utils/PageScrollLock.js.map +1 -0
  412. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js +4 -2
  413. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js.map +1 -1
  414. package/build/dist/Utils/Dashboard/VariableInterpolation.js +19 -1
  415. package/build/dist/Utils/Dashboard/VariableInterpolation.js.map +1 -1
  416. package/build/dist/Utils/LegacySerializedArray.js +51 -0
  417. package/build/dist/Utils/LegacySerializedArray.js.map +1 -0
  418. package/build/dist/Utils/Monitor/MonitorDependencyRule.js +47 -0
  419. package/build/dist/Utils/Monitor/MonitorDependencyRule.js.map +1 -0
  420. package/build/dist/Utils/Telemetry/SavedViewFilters.js +52 -0
  421. package/build/dist/Utils/Telemetry/SavedViewFilters.js.map +1 -0
  422. package/jest.config.json +3 -0
  423. package/package.json +1 -1
  424. package/tsconfig.json +3 -1
@@ -0,0 +1,316 @@
1
+ import File from "../../../Models/DatabaseModels/File";
2
+ import FileAPI from "../../../Server/API/FileAPI";
3
+ import FileService from "../../../Server/Services/FileService";
4
+ import {
5
+ ExpressRequest,
6
+ ExpressResponse,
7
+ NextFunction,
8
+ } from "../../../Server/Utils/Express";
9
+ import JSONWebToken from "../../../Server/Utils/JsonWebToken";
10
+ import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
11
+ import Response from "../../../Server/Utils/Response";
12
+ import MimeType from "../../../Types/File/MimeType";
13
+ import ObjectID from "../../../Types/ObjectID";
14
+ import { mockRouter } from "./Helpers";
15
+ import { beforeAll, beforeEach, describe, expect, it } from "@jest/globals";
16
+
17
+ jest.mock("../../../Server/Utils/Express", () => {
18
+ return {
19
+ getRouter: () => {
20
+ return mockRouter;
21
+ },
22
+ };
23
+ });
24
+
25
+ jest.mock("../../../Server/Utils/Response", () => {
26
+ return {
27
+ sendFileResponse: jest.fn(),
28
+ sendErrorResponse: jest.fn(),
29
+ sendEntityResponse: jest.fn(),
30
+ sendEntityArrayResponse: jest.fn(),
31
+ sendJsonObjectResponse: jest.fn(),
32
+ sendEmptySuccessResponse: jest.fn(),
33
+ };
34
+ });
35
+
36
+ jest.mock("../../../Server/Services/FileService", () => {
37
+ return {
38
+ findOneBy: jest.fn(),
39
+ findOneById: jest.fn(),
40
+ };
41
+ });
42
+
43
+ jest.mock("../../../Server/Middleware/UserAuthorization", () => {
44
+ return {
45
+ getAccessTokenFromExpressRequest: jest.fn(),
46
+ };
47
+ });
48
+
49
+ jest.mock("../../../Server/Utils/JsonWebToken", () => {
50
+ return {
51
+ decode: jest.fn(),
52
+ };
53
+ });
54
+
55
+ const TOKEN_ROUTE: string = "/file/image/access-token/:token";
56
+ const ID_ROUTE: string = "/file/image/:imageId";
57
+
58
+ type BuildFileFunction = (isPublic: unknown) => File;
59
+
60
+ /*
61
+ * The route only ever reads file/fileType/isPublic, and isPublic is
62
+ * deliberately typed loose here so the varchar-era value (the STRING
63
+ * "false") can be fed through the gate as well as a real boolean.
64
+ */
65
+ const buildFile: BuildFileFunction = (isPublic: unknown): File => {
66
+ const file: File = new File();
67
+ file.file = Buffer.from("png-bytes");
68
+ file.fileType = MimeType.png;
69
+ (file as unknown as { isPublic: unknown }).isPublic = isPublic;
70
+ return file;
71
+ };
72
+
73
+ type CallRouteFunction = (
74
+ uri: string,
75
+ params: Record<string, string>,
76
+ ) => Promise<void>;
77
+
78
+ const callRoute: CallRouteFunction = async (
79
+ uri: string,
80
+ params: Record<string, string>,
81
+ ): Promise<void> => {
82
+ const req: ExpressRequest = { params } as unknown as ExpressRequest;
83
+ const res: ExpressResponse = {} as ExpressResponse;
84
+ const next: NextFunction = (() => {}) as NextFunction;
85
+
86
+ await mockRouter.match("GET", uri).handlerFunction(req, res, next);
87
+ };
88
+
89
+ type SetAuthenticatedFunction = (isAuthenticated: boolean) => void;
90
+
91
+ const setAuthenticated: SetAuthenticatedFunction = (
92
+ isAuthenticated: boolean,
93
+ ): void => {
94
+ (
95
+ UserMiddleware.getAccessTokenFromExpressRequest as unknown as jest.Mock
96
+ ).mockReturnValue(isAuthenticated ? "a-token" : undefined);
97
+ (JSONWebToken.decode as unknown as jest.Mock).mockReturnValue({
98
+ userId: isAuthenticated ? new ObjectID("user-id") : undefined,
99
+ });
100
+ };
101
+
102
+ describe("FileAPI access control", () => {
103
+ beforeAll(() => {
104
+ new FileAPI();
105
+ });
106
+
107
+ beforeEach(() => {
108
+ jest.clearAllMocks();
109
+ setAuthenticated(false);
110
+ });
111
+
112
+ describe("token route — anonymous callers", () => {
113
+ it("serves a public file", async () => {
114
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
115
+ buildFile(true) as never,
116
+ );
117
+
118
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
119
+
120
+ expect(Response.sendFileResponse).toHaveBeenCalled();
121
+ expect(Response.sendErrorResponse).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it("refuses a private file", async () => {
125
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
126
+ buildFile(false) as never,
127
+ );
128
+
129
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
130
+
131
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
132
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
133
+ });
134
+
135
+ /*
136
+ * Regression guard for the varchar bug: isPublic was persisted as a
137
+ * varchar, so a private file hydrated as the STRING "false", which is
138
+ * truthy — the gate below never fired and every inline image was served
139
+ * to anonymous callers. The column is a real boolean now; this pins the
140
+ * gate to a strict boolean check so a loose value cannot slip through.
141
+ */
142
+ it("refuses a file whose isPublic is the string 'false'", async () => {
143
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
144
+ buildFile("false") as never,
145
+ );
146
+
147
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
148
+
149
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
150
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
151
+ });
152
+
153
+ it("refuses when no file matches the token", async () => {
154
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
155
+ null as never,
156
+ );
157
+
158
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
159
+
160
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
161
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
162
+ });
163
+
164
+ it("refuses when the token param is missing", async () => {
165
+ await callRoute(TOKEN_ROUTE, {});
166
+
167
+ expect(FileService.findOneBy).not.toHaveBeenCalled();
168
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
169
+ });
170
+
171
+ it("refuses a public row that carries no bytes", async () => {
172
+ const file: File = buildFile(true);
173
+ delete (file as Partial<File>).file;
174
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
175
+ file as never,
176
+ );
177
+
178
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
179
+
180
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
181
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
182
+ });
183
+
184
+ it("refuses a public row that carries no fileType", async () => {
185
+ const file: File = buildFile(true);
186
+ delete (file as Partial<File>).fileType;
187
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
188
+ file as never,
189
+ );
190
+
191
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
192
+
193
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
194
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
195
+ });
196
+ });
197
+
198
+ describe("token route — authenticated callers", () => {
199
+ it("serves a private file to a valid session", async () => {
200
+ setAuthenticated(true);
201
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
202
+ buildFile(false) as never,
203
+ );
204
+
205
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
206
+
207
+ expect(Response.sendFileResponse).toHaveBeenCalled();
208
+ expect(Response.sendErrorResponse).not.toHaveBeenCalled();
209
+ });
210
+
211
+ it("refuses a private file when the session token has no userId", async () => {
212
+ (
213
+ UserMiddleware.getAccessTokenFromExpressRequest as unknown as jest.Mock
214
+ ).mockReturnValue("a-token");
215
+ (JSONWebToken.decode as unknown as jest.Mock).mockReturnValue({});
216
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
217
+ buildFile(false) as never,
218
+ );
219
+
220
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
221
+
222
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
223
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
224
+ });
225
+
226
+ it("refuses a private file when the session token fails verification", async () => {
227
+ (
228
+ UserMiddleware.getAccessTokenFromExpressRequest as unknown as jest.Mock
229
+ ).mockReturnValue("a-forged-token");
230
+ (JSONWebToken.decode as unknown as jest.Mock).mockImplementation(() => {
231
+ throw new Error("invalid signature");
232
+ });
233
+ (FileService.findOneBy as unknown as jest.Mock).mockResolvedValue(
234
+ buildFile(false) as never,
235
+ );
236
+
237
+ await callRoute(TOKEN_ROUTE, { token: "abc123" });
238
+
239
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
240
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
241
+ });
242
+ });
243
+
244
+ describe("legacy id route", () => {
245
+ it("serves a public file", async () => {
246
+ (FileService.findOneById as unknown as jest.Mock).mockResolvedValue(
247
+ buildFile(true) as never,
248
+ );
249
+
250
+ await callRoute(ID_ROUTE, {
251
+ imageId: "e7c4f2a1-0000-4000-8000-000000000000",
252
+ });
253
+
254
+ expect(Response.sendFileResponse).toHaveBeenCalled();
255
+ expect(Response.sendErrorResponse).not.toHaveBeenCalled();
256
+ });
257
+
258
+ it("refuses a private file", async () => {
259
+ (FileService.findOneById as unknown as jest.Mock).mockResolvedValue(
260
+ buildFile(false) as never,
261
+ );
262
+
263
+ await callRoute(ID_ROUTE, {
264
+ imageId: "e7c4f2a1-0000-4000-8000-000000000000",
265
+ });
266
+
267
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
268
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
269
+ });
270
+
271
+ it("refuses a file whose isPublic is the string 'false'", async () => {
272
+ (FileService.findOneById as unknown as jest.Mock).mockResolvedValue(
273
+ buildFile("false") as never,
274
+ );
275
+
276
+ await callRoute(ID_ROUTE, {
277
+ imageId: "e7c4f2a1-0000-4000-8000-000000000000",
278
+ });
279
+
280
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
281
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
282
+ });
283
+
284
+ /*
285
+ * This route is public-only by design — it addresses files by an
286
+ * enumerable ObjectID, so a session is deliberately NOT enough to reach a
287
+ * private file through it. Private inline images use the token route.
288
+ */
289
+ it("refuses a private file even to a valid session", async () => {
290
+ setAuthenticated(true);
291
+ (FileService.findOneById as unknown as jest.Mock).mockResolvedValue(
292
+ buildFile(false) as never,
293
+ );
294
+
295
+ await callRoute(ID_ROUTE, {
296
+ imageId: "e7c4f2a1-0000-4000-8000-000000000000",
297
+ });
298
+
299
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
300
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
301
+ });
302
+
303
+ it("refuses when no file matches the id", async () => {
304
+ (FileService.findOneById as unknown as jest.Mock).mockResolvedValue(
305
+ null as never,
306
+ );
307
+
308
+ await callRoute(ID_ROUTE, {
309
+ imageId: "e7c4f2a1-0000-4000-8000-000000000000",
310
+ });
311
+
312
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
313
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
314
+ });
315
+ });
316
+ });
@@ -1,4 +1,6 @@
1
- import ProjectAPI from "../../../Server/API/ProjectAPI";
1
+ import ProjectAPI, {
2
+ MAX_DELETION_REASON_LENGTH,
3
+ } from "../../../Server/API/ProjectAPI";
2
4
  import MasterAdminAuthorization from "../../../Server/Middleware/MasterAdminAuthorization";
3
5
  import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
4
6
  import JSONWebToken from "../../../Server/Utils/JsonWebToken";
@@ -487,4 +489,300 @@ describe("ProjectAPI", () => {
487
489
  expect(Response.sendEmptySuccessResponse).not.toHaveBeenCalled();
488
490
  });
489
491
  });
492
+
493
+ /*
494
+ * The delete confirmation in the dashboard asks why the customer is leaving,
495
+ * and a DELETE has nowhere to put the answer. This route is the same delete
496
+ * with a body - so the interesting cases are the ones where the body could
497
+ * be used to reach a project the caller is not authenticated for, or to
498
+ * store something the column was never sized for.
499
+ */
500
+ describe("POST /project/:id/delete-project", () => {
501
+ const tenantMismatchError: BadDataException = new BadDataException(
502
+ "Project ID in the URL does not match the project the request is authenticated for",
503
+ );
504
+
505
+ beforeEach(() => {
506
+ ProjectService.deleteOneById = jest.fn().mockResolvedValue(1);
507
+ /*
508
+ * getDatabaseCommonInteractionProps asks for the plan of the
509
+ * authenticated tenant when billing is on.
510
+ */
511
+ ProjectService.getCurrentPlan = jest.fn().mockResolvedValue({
512
+ plan: null,
513
+ isSubscriptionUnpaid: false,
514
+ });
515
+ mockRequest.headers = {};
516
+
517
+ /*
518
+ * Response is module-mocked once for the whole file, so its call log
519
+ * carries over from the suites above.
520
+ */
521
+ (Response.sendEmptySuccessResponse as jest.Mock).mockClear();
522
+ });
523
+
524
+ it("deletes the authenticated tenant's own project", async () => {
525
+ const projectId: ObjectID = ObjectID.generate();
526
+
527
+ mockRequest.params = { id: projectId.toString() };
528
+ mockRequest.tenantId = projectId;
529
+ mockRequest.body = { data: { deletionReason: "We built our own" } };
530
+
531
+ await mockRouter
532
+ .match("post", "/project/:id/delete-project")
533
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
534
+
535
+ expect(nextFunction).not.toHaveBeenCalled();
536
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
537
+ expect.objectContaining({
538
+ id: projectId,
539
+ deletionReason: "We built our own",
540
+ }),
541
+ );
542
+ expect(Response.sendEmptySuccessResponse).toHaveBeenCalledWith(
543
+ mockRequest,
544
+ mockResponse,
545
+ );
546
+ });
547
+
548
+ /*
549
+ * Permissions are resolved for the authenticated tenant, so without this
550
+ * guard being an owner of one project would authorize deleting another.
551
+ */
552
+ it("refuses to delete a project the request is not authenticated for", async () => {
553
+ const victimProjectId: ObjectID = ObjectID.generate();
554
+ const attackerProjectId: ObjectID = ObjectID.generate();
555
+
556
+ mockRequest.params = { id: victimProjectId.toString() };
557
+ mockRequest.tenantId = attackerProjectId;
558
+ mockRequest.body = { data: { deletionReason: "not mine to delete" } };
559
+
560
+ await mockRouter
561
+ .match("post", "/project/:id/delete-project")
562
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
563
+
564
+ expect(nextFunction).toHaveBeenCalledWith(tenantMismatchError);
565
+ expect(ProjectService.deleteOneById).not.toHaveBeenCalled();
566
+ });
567
+
568
+ it("refuses when the request has no authenticated tenant", async () => {
569
+ const projectId: ObjectID = ObjectID.generate();
570
+
571
+ mockRequest.params = { id: projectId.toString() };
572
+ delete mockRequest.tenantId;
573
+ mockRequest.body = { data: { deletionReason: "anything" } };
574
+
575
+ await mockRouter
576
+ .match("post", "/project/:id/delete-project")
577
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
578
+
579
+ expect(nextFunction).toHaveBeenCalledWith(tenantMismatchError);
580
+ expect(ProjectService.deleteOneById).not.toHaveBeenCalled();
581
+ });
582
+
583
+ it("rejects an id that is not a project id", async () => {
584
+ mockRequest.params = { id: "not-a-uuid" };
585
+ mockRequest.tenantId = ObjectID.generate();
586
+ mockRequest.body = { data: {} };
587
+
588
+ await mockRouter
589
+ .match("post", "/project/:id/delete-project")
590
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
591
+
592
+ expect(nextFunction).toHaveBeenCalled();
593
+ expect(ProjectService.deleteOneById).not.toHaveBeenCalled();
594
+ });
595
+
596
+ /*
597
+ * The reason is optional: a customer who does not want to answer must
598
+ * still be able to delete their project.
599
+ */
600
+ it("deletes without a reason when none was given", async () => {
601
+ const projectId: ObjectID = ObjectID.generate();
602
+
603
+ mockRequest.params = { id: projectId.toString() };
604
+ mockRequest.tenantId = projectId;
605
+ mockRequest.body = {};
606
+
607
+ await mockRouter
608
+ .match("post", "/project/:id/delete-project")
609
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
610
+
611
+ expect(nextFunction).not.toHaveBeenCalled();
612
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
613
+ expect.objectContaining({
614
+ id: projectId,
615
+ deletionReason: undefined,
616
+ }),
617
+ );
618
+ });
619
+
620
+ it("treats a blank reason as no reason", async () => {
621
+ const projectId: ObjectID = ObjectID.generate();
622
+
623
+ mockRequest.params = { id: projectId.toString() };
624
+ mockRequest.tenantId = projectId;
625
+ mockRequest.body = { data: { deletionReason: " " } };
626
+
627
+ await mockRouter
628
+ .match("post", "/project/:id/delete-project")
629
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
630
+
631
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
632
+ expect.objectContaining({ deletionReason: undefined }),
633
+ );
634
+ });
635
+
636
+ it("ignores a reason that is not text", async () => {
637
+ const projectId: ObjectID = ObjectID.generate();
638
+
639
+ mockRequest.params = { id: projectId.toString() };
640
+ mockRequest.tenantId = projectId;
641
+ mockRequest.body = { data: { deletionReason: { $ne: null } } };
642
+
643
+ await mockRouter
644
+ .match("post", "/project/:id/delete-project")
645
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
646
+
647
+ expect(nextFunction).not.toHaveBeenCalled();
648
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
649
+ expect.objectContaining({ deletionReason: undefined }),
650
+ );
651
+ });
652
+
653
+ /*
654
+ * Postgres rejects NUL bytes in a text column. The insert that would fail
655
+ * is the audit record of a project that has already been deleted, and
656
+ * there is no second chance at writing it - so they are dropped here.
657
+ */
658
+ it("strips NUL bytes the database would reject", async () => {
659
+ const projectId: ObjectID = ObjectID.generate();
660
+
661
+ mockRequest.params = { id: projectId.toString() };
662
+ mockRequest.tenantId = projectId;
663
+ mockRequest.body = {
664
+ data: { deletionReason: "too\u0000 expensive\u0000" },
665
+ };
666
+
667
+ await mockRouter
668
+ .match("post", "/project/:id/delete-project")
669
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
670
+
671
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
672
+ expect.objectContaining({ deletionReason: "too expensive" }),
673
+ );
674
+ });
675
+
676
+ it("treats a reason that is only NUL bytes as no reason", async () => {
677
+ const projectId: ObjectID = ObjectID.generate();
678
+
679
+ mockRequest.params = { id: projectId.toString() };
680
+ mockRequest.tenantId = projectId;
681
+ mockRequest.body = { data: { deletionReason: "\u0000\u0000" } };
682
+
683
+ await mockRouter
684
+ .match("post", "/project/:id/delete-project")
685
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
686
+
687
+ expect(nextFunction).not.toHaveBeenCalled();
688
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
689
+ expect.objectContaining({ deletionReason: undefined }),
690
+ );
691
+ });
692
+
693
+ it("trims the reason before storing it", async () => {
694
+ const projectId: ObjectID = ObjectID.generate();
695
+
696
+ mockRequest.params = { id: projectId.toString() };
697
+ mockRequest.tenantId = projectId;
698
+ mockRequest.body = { data: { deletionReason: " too expensive " } };
699
+
700
+ await mockRouter
701
+ .match("post", "/project/:id/delete-project")
702
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
703
+
704
+ expect(ProjectService.deleteOneById).toHaveBeenCalledWith(
705
+ expect.objectContaining({ deletionReason: "too expensive" }),
706
+ );
707
+ });
708
+
709
+ /*
710
+ * The column is unbounded text and the field is free text from a browser,
711
+ * so the only thing standing between a client and an arbitrarily large
712
+ * row is this cap.
713
+ */
714
+ it("caps how much free text a client can store", async () => {
715
+ const projectId: ObjectID = ObjectID.generate();
716
+
717
+ mockRequest.params = { id: projectId.toString() };
718
+ mockRequest.tenantId = projectId;
719
+ mockRequest.body = {
720
+ data: { deletionReason: "a".repeat(MAX_DELETION_REASON_LENGTH + 500) },
721
+ };
722
+
723
+ await mockRouter
724
+ .match("post", "/project/:id/delete-project")
725
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
726
+
727
+ const call: { deletionReason?: string } = (
728
+ ProjectService.deleteOneById as unknown as {
729
+ mock: { calls: Array<Array<{ deletionReason?: string }>> };
730
+ }
731
+ ).mock.calls[0]![0]!;
732
+
733
+ expect(call.deletionReason).toHaveLength(MAX_DELETION_REASON_LENGTH);
734
+ });
735
+
736
+ /*
737
+ * The delete goes through the ordinary permission-checked path with the
738
+ * caller's own props - the reason must not turn this into a privileged
739
+ * delete.
740
+ */
741
+ it("deletes as the caller, not as root", async () => {
742
+ const projectId: ObjectID = ObjectID.generate();
743
+ const userId: ObjectID = ObjectID.generate();
744
+
745
+ mockRequest.params = { id: projectId.toString() };
746
+ mockRequest.tenantId = projectId;
747
+ mockRequest.userAuthorization = { userId: userId } as JSONWebTokenData;
748
+ mockRequest.body = { data: { deletionReason: "done with it" } };
749
+
750
+ await mockRouter
751
+ .match("post", "/project/:id/delete-project")
752
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
753
+
754
+ const call: { props: { isRoot?: boolean; userId?: ObjectID } } = (
755
+ ProjectService.deleteOneById as unknown as {
756
+ mock: {
757
+ calls: Array<
758
+ Array<{ props: { isRoot?: boolean; userId?: ObjectID } }>
759
+ >;
760
+ };
761
+ }
762
+ ).mock.calls[0]![0]!;
763
+
764
+ expect(call.props.isRoot).toBeFalsy();
765
+ expect(call.props.userId).toBe(userId);
766
+ });
767
+
768
+ it("forwards a refused delete to next instead of reporting success", async () => {
769
+ const projectId: ObjectID = ObjectID.generate();
770
+ const notAllowed: BadDataException = new BadDataException(
771
+ "Delete on Project is not allowed",
772
+ );
773
+
774
+ ProjectService.deleteOneById = jest.fn().mockRejectedValue(notAllowed);
775
+
776
+ mockRequest.params = { id: projectId.toString() };
777
+ mockRequest.tenantId = projectId;
778
+ mockRequest.body = { data: { deletionReason: "nope" } };
779
+
780
+ await mockRouter
781
+ .match("post", "/project/:id/delete-project")
782
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
783
+
784
+ expect(nextFunction).toHaveBeenCalledWith(notAllowed);
785
+ expect(Response.sendEmptySuccessResponse).not.toHaveBeenCalled();
786
+ });
787
+ });
490
788
  });