@oneuptime/common 12.0.18 → 12.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  11. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  12. package/Server/API/UserSlackAPI.ts +125 -0
  13. package/Server/EnvironmentConfig.ts +29 -107
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  19. package/Server/Infrastructure/Queue.ts +7 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +171 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Response.ts +2 -2
  40. package/Server/Utils/StartServer.ts +2 -2
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  42. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  43. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  44. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  45. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  46. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  47. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  48. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  49. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  50. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  51. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  52. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  53. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  54. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  55. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  56. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  57. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  58. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  59. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  60. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  61. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  62. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  63. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  64. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  65. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  66. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  67. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  68. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  69. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  70. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  71. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  72. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  73. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  74. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  75. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  76. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  77. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  78. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  79. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  80. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  81. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  82. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  83. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  84. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  85. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  86. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  87. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  88. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  89. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  90. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  91. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  92. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  93. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  94. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  95. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  97. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  98. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  99. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  100. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  101. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  102. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  103. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  104. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  105. package/Types/Analytics/RevenueEvent.ts +1 -0
  106. package/Types/Marketing/MarketingEvent.ts +140 -0
  107. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  108. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  109. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  110. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  111. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  112. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  113. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  114. package/UI/Components/Detail/Detail.tsx +18 -1
  115. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  116. package/UI/Utils/PermissionGate.ts +56 -0
  117. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  118. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  119. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  121. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  123. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  125. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  126. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  127. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  129. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  131. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  133. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  134. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  135. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  136. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  137. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  138. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  139. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  140. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  141. package/build/dist/Server/EnvironmentConfig.js +25 -70
  142. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  152. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  153. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  154. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  155. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  156. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  157. package/build/dist/Server/Services/Index.js +6 -2
  158. package/build/dist/Server/Services/Index.js.map +1 -1
  159. package/build/dist/Server/Services/LlmLogService.js +25 -9
  160. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  161. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  162. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  163. package/build/dist/Server/Services/ProjectService.js +144 -0
  164. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  165. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  166. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  167. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  168. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  169. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  170. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  171. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  172. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  173. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  174. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  175. package/build/dist/Server/Services/UserService.js +26 -1
  176. package/build/dist/Server/Services/UserService.js.map +1 -1
  177. package/build/dist/Server/Services/UserSlackService.js +195 -0
  178. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  179. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  180. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  181. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  182. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  183. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  184. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  185. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  186. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  187. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  188. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  190. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  192. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  193. package/build/dist/Server/Utils/Response.js +2 -2
  194. package/build/dist/Server/Utils/Response.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +2 -2
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  198. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  199. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  200. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  201. package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
  202. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  203. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  204. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  205. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  206. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  207. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  208. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  209. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  210. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  211. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  212. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  213. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  214. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  215. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  216. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  217. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  218. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  219. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  220. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  221. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  222. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  223. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  224. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  225. package/package.json +1 -1
  226. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  227. package/Server/Services/MarketingConversionService.ts +0 -10
  228. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  229. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  230. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  231. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  232. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  233. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  234. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  235. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  236. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  237. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  238. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  239. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  240. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  241. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  242. package/Types/Marketing/MarketingConversion.ts +0 -53
  243. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  244. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  245. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  246. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  247. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  248. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  249. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  250. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  251. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  252. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  253. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  254. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  255. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  256. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  257. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  258. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  259. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  260. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  261. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  262. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,300 @@
1
+ /*
2
+ * https://github.com/OneUptime/oneuptime/issues/3360
3
+ *
4
+ * MonitorLogUtil is the widest-reach persistence sink in the monitoring path:
5
+ * every monitor type, every evaluation, straight into `MonitorLog.logBody` --
6
+ * a column whose read ACL includes `Permission.Viewer`. Whatever
7
+ * `saveMonitorLog` is handed is what a read-only API key can select back out.
8
+ *
9
+ * The unit-level behaviour of the redactor is pinned in
10
+ * MonitorPayloadRedaction.test.ts. What THIS suite pins is the wiring: that
11
+ * the row actually enqueued for ClickHouse is the redacted one, that the
12
+ * caller's live `dataToProcess` is not collaterally damaged on the way, and
13
+ * that the rest of the row (ids, timestamps, retention) is unchanged -- so the
14
+ * fix cannot be quietly reverted by someone reinstating the raw
15
+ * `JSON.parse(JSON.stringify(...))`.
16
+ *
17
+ * Flushing: the buffer drains on a 5s timer or at 10,000 rows, neither of
18
+ * which a test should wait for. Rather than reach into a private, the suite
19
+ * drives the flush through MonitorLogUtil's own PUBLIC contract -- the
20
+ * GracefulShutdown handler it registers, which awaits `flushAndWait()`. The
21
+ * mock below captures that callback so a test can invoke it directly.
22
+ */
23
+
24
+ const mockShutdown: { callback: (() => Promise<void> | void) | null } = {
25
+ callback: null,
26
+ };
27
+
28
+ jest.mock("../../../../Server/Utils/GracefulShutdown", () => {
29
+ return {
30
+ __esModule: true,
31
+ default: {
32
+ registerHandler: (
33
+ _name: string,
34
+ _priority: number,
35
+ callback: () => Promise<void> | void,
36
+ ): void => {
37
+ mockShutdown.callback = callback;
38
+ },
39
+ },
40
+ ShutdownPriority: {
41
+ HttpServer: 10,
42
+ Workers: 20,
43
+ Buffers: 30,
44
+ DataStores: 40,
45
+ Telemetry: 50,
46
+ },
47
+ };
48
+ });
49
+
50
+ jest.mock("../../../../Server/Services/MonitorLogService", () => {
51
+ return {
52
+ __esModule: true,
53
+ default: {
54
+ insertJsonRows: jest.fn(() => {
55
+ return Promise.resolve();
56
+ }),
57
+ },
58
+ };
59
+ });
60
+
61
+ jest.mock("../../../../Server/Services/GlobalConfigService", () => {
62
+ return {
63
+ __esModule: true,
64
+ default: {
65
+ findOneBy: jest.fn(() => {
66
+ return Promise.resolve({ monitorLogRetentionInDays: 7 });
67
+ }),
68
+ },
69
+ };
70
+ });
71
+
72
+ jest.mock("../../../../Server/Utils/Logger", () => {
73
+ return {
74
+ __esModule: true,
75
+ default: {
76
+ debug: jest.fn(),
77
+ info: jest.fn(),
78
+ warn: jest.fn(),
79
+ error: jest.fn(),
80
+ trace: jest.fn(),
81
+ },
82
+ };
83
+ });
84
+
85
+ import MonitorLogUtil from "../../../../Server/Utils/Monitor/MonitorLogUtil";
86
+ import MonitorLogService from "../../../../Server/Services/MonitorLogService";
87
+ import DataToProcess from "../../../../Server/Utils/Monitor/DataToProcess";
88
+ import { REDACTED } from "../../../../Server/Utils/LogRedaction";
89
+ import { JSONObject } from "../../../../Types/JSON";
90
+ import ObjectID from "../../../../Types/ObjectID";
91
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
92
+
93
+ const SECRET: string = "b1946ac9-2492-4b0f-9b2f-ee9b6cbe36ba";
94
+
95
+ const MONITOR_ID: ObjectID = new ObjectID(
96
+ "8f14e45f-ceea-467a-9575-1b0d0d3e7a9c",
97
+ );
98
+ const PROJECT_ID: ObjectID = new ObjectID(
99
+ "3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19",
100
+ );
101
+
102
+ /*
103
+ * Loosely typed on purpose: the module factory above returns an untyped
104
+ * jest.fn(), and pinning a signature here only fights ts-jest without making
105
+ * the assertions any stronger.
106
+ */
107
+ const insertJsonRows: jest.Mock =
108
+ MonitorLogService.insertJsonRows as unknown as jest.Mock;
109
+
110
+ type SaveAndFlushFunction = (
111
+ dataToProcess: DataToProcess,
112
+ ) => Promise<JSONObject>;
113
+
114
+ /*
115
+ * Run one payload all the way through to the row handed to ClickHouse.
116
+ *
117
+ * `saveMonitorLog` is deliberately fire-and-forget (the monitor hot path must
118
+ * not block on a retention lookup), so the promise chain has to be given a
119
+ * turn of the event loop before the row exists in the buffer at all.
120
+ */
121
+ const saveAndFlush: SaveAndFlushFunction = async (
122
+ dataToProcess: DataToProcess,
123
+ ): Promise<JSONObject> => {
124
+ MonitorLogUtil.saveMonitorLog({
125
+ monitorId: MONITOR_ID,
126
+ projectId: PROJECT_ID,
127
+ dataToProcess: dataToProcess,
128
+ });
129
+
130
+ await new Promise<void>((resolve: () => void) => {
131
+ setTimeout(resolve, 0);
132
+ });
133
+
134
+ if (!mockShutdown.callback) {
135
+ throw new Error(
136
+ "MonitorLogUtil did not register its shutdown flush handler",
137
+ );
138
+ }
139
+
140
+ await mockShutdown.callback();
141
+
142
+ const calls: Array<Array<unknown>> = insertJsonRows.mock
143
+ .calls as unknown as Array<Array<unknown>>;
144
+
145
+ expect(calls.length).toBeGreaterThan(0);
146
+
147
+ const rows: Array<JSONObject> = calls[
148
+ calls.length - 1
149
+ ]![0] as Array<JSONObject>;
150
+
151
+ expect(rows).toHaveLength(1);
152
+
153
+ return rows[0]!;
154
+ };
155
+
156
+ type ServerBeatFunction = () => JSONObject;
157
+
158
+ // The payload the Go agent sends, with the two fields MonitorResource stamps on.
159
+ const serverBeat: ServerBeatFunction = (): JSONObject => {
160
+ return {
161
+ secretKey: SECRET,
162
+ hostname: "web-01.internal",
163
+ monitorId: MONITOR_ID,
164
+ projectId: PROJECT_ID,
165
+ requestReceivedAt: new Date("2026-08-23T10:00:00.000Z"),
166
+ onlyCheckRequestReceivedAt: false,
167
+ basicInfrastructureMetrics: {
168
+ cpuMetrics: { percentUsed: 12.5, cores: 8 },
169
+ memoryMetrics: { total: 16_777_216, percentUsed: 75 },
170
+ },
171
+ processes: [{ pid: 42, name: "node", command: "node index.js" }],
172
+ };
173
+ };
174
+
175
+ describe("MonitorLogUtil.saveMonitorLog", () => {
176
+ beforeEach(() => {
177
+ insertJsonRows.mockClear();
178
+ });
179
+
180
+ it("does not write the server-agent secret into logBody", async () => {
181
+ const row: JSONObject = await saveAndFlush(
182
+ serverBeat() as unknown as DataToProcess,
183
+ );
184
+
185
+ const logBody: JSONObject = row["logBody"] as JSONObject;
186
+
187
+ expect(logBody["secretKey"]).toBe(REDACTED);
188
+ expect(JSON.stringify(row)).not.toContain(SECRET);
189
+ });
190
+
191
+ it("still writes everything the monitor is actually for", async () => {
192
+ const row: JSONObject = await saveAndFlush(
193
+ serverBeat() as unknown as DataToProcess,
194
+ );
195
+
196
+ const logBody: JSONObject = row["logBody"] as JSONObject;
197
+
198
+ expect(logBody["hostname"]).toBe("web-01.internal");
199
+ expect(logBody["onlyCheckRequestReceivedAt"]).toBe(false);
200
+ expect(
201
+ (logBody["basicInfrastructureMetrics"] as JSONObject)["cpuMetrics"],
202
+ ).toEqual({ percentUsed: 12.5, cores: 8 });
203
+ expect(logBody["processes"]).toEqual([
204
+ { pid: 42, name: "node", command: "node index.js" },
205
+ ]);
206
+ });
207
+
208
+ it("leaves the caller's live payload untouched", async () => {
209
+ /*
210
+ * saveMonitorLog returns immediately and MonitorResource keeps evaluating
211
+ * criteria against the same object afterwards. Redacting in place would
212
+ * change the monitor's verdict, not just its stored log -- so the clone,
213
+ * not the original, must be what gets masked.
214
+ */
215
+ const live: JSONObject = serverBeat();
216
+
217
+ await saveAndFlush(live as unknown as DataToProcess);
218
+
219
+ expect(live["secretKey"]).toBe(SECRET);
220
+ expect(live["hostname"]).toBe("web-01.internal");
221
+ });
222
+
223
+ it("keeps the rest of the row intact", async () => {
224
+ /*
225
+ * Redaction must not disturb the columns ClickHouse partitions, sorts and
226
+ * expires on. A row that loses its retentionDate never gets TTL'd.
227
+ */
228
+ const row: JSONObject = await saveAndFlush(
229
+ serverBeat() as unknown as DataToProcess,
230
+ );
231
+
232
+ expect(row["monitorId"]).toBe(MONITOR_ID.toString());
233
+ expect(row["projectId"]).toBe(PROJECT_ID.toString());
234
+ expect(typeof row["_id"]).toBe("string");
235
+ expect(typeof row["time"]).toBe("string");
236
+ expect(typeof row["createdAt"]).toBe("string");
237
+ expect(typeof row["retentionDate"]).toBe("string");
238
+ });
239
+
240
+ it("masks caller-supplied auth headers on an incoming-request monitor", async () => {
241
+ /*
242
+ * The other half of the issue: an incoming-request monitor stores the
243
+ * headers and body of whatever called it, so a caller's bearer token was
244
+ * landing in Viewer-readable logBody as well.
245
+ */
246
+ const row: JSONObject = await saveAndFlush({
247
+ monitorId: MONITOR_ID,
248
+ projectId: PROJECT_ID,
249
+ incomingRequestReceivedAt: new Date("2026-08-23T10:00:00.000Z"),
250
+ checkedAt: new Date("2026-08-23T10:00:01.000Z"),
251
+ requestHeaders: {
252
+ authorization: `Bearer ${SECRET}`,
253
+ "content-type": "application/json",
254
+ },
255
+ requestBody: { status: "ok" },
256
+ } as unknown as DataToProcess);
257
+
258
+ const logBody: JSONObject = row["logBody"] as JSONObject;
259
+ const headers: JSONObject = logBody["requestHeaders"] as JSONObject;
260
+
261
+ expect(headers["authorization"]).toBe(REDACTED);
262
+ expect(headers["content-type"]).toBe("application/json");
263
+ expect((logBody["requestBody"] as JSONObject)["status"]).toBe("ok");
264
+ expect(JSON.stringify(row)).not.toContain(SECRET);
265
+ });
266
+
267
+ it("writes nothing at all when required ids are missing", async () => {
268
+ /*
269
+ * Pins the existing guards, which the redaction change sits directly on
270
+ * top of: a row with no monitorId cannot be scoped or TTL'd.
271
+ */
272
+ MonitorLogUtil.saveMonitorLog({
273
+ monitorId: undefined as unknown as ObjectID,
274
+ projectId: PROJECT_ID,
275
+ dataToProcess: serverBeat() as unknown as DataToProcess,
276
+ });
277
+
278
+ MonitorLogUtil.saveMonitorLog({
279
+ monitorId: MONITOR_ID,
280
+ projectId: undefined as unknown as ObjectID,
281
+ dataToProcess: serverBeat() as unknown as DataToProcess,
282
+ });
283
+
284
+ MonitorLogUtil.saveMonitorLog({
285
+ monitorId: MONITOR_ID,
286
+ projectId: PROJECT_ID,
287
+ dataToProcess: undefined as unknown as DataToProcess,
288
+ });
289
+
290
+ await new Promise<void>((resolve: () => void) => {
291
+ setTimeout(resolve, 0);
292
+ });
293
+
294
+ if (mockShutdown.callback) {
295
+ await mockShutdown.callback();
296
+ }
297
+
298
+ expect(insertJsonRows).not.toHaveBeenCalled();
299
+ });
300
+ });
@@ -0,0 +1,381 @@
1
+ import {
2
+ redactForPersistence,
3
+ stripAgentCredentials,
4
+ } from "../../../../Server/Utils/Monitor/MonitorPayloadRedaction";
5
+ import { REDACTED } from "../../../../Server/Utils/LogRedaction";
6
+ import { JSONObject } from "../../../../Types/JSON";
7
+ import { describe, expect, it } from "@jest/globals";
8
+
9
+ /*
10
+ * Regression tests for https://github.com/OneUptime/oneuptime/issues/3360.
11
+ *
12
+ * The infrastructure agent authenticates with the monitor's
13
+ * `serverMonitorSecretKey` and sends that key in the request BODY as well as
14
+ * the URL (InfrastructureAgent/model/server_monitor_report.go:
15
+ * `SecretKey string \`json:"secretKey"\``). The body became `dataToProcess`,
16
+ * and `dataToProcess` was written verbatim into three columns that
17
+ * `Permission.Viewer` -- the least privilege OneUptime grants -- can select:
18
+ * `MonitorLog.logBody`, `Monitor.serverMonitorResponse` and
19
+ * `MonitorProbe.lastMonitoringLog`.
20
+ *
21
+ * The property under test is therefore not "the redactor works" in the
22
+ * abstract. It is: given the payload the real agent actually sends, the secret
23
+ * does not appear ANYWHERE in the output, in any spelling, at any depth --
24
+ * while every legitimate monitor observation survives untouched, because a
25
+ * redactor that eats the metrics is a redactor somebody will turn off.
26
+ */
27
+
28
+ // A value distinctive enough that a substring search for it is meaningful.
29
+ const SECRET: string = "b1946ac9-2492-4b0f-9b2f-ee9b6cbe36ba";
30
+
31
+ type AgentPayloadFunction = () => JSONObject;
32
+
33
+ /*
34
+ * The real shape, key-for-key, that the Go agent marshals and
35
+ * ProcessServerMonitorIngest deserializes -- plus the two fields
36
+ * MonitorResource stamps on before anything is persisted (`projectId`,
37
+ * `evaluationSummary`). Reproduced faithfully because a redactor tested only
38
+ * against a toy `{secretKey: "x"}` proves nothing about the payload that
39
+ * actually leaked.
40
+ */
41
+ const agentPayload: AgentPayloadFunction = (): JSONObject => {
42
+ return {
43
+ secretKey: SECRET,
44
+ basicInfrastructureMetrics: {
45
+ memoryMetrics: {
46
+ total: 16_777_216,
47
+ free: 4_194_304,
48
+ used: 12_582_912,
49
+ percentUsed: 75,
50
+ percentFree: 25,
51
+ cached: 1_048_576,
52
+ swapTotal: 0,
53
+ },
54
+ cpuMetrics: {
55
+ percentUsed: 12.5,
56
+ cores: 8,
57
+ perCorePercent: [10, 15, 12, 11, 14, 13, 12, 13],
58
+ },
59
+ diskMetrics: [{ diskPath: "/", total: 500, free: 200, percentUsed: 60 }],
60
+ },
61
+ requestReceivedAt: "2026-08-23T10:00:00.000Z",
62
+ onlyCheckRequestReceivedAt: false,
63
+ processes: [
64
+ { pid: 1, name: "systemd", command: "/sbin/init", cpuPercent: 0.1 },
65
+ { pid: 42, name: "node", command: "node index.js", cpuPercent: 3.2 },
66
+ ],
67
+ hostname: "web-01.internal",
68
+ monitorId: "8f14e45f-ceea-467a-9575-1b0d0d3e7a9c",
69
+ timeNow: "2026-08-23T10:00:01.000Z",
70
+ evaluationSummary: {
71
+ evaluatedAt: "2026-08-23T10:00:01.000Z",
72
+ criteriaResults: [],
73
+ events: [],
74
+ },
75
+ projectId: "3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19",
76
+ };
77
+ };
78
+
79
+ type ContainsSecretFunction = (value: unknown) => boolean;
80
+
81
+ /*
82
+ * The assertion that actually matters. Checking `output.secretKey` alone would
83
+ * pass for an implementation that merely moved the value one level down, so
84
+ * every test asserts on the serialized whole.
85
+ */
86
+ const containsSecret: ContainsSecretFunction = (value: unknown): boolean => {
87
+ return JSON.stringify(value)?.includes(SECRET) ?? false;
88
+ };
89
+
90
+ describe("stripAgentCredentials - the ingest boundary", () => {
91
+ it("removes the secret the agent puts in the request body", () => {
92
+ const output: JSONObject = stripAgentCredentials(agentPayload());
93
+
94
+ expect(output["secretKey"]).toBeUndefined();
95
+ expect("secretKey" in output).toBe(false);
96
+ expect(containsSecret(output)).toBe(false);
97
+ });
98
+
99
+ it("keeps every legitimate observation in the beat", () => {
100
+ const input: JSONObject = agentPayload();
101
+ const output: JSONObject = stripAgentCredentials(input);
102
+
103
+ /*
104
+ * The whole point of a server monitor. If redaction ever starts eating
105
+ * these, criteria stop evaluating and the feature is dead -- so pin the
106
+ * metrics block as deep-equal rather than spot-checking one field.
107
+ */
108
+ expect(output["basicInfrastructureMetrics"]).toEqual(
109
+ input["basicInfrastructureMetrics"],
110
+ );
111
+ expect(output["processes"]).toEqual(input["processes"]);
112
+ expect(output["hostname"]).toBe("web-01.internal");
113
+ expect(output["monitorId"]).toBe("8f14e45f-ceea-467a-9575-1b0d0d3e7a9c");
114
+ expect(output["projectId"]).toBe("3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19");
115
+ expect(output["onlyCheckRequestReceivedAt"]).toBe(false);
116
+ expect(output["requestReceivedAt"]).toBe("2026-08-23T10:00:00.000Z");
117
+ expect(output["evaluationSummary"]).toEqual(input["evaluationSummary"]);
118
+ });
119
+
120
+ it("drops the key rather than masking it, so the typed column stays faithful", () => {
121
+ /*
122
+ * `Monitor.serverMonitorResponse` is typed as ServerMonitorResponse, which
123
+ * declares no `secretKey`. Masking would persist a phantom field on a
124
+ * typed jsonb column; removing keeps the stored object a valid instance of
125
+ * its own interface.
126
+ */
127
+ const output: JSONObject = stripAgentCredentials(agentPayload());
128
+
129
+ expect(Object.keys(output)).not.toContain("secretKey");
130
+ expect(JSON.stringify(output)).not.toContain(REDACTED);
131
+ });
132
+
133
+ it("does not mutate the payload it was handed", () => {
134
+ /*
135
+ * The caller keeps using the input object. An in-place strip would be a
136
+ * different bug wearing the same fix: it would change what the monitor
137
+ * evaluates, not just what gets stored.
138
+ */
139
+ const input: JSONObject = agentPayload();
140
+
141
+ stripAgentCredentials(input);
142
+
143
+ expect(input["secretKey"]).toBe(SECRET);
144
+ });
145
+
146
+ it("returns a defensive copy, not the same object", () => {
147
+ const input: JSONObject = agentPayload();
148
+ const output: JSONObject = stripAgentCredentials(input);
149
+
150
+ expect(output).not.toBe(input);
151
+ expect(output["processes"]).not.toBe(input["processes"]);
152
+ });
153
+ });
154
+
155
+ describe("stripAgentCredentials - spellings and nesting", () => {
156
+ it("catches the secret under every spelling of the key", () => {
157
+ /*
158
+ * The Go struct tag is `secretKey` today. A future agent, a proxy that
159
+ * renames fields, or a different ingest path may spell it otherwise, and
160
+ * the classifier normalizes case and separators for exactly that reason.
161
+ */
162
+ for (const key of [
163
+ "secretKey",
164
+ "secret_key",
165
+ "SecretKey",
166
+ "SECRET_KEY",
167
+ "secret-key",
168
+ "monitorSecretKey",
169
+ "serverMonitorSecretKey",
170
+ ]) {
171
+ const output: JSONObject = stripAgentCredentials({
172
+ [key]: SECRET,
173
+ hostname: "web-01",
174
+ });
175
+
176
+ expect(containsSecret(output)).toBe(false);
177
+ expect(output["hostname"]).toBe("web-01");
178
+ }
179
+ });
180
+
181
+ it("finds a credential buried below the top level", () => {
182
+ const output: JSONObject = stripAgentCredentials({
183
+ hostname: "web-01",
184
+ nested: { deeper: { secretKey: SECRET, keep: "yes" } },
185
+ });
186
+
187
+ expect(containsSecret(output)).toBe(false);
188
+ expect(
189
+ ((output["nested"] as JSONObject)["deeper"] as JSONObject)["keep"],
190
+ ).toBe("yes");
191
+ });
192
+
193
+ it("finds a credential inside an array element", () => {
194
+ const output: JSONObject = stripAgentCredentials({
195
+ beats: [{ secretKey: SECRET }, { hostname: "web-02" }],
196
+ });
197
+
198
+ expect(containsSecret(output)).toBe(false);
199
+ expect(output["beats"] as Array<JSONObject>).toHaveLength(2);
200
+ expect((output["beats"] as Array<JSONObject>)[1]!["hostname"]).toBe(
201
+ "web-02",
202
+ );
203
+ });
204
+
205
+ it("handles null, undefined and empty payloads without throwing", () => {
206
+ expect(stripAgentCredentials(null)).toBeNull();
207
+ expect(stripAgentCredentials(undefined)).toBeUndefined();
208
+ expect(stripAgentCredentials({})).toEqual({});
209
+ expect(stripAgentCredentials({ a: null, b: undefined })).toEqual({
210
+ a: null,
211
+ b: undefined,
212
+ });
213
+ });
214
+
215
+ it("passes class instances through as leaves", () => {
216
+ /*
217
+ * The ingest boundary runs on JSONFunctions.deserialize output, which
218
+ * rehydrates Dates and ObjectIDs into live instances. Walking into them
219
+ * would rebuild them as plain objects and destroy the type.
220
+ */
221
+ const date: Date = new Date("2026-08-23T10:00:00.000Z");
222
+
223
+ const output: JSONObject = stripAgentCredentials({
224
+ requestReceivedAt: date,
225
+ secretKey: SECRET,
226
+ });
227
+
228
+ expect(output["requestReceivedAt"]).toBe(date);
229
+ expect(output["requestReceivedAt"] instanceof Date).toBe(true);
230
+ expect(containsSecret(output)).toBe(false);
231
+ });
232
+ });
233
+
234
+ describe("redactForPersistence - the logBody sink", () => {
235
+ it("masks the secret instead of dropping it", () => {
236
+ /*
237
+ * logBody is a diagnostic record, so the useful answer is "a credential
238
+ * was here" rather than silence. The value still must not survive.
239
+ */
240
+ const output: JSONObject = redactForPersistence(
241
+ agentPayload(),
242
+ ) as JSONObject;
243
+
244
+ expect(output["secretKey"]).toBe(REDACTED);
245
+ expect(containsSecret(output)).toBe(false);
246
+ });
247
+
248
+ it("masks the auth headers an incoming-request monitor records", () => {
249
+ /*
250
+ * The second half of the issue: IncomingMonitorRequest carries
251
+ * requestHeaders and requestBody straight from the caller, so whatever
252
+ * token the caller sent was landing in Viewer-readable logBody too.
253
+ */
254
+ const output: JSONObject = redactForPersistence({
255
+ requestHeaders: {
256
+ Authorization: `Bearer ${SECRET}`,
257
+ Cookie: `session=${SECRET}`,
258
+ "x-api-key": SECRET,
259
+ "content-type": "application/json",
260
+ "user-agent": "curl/8.0",
261
+ },
262
+ requestBody: { password: SECRET, orderId: "A-1001" },
263
+ incomingRequestReceivedAt: "2026-08-23T10:00:00.000Z",
264
+ }) as JSONObject;
265
+
266
+ const headers: JSONObject = output["requestHeaders"] as JSONObject;
267
+
268
+ expect(headers["Authorization"]).toBe(REDACTED);
269
+ expect(headers["Cookie"]).toBe(REDACTED);
270
+ expect(headers["x-api-key"]).toBe(REDACTED);
271
+
272
+ // Non-credential headers are exactly why masking beats dropping the block.
273
+ expect(headers["content-type"]).toBe("application/json");
274
+ expect(headers["user-agent"]).toBe("curl/8.0");
275
+
276
+ expect((output["requestBody"] as JSONObject)["password"]).toBe(REDACTED);
277
+ expect((output["requestBody"] as JSONObject)["orderId"]).toBe("A-1001");
278
+ expect(containsSecret(output)).toBe(false);
279
+ });
280
+
281
+ it("leaves probe observations that only look credential-shaped alone", () => {
282
+ /*
283
+ * Over-redaction is a real cost here: these are the fields monitor
284
+ * criteria are written against, and `code` in particular is classified by
285
+ * VALUE, so an error code must survive while an OAuth code must not.
286
+ */
287
+ const output: JSONObject = redactForPersistence({
288
+ responseCode: 200,
289
+ responseTimeInMs: 143,
290
+ requestFailedDetails: {
291
+ failedPhase: "TCP Connection",
292
+ errorCode: "ECONNREFUSED",
293
+ errorDescription: "Connection refused",
294
+ },
295
+ sslResponse: {
296
+ certificateValidationErrorCode: "CERT_HAS_EXPIRED",
297
+ fingerprint: "AA:BB:CC",
298
+ serialNumber: "0123456789",
299
+ commonName: "example.com",
300
+ },
301
+ customCodeMonitorResponse: {
302
+ executionTimeInMS: 12,
303
+ scriptError: undefined,
304
+ },
305
+ code: "ECONNREFUSED",
306
+ }) as JSONObject;
307
+
308
+ expect(output["responseCode"]).toBe(200);
309
+ expect(output["responseTimeInMs"]).toBe(143);
310
+ expect((output["requestFailedDetails"] as JSONObject)["errorCode"]).toBe(
311
+ "ECONNREFUSED",
312
+ );
313
+ expect(
314
+ (output["sslResponse"] as JSONObject)["certificateValidationErrorCode"],
315
+ ).toBe("CERT_HAS_EXPIRED");
316
+ expect((output["sslResponse"] as JSONObject)["commonName"]).toBe(
317
+ "example.com",
318
+ );
319
+ expect(
320
+ (output["customCodeMonitorResponse"] as JSONObject)["executionTimeInMS"],
321
+ ).toBe(12);
322
+ expect(output["code"]).toBe("ECONNREFUSED");
323
+ });
324
+
325
+ it("still redacts a code that is actually a credential", () => {
326
+ const output: JSONObject = redactForPersistence({
327
+ code: SECRET,
328
+ }) as JSONObject;
329
+
330
+ expect(output["code"]).toBe(REDACTED);
331
+ });
332
+
333
+ it("does not mutate the payload it was handed", () => {
334
+ const input: JSONObject = agentPayload();
335
+
336
+ redactForPersistence(input);
337
+
338
+ expect(input["secretKey"]).toBe(SECRET);
339
+ });
340
+
341
+ it("handles null and undefined without throwing", () => {
342
+ expect(redactForPersistence(null)).toBeNull();
343
+ expect(redactForPersistence(undefined as never)).toBeUndefined();
344
+ });
345
+
346
+ it("drops rather than passes through a subtree past the depth ceiling", () => {
347
+ /*
348
+ * A payload nested deeper than the walk will go is not something a monitor
349
+ * legitimately reports, and passing the tail through unredacted would be
350
+ * an trivially exploitable way to smuggle a secret past the walk.
351
+ */
352
+ let deep: JSONObject = { secretKey: SECRET };
353
+
354
+ for (let i: number = 0; i < 40; i++) {
355
+ deep = { nested: deep };
356
+ }
357
+
358
+ expect(containsSecret(redactForPersistence(deep))).toBe(false);
359
+ expect(containsSecret(stripAgentCredentials(deep))).toBe(false);
360
+ });
361
+ });
362
+
363
+ describe("the reproduction in the issue", () => {
364
+ it("no longer yields the monitor secret from a stored beat", () => {
365
+ /*
366
+ * Issue #3360, steps 1-4: register an agent, select logBody with a
367
+ * Viewer-only key, read logBody.secretKey, compare to
368
+ * Monitor.serverMonitorSecretKey. The comparison is what must now fail.
369
+ */
370
+ const stored: JSONObject = redactForPersistence(
371
+ stripAgentCredentials(agentPayload()),
372
+ ) as JSONObject;
373
+
374
+ expect(stored["secretKey"]).toBeUndefined();
375
+ expect(JSON.stringify(stored)).not.toContain(SECRET);
376
+
377
+ // And the beat is still a usable monitor observation.
378
+ expect(stored["hostname"]).toBe("web-01.internal");
379
+ expect(stored["basicInfrastructureMetrics"]).toBeDefined();
380
+ });
381
+ });