@oneuptime/common 12.0.17 → 12.0.19

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 (220) 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/UserMicrosoftTeamsAPI.ts +127 -0
  11. package/Server/API/UserSlackAPI.ts +125 -0
  12. package/Server/EnvironmentConfig.ts +12 -107
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Infrastructure/Queue.ts +7 -0
  19. package/Server/Services/BillingService.ts +8 -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 +67 -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/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  40. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  41. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  42. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  43. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  44. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
  45. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  46. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  47. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  48. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  49. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  50. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  51. package/Tests/Server/Services/BillingService.test.ts +20 -0
  52. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  53. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  54. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  55. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  56. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  57. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  58. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  59. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  60. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  61. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  62. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  63. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  64. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  65. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  66. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  67. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  68. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  69. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  70. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  71. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  72. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  73. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  74. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  75. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  76. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  77. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  78. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  79. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  80. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  81. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  82. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  83. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  84. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  85. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  86. package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
  87. package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
  88. package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
  90. package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
  91. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  92. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  93. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
  94. package/Types/Billing/PayAsYouGoPricing.ts +1 -1
  95. package/Types/Marketing/MarketingEvent.ts +97 -0
  96. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  97. package/UI/Utils/PermissionGate.ts +56 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  101. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  103. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  105. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  106. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  107. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  109. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  111. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  113. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  114. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  115. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  116. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  117. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  118. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  119. package/build/dist/Server/EnvironmentConfig.js +10 -70
  120. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  131. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  132. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  133. package/build/dist/Server/Services/BillingService.js +6 -0
  134. package/build/dist/Server/Services/BillingService.js.map +1 -1
  135. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  136. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  137. package/build/dist/Server/Services/Index.js +6 -2
  138. package/build/dist/Server/Services/Index.js.map +1 -1
  139. package/build/dist/Server/Services/LlmLogService.js +25 -9
  140. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  141. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  142. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  143. package/build/dist/Server/Services/ProjectService.js +56 -0
  144. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  145. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  146. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  147. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  148. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  149. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  150. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  151. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  152. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  153. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  154. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  155. package/build/dist/Server/Services/UserService.js +26 -1
  156. package/build/dist/Server/Services/UserService.js.map +1 -1
  157. package/build/dist/Server/Services/UserSlackService.js +195 -0
  158. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  159. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  160. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  161. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  162. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  163. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  164. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  165. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  166. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  167. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  168. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  169. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  170. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  171. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  172. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  173. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  174. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  175. package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
  176. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
  177. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  178. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  179. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  180. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  181. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  182. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  183. package/package.json +1 -1
  184. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  185. package/Server/Services/MarketingConversionService.ts +0 -10
  186. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  187. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  188. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  189. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  190. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  191. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  192. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  193. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  194. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  195. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  196. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  197. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  198. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  199. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  200. package/Types/Marketing/MarketingConversion.ts +0 -53
  201. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  202. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  203. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  204. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  205. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  206. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  207. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  208. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  209. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  210. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  211. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  212. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  213. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  214. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  215. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  216. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  217. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  218. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  219. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  220. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,214 @@
1
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
2
+ import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
3
+ import Dictionary from "../../../Types/Dictionary";
4
+ import Permission, { PermissionHelper } from "../../../Types/Permission";
5
+ import { describe, expect, it } from "@jest/globals";
6
+
7
+ /*
8
+ * https://github.com/OneUptime/oneuptime/issues/3360
9
+ *
10
+ * Monitor carries three bearer credentials as ordinary columns:
11
+ *
12
+ * serverMonitorSecretKey - authenticates every host agent reporting in
13
+ * incomingRequestSecretKey - IS the heartbeat URL
14
+ * incomingEmailSecretKey - IS the monitor's inbound address
15
+ *
16
+ * All three used to list Permission.Viewer in their read ACL. Viewer is the
17
+ * least privilege OneUptime offers and is precisely what gets handed to a
18
+ * dashboard, an auditor, a contractor or an automation -- so "read-only" also
19
+ * meant "can read a live credential and then forge or suppress this monitor's
20
+ * heartbeats". Redacting the secret out of MonitorLog.logBody (the other half
21
+ * of the fix) does nothing about this path: it is a plain column select.
22
+ *
23
+ * The rule these tests encode is a single sentence: reading a monitor secret
24
+ * requires the ability to ROTATE it. Anything weaker is a credential handed to
25
+ * someone who cannot revoke it.
26
+ */
27
+
28
+ const SECRET_KEY_COLUMNS: Array<string> = [
29
+ "serverMonitorSecretKey",
30
+ "incomingRequestSecretKey",
31
+ "incomingEmailSecretKey",
32
+ ];
33
+
34
+ /*
35
+ * The read-only roles. Every one of these was on the read list before the fix,
36
+ * and none of them can rotate a key.
37
+ */
38
+ const READ_ONLY_PERMISSIONS: Array<Permission> = [
39
+ Permission.Viewer,
40
+ Permission.MonitorViewer,
41
+ Permission.ReadProjectMonitor,
42
+ ];
43
+
44
+ // The roles that can reset a monitor secret, and so may also see it.
45
+ const ROTATION_PERMISSIONS: Array<Permission> = [
46
+ Permission.ProjectOwner,
47
+ Permission.ProjectAdmin,
48
+ Permission.ProjectMember,
49
+ Permission.MonitorAdmin,
50
+ Permission.MonitorMember,
51
+ Permission.EditProjectMonitor,
52
+ ];
53
+
54
+ const accessControl: Dictionary<ColumnAccessControl> =
55
+ new Monitor().getColumnAccessControlForAllColumns();
56
+
57
+ type CanReadFunction = (
58
+ columnName: string,
59
+ userPermissions: Array<Permission>,
60
+ ) => boolean;
61
+
62
+ /*
63
+ * ColumnPermission.checkDataColumnPermissions, reduced to the question that
64
+ * matters. Note what the server does when this is false: it THROWS
65
+ * `User is not allowed to read on <column> column of Monitor` and the whole
66
+ * request fails. There is no partial response -- which is why the dashboard
67
+ * has to gate the select rather than the render.
68
+ */
69
+ const canRead: CanReadFunction = (
70
+ columnName: string,
71
+ userPermissions: Array<Permission>,
72
+ ): boolean => {
73
+ const columnPermissions: Array<Permission> =
74
+ accessControl[columnName]?.read || [];
75
+
76
+ return PermissionHelper.doesPermissionsIntersect(
77
+ userPermissions,
78
+ columnPermissions,
79
+ );
80
+ };
81
+
82
+ describe("Monitor secret key columns are not readable by read-only roles", () => {
83
+ it.each(SECRET_KEY_COLUMNS)(
84
+ "%s is unreadable by a Viewer-only principal",
85
+ (columnName: string) => {
86
+ /*
87
+ * This is step 2 of the reproduction in the issue: an API key whose only
88
+ * permission is Viewer.
89
+ */
90
+ expect(canRead(columnName, [Permission.Viewer])).toBe(false);
91
+ },
92
+ );
93
+
94
+ it.each(SECRET_KEY_COLUMNS)(
95
+ "%s is unreadable by every read-only role, individually",
96
+ (columnName: string) => {
97
+ for (const permission of READ_ONLY_PERMISSIONS) {
98
+ expect(canRead(columnName, [permission])).toBe(false);
99
+ }
100
+ },
101
+ );
102
+
103
+ it.each(SECRET_KEY_COLUMNS)(
104
+ "%s is unreadable even when all the read-only roles are held at once",
105
+ (columnName: string) => {
106
+ /*
107
+ * Intersection semantics mean any single surviving read-only permission
108
+ * re-opens the hole, so hold them all and assert the union is still
109
+ * refused.
110
+ */
111
+ expect(canRead(columnName, READ_ONLY_PERMISSIONS)).toBe(false);
112
+ },
113
+ );
114
+
115
+ it.each(SECRET_KEY_COLUMNS)(
116
+ "%s names none of the read-only roles in its read ACL",
117
+ (columnName: string) => {
118
+ const readPermissions: Array<Permission> =
119
+ accessControl[columnName]?.read || [];
120
+
121
+ for (const permission of READ_ONLY_PERMISSIONS) {
122
+ expect(readPermissions).not.toContain(permission);
123
+ }
124
+ },
125
+ );
126
+ });
127
+
128
+ describe("Monitor secret key columns stay readable by the roles that can rotate them", () => {
129
+ it.each(SECRET_KEY_COLUMNS)(
130
+ "%s is readable by each rotation-capable role",
131
+ (columnName: string) => {
132
+ /*
133
+ * The other failure mode. Locking these down too far breaks Monitor >
134
+ * Settings for a project member, who legitimately needs the key to set
135
+ * an agent up.
136
+ */
137
+ for (const permission of ROTATION_PERMISSIONS) {
138
+ expect(canRead(columnName, [permission])).toBe(true);
139
+ }
140
+ },
141
+ );
142
+
143
+ it.each(SECRET_KEY_COLUMNS)(
144
+ "%s gates read on exactly the same roles as update",
145
+ (columnName: string) => {
146
+ /*
147
+ * The invariant, stated directly: you may see a monitor secret if and
148
+ * only if you may replace it. If someone later widens read without
149
+ * widening update, this is the test that should stop them and make them
150
+ * say why.
151
+ */
152
+ const column: ColumnAccessControl | undefined = accessControl[columnName];
153
+
154
+ expect(column).toBeDefined();
155
+ expect([...(column?.read || [])].sort()).toEqual(
156
+ [...(column?.update || [])].sort(),
157
+ );
158
+ },
159
+ );
160
+
161
+ it.each(SECRET_KEY_COLUMNS)(
162
+ "%s is never writable on create",
163
+ (columnName: string) => {
164
+ // The keys are computed server-side by MonitorService, never supplied.
165
+ expect(accessControl[columnName]?.create || []).toEqual([]);
166
+ },
167
+ );
168
+ });
169
+
170
+ describe("the rest of the monitor page still loads for a Viewer", () => {
171
+ /*
172
+ * Tightening the secret columns must not collaterally break the monitor view
173
+ * for read-only users: an unreadable column in a select fails the WHOLE
174
+ * request, so if any of these regressed, a Viewer would get an error screen
175
+ * instead of a monitor. These are the other columns
176
+ * Pages/Monitor/View/Index.tsx asks for.
177
+ */
178
+ const VIEWER_READABLE_COLUMNS: Array<string> = [
179
+ "monitorType",
180
+ "serverMonitorRequestReceivedAt",
181
+ "incomingRequestMonitorHeartbeatCheckedAt",
182
+ "incomingEmailMonitorHeartbeatCheckedAt",
183
+ "incomingEmailMonitorLastEmailReceivedAt",
184
+ "monitorSteps",
185
+ ];
186
+
187
+ it.each(VIEWER_READABLE_COLUMNS)(
188
+ "%s is still readable by a Viewer",
189
+ (columnName: string) => {
190
+ expect(canRead(columnName, [Permission.Viewer])).toBe(true);
191
+ },
192
+ );
193
+ });
194
+
195
+ describe("the payload columns rely on redaction, not on access control", () => {
196
+ /*
197
+ * serverMonitorResponse and incomingMonitorRequest are the OTHER two places
198
+ * the agent payload lands, and both remain Viewer-readable -- deliberately:
199
+ * they are what the monitor page renders. That is exactly why the secret has
200
+ * to be stripped at the ingest boundary (see MonitorPayloadRedaction) rather
201
+ * than merely hidden behind a permission. If someone ever removes that
202
+ * strip, no ACL here will catch it.
203
+ *
204
+ * This test is a signpost, not a guard. If these columns are deliberately
205
+ * locked down later, delete it -- but do not delete the redaction.
206
+ */
207
+ it("serverMonitorResponse is readable by a Viewer", () => {
208
+ expect(canRead("serverMonitorResponse", [Permission.Viewer])).toBe(true);
209
+ });
210
+
211
+ it("incomingMonitorRequest is readable by a Viewer", () => {
212
+ expect(canRead("incomingMonitorRequest", [Permission.Viewer])).toBe(true);
213
+ });
214
+ });
@@ -26,9 +26,11 @@ import TeamMemberService from "../../../Server/Services/TeamMemberService";
26
26
  import TeamService from "../../../Server/Services/TeamService";
27
27
  import UserCallService from "../../../Server/Services/UserCallService";
28
28
  import UserEmailService from "../../../Server/Services/UserEmailService";
29
+ import UserMicrosoftTeamsService from "../../../Server/Services/UserMicrosoftTeamsService";
29
30
  import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
30
31
  import UserPushService from "../../../Server/Services/UserPushService";
31
32
  import UserService from "../../../Server/Services/UserService";
33
+ import UserSlackService from "../../../Server/Services/UserSlackService";
32
34
  import UserSmsService from "../../../Server/Services/UserSmsService";
33
35
  import UserTelegramService from "../../../Server/Services/UserTelegramService";
34
36
  import UserWebhookService from "../../../Server/Services/UserWebhookService";
@@ -41,7 +43,9 @@ import {
41
43
  import Response from "../../../Server/Utils/Response";
42
44
  import UserCall from "../../../Models/DatabaseModels/UserCall";
43
45
  import UserEmail from "../../../Models/DatabaseModels/UserEmail";
46
+ import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
44
47
  import UserPush from "../../../Models/DatabaseModels/UserPush";
48
+ import UserSlack from "../../../Models/DatabaseModels/UserSlack";
45
49
  import UserSMS from "../../../Models/DatabaseModels/UserSMS";
46
50
  import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
47
51
  import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
@@ -316,6 +320,8 @@ let userSmsFindBy: jest.SpyInstance;
316
320
  let userCallFindBy: jest.SpyInstance;
317
321
  let userWhatsAppFindBy: jest.SpyInstance;
318
322
  let userTelegramFindBy: jest.SpyInstance;
323
+ let userSlackFindBy: jest.SpyInstance;
324
+ let userMicrosoftTeamsFindBy: jest.SpyInstance;
319
325
  let userWebhookFindBy: jest.SpyInstance;
320
326
  let notificationRuleFindBy: jest.SpyInstance;
321
327
  let incidentSeverityFindBy: jest.SpyInstance;
@@ -340,6 +346,8 @@ function everyFindBySpy(): Array<jest.SpyInstance> {
340
346
  userCallFindBy,
341
347
  userWhatsAppFindBy,
342
348
  userTelegramFindBy,
349
+ userSlackFindBy,
350
+ userMicrosoftTeamsFindBy,
343
351
  userWebhookFindBy,
344
352
  notificationRuleFindBy,
345
353
  incidentSeverityFindBy,
@@ -445,6 +453,12 @@ beforeEach(() => {
445
453
  userTelegramFindBy = jest
446
454
  .spyOn(UserTelegramService, "findBy")
447
455
  .mockResolvedValue([] as never);
456
+ userSlackFindBy = jest
457
+ .spyOn(UserSlackService, "findBy")
458
+ .mockResolvedValue([] as never);
459
+ userMicrosoftTeamsFindBy = jest
460
+ .spyOn(UserMicrosoftTeamsService, "findBy")
461
+ .mockResolvedValue([] as never);
448
462
  userWebhookFindBy = jest
449
463
  .spyOn(UserWebhookService, "findBy")
450
464
  .mockResolvedValue([] as never);
@@ -759,13 +773,14 @@ describe("GET /on-call-readiness/policy/:policyId", () => {
759
773
 
760
774
  /*
761
775
  * Four fields and no fifth. `methods` is the part of this payload that
762
- * describes rows an administrator is NOT permitted to read - the seven
776
+ * describes rows an administrator is NOT permitted to read - the nine
763
777
  * method models are owner-scoped precisely because their columns are the raw
764
- * phone number, the webhook bearer url, the push token, the telegram chat id
765
- * and the verification code - so the wire shape is asserted exhaustively
766
- * rather than field by field. A field added to ReadinessMethod is a field
767
- * that ships to every administrator of the project, and it should have to
768
- * pass through here on the way.
778
+ * phone number, the webhook bearer url, the push token, the telegram chat
779
+ * id, the slack member id, the teams object id and the verification code -
780
+ * so the wire shape is asserted exhaustively rather than field by field. A
781
+ * field added to ReadinessMethod is a field that ships to every
782
+ * administrator of the project, and it should have to pass through here on
783
+ * the way.
769
784
  */
770
785
  expect(user["methods"]).toEqual([
771
786
  {
@@ -1437,6 +1452,15 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1437
1452
  const RAW_WHATSAPP: string = "+14155559999";
1438
1453
  const RAW_HANDLE: string = "@jamesbond";
1439
1454
  const RAW_DEVICE: string = "Jane's iPhone 15 Pro";
1455
+ const RAW_SLACK_USERNAME: string = "jane.oncall.slack";
1456
+ const RAW_TEAMS_USERNAME: string = "Jane Doe [Overnight]";
1457
+ /*
1458
+ * The addressable targets behind the two workspace channels - the Slack
1459
+ * member id the bot DMs and the Entra object id the Teams bot resolves. On
1460
+ * the rows, like the webhook's bearer url, and never selected.
1461
+ */
1462
+ const RAW_SLACK_USER_ID: string = "U0MEMBERIDLEAK";
1463
+ const RAW_TEAMS_USER_ID: string = "aad-ENTRALEAK-7788";
1440
1464
  const RAW_WEBHOOK_NAME: string = "Payments Slack Hook";
1441
1465
  const RAW_WEBHOOK_URL: string =
1442
1466
  "https://hooks.slack.com/services/T000/B000/XXXXsecretXXXX";
@@ -1445,7 +1469,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1445
1469
  * One fixed id per channel, and fixed rather than generated so that a failure
1446
1470
  * prints a value a reader can match against the fixture that produced it.
1447
1471
  *
1448
- * These are the ids UserNotificationRule's seven foreign keys reference -
1472
+ * These are the ids UserNotificationRule's nine foreign keys reference -
1449
1473
  * userEmailId points at a UserEmail row, userSmsId at a UserSMS row - and they
1450
1474
  * are the reason `methodId` exists at all. An administrator building a rule
1451
1475
  * for somebody else may not READ any of these rows; what they get instead is
@@ -1475,10 +1499,16 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1475
1499
  const WEBHOOK_METHOD_ID: ObjectID = new ObjectID(
1476
1500
  "1a000000-0000-4000-8000-000000000007",
1477
1501
  );
1502
+ const SLACK_METHOD_ID: ObjectID = new ObjectID(
1503
+ "1a000000-0000-4000-8000-000000000008",
1504
+ );
1505
+ const MICROSOFT_TEAMS_METHOD_ID: ObjectID = new ObjectID(
1506
+ "1a000000-0000-4000-8000-000000000009",
1507
+ );
1478
1508
 
1479
1509
  /*
1480
1510
  * The expected id per channel, in one place, so the sweep below is a sweep
1481
- * over all seven rather than seven assertions that can each be forgotten
1511
+ * over all nine rather than nine assertions that can each be forgotten
1482
1512
  * individually. Keyed by the wire's `methodType` string.
1483
1513
  */
1484
1514
  const METHOD_ID_BY_TYPE: Dictionary<string> = {
@@ -1488,6 +1518,8 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1488
1518
  [ReadinessMethodType.Call]: CALL_METHOD_ID.toString(),
1489
1519
  [ReadinessMethodType.WhatsApp]: WHATSAPP_METHOD_ID.toString(),
1490
1520
  [ReadinessMethodType.Telegram]: TELEGRAM_METHOD_ID.toString(),
1521
+ [ReadinessMethodType.Slack]: SLACK_METHOD_ID.toString(),
1522
+ [ReadinessMethodType.MicrosoftTeams]: MICROSOFT_TEAMS_METHOD_ID.toString(),
1491
1523
  [ReadinessMethodType.Webhook]: WEBHOOK_METHOD_ID.toString(),
1492
1524
  };
1493
1525
 
@@ -1548,6 +1580,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1548
1580
  telegram.isVerified = true;
1549
1581
  userTelegramFindBy.mockResolvedValue([telegram] as never);
1550
1582
 
1583
+ const slack: UserSlack = new UserSlack();
1584
+ slack.id = SLACK_METHOD_ID;
1585
+ slack.userId = subjectUserId;
1586
+ slack.slackUserName = RAW_SLACK_USERNAME;
1587
+ // On the row, and never selected: the member id is the addressable target.
1588
+ slack.slackUserId = RAW_SLACK_USER_ID;
1589
+ slack.isVerified = true;
1590
+ userSlackFindBy.mockResolvedValue([slack] as never);
1591
+
1592
+ const microsoftTeams: UserMicrosoftTeams = new UserMicrosoftTeams();
1593
+ microsoftTeams.id = MICROSOFT_TEAMS_METHOD_ID;
1594
+ microsoftTeams.userId = subjectUserId;
1595
+ microsoftTeams.microsoftTeamsUserName = RAW_TEAMS_USERNAME;
1596
+ // On the row, and never selected: the Entra id is the addressable target.
1597
+ microsoftTeams.microsoftTeamsUserId = RAW_TEAMS_USER_ID;
1598
+ microsoftTeams.isVerified = true;
1599
+ userMicrosoftTeamsFindBy.mockResolvedValue([microsoftTeams] as never);
1600
+
1551
1601
  const push: UserPush = new UserPush();
1552
1602
  push.id = PUSH_METHOD_ID;
1553
1603
  push.userId = subjectUserId;
@@ -1597,6 +1647,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1597
1647
  maskedIdentifier: `j${IDENTIFIER_MASK}@personal.example.com`,
1598
1648
  isVerified: true,
1599
1649
  },
1650
+ /*
1651
+ * The two workspace channels sit in the zero-cost tier beside Push and
1652
+ * Email, ahead of every paid channel. Their masked identifier is the
1653
+ * human-facing NAME column under the handle rule; the addressable ids
1654
+ * are never selected at all - see the select tests below.
1655
+ */
1656
+ {
1657
+ methodId: SLACK_METHOD_ID.toString(),
1658
+ methodType: ReadinessMethodType.Slack,
1659
+ maskedIdentifier: `ja${IDENTIFIER_MASK}`,
1660
+ isVerified: true,
1661
+ },
1662
+ {
1663
+ methodId: MICROSOFT_TEAMS_METHOD_ID.toString(),
1664
+ methodType: ReadinessMethodType.MicrosoftTeams,
1665
+ maskedIdentifier: `Ja${IDENTIFIER_MASK}`,
1666
+ isVerified: true,
1667
+ },
1600
1668
  {
1601
1669
  methodId: SMS_METHOD_ID.toString(),
1602
1670
  methodType: ReadinessMethodType.SMS,
@@ -1634,10 +1702,10 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1634
1702
  ]);
1635
1703
  });
1636
1704
 
1637
- test("all seven channels reach the wire carrying the id of their OWN row", async () => {
1705
+ test("all nine channels reach the wire carrying the id of their OWN row", async () => {
1638
1706
  /*
1639
1707
  * The dropdown's entire premise, swept across every channel rather than
1640
- * spot-checked on one, because the seven are seven separate code paths that
1708
+ * spot-checked on one, because the nine are nine separate code paths that
1641
1709
  * each build their own row and each have their own opportunity to hand over
1642
1710
  * somebody else's id - or none at all.
1643
1711
  *
@@ -1650,7 +1718,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1650
1718
  const payload: Record<string, unknown> = await readUser();
1651
1719
  const methods: Array<Record<string, unknown>> = methodsOf(payload);
1652
1720
 
1653
- expect(methods).toHaveLength(7);
1721
+ expect(methods).toHaveLength(9);
1654
1722
 
1655
1723
  for (const method of methods) {
1656
1724
  const methodType: string = method["methodType"] as string;
@@ -1682,7 +1750,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1682
1750
  const payload: Record<string, unknown> = await readUser();
1683
1751
  const methods: Array<Record<string, unknown>> = methodsOf(payload);
1684
1752
 
1685
- expect(methods).toHaveLength(7);
1753
+ expect(methods).toHaveLength(9);
1686
1754
 
1687
1755
  for (const method of methods) {
1688
1756
  expect(method["methodId"]).not.toBe(subjectUserId.toString());
@@ -1691,23 +1759,23 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1691
1759
  expect(method["methodId"]).not.toBe(projectId.toString());
1692
1760
  }
1693
1761
 
1694
- // And all seven are distinct - one id copied across channels is the same bug.
1762
+ // And all nine are distinct - one id copied across channels is the same bug.
1695
1763
  const ids: Array<unknown> = methods.map(
1696
1764
  (method: Record<string, unknown>): unknown => {
1697
1765
  return method["methodId"];
1698
1766
  },
1699
1767
  );
1700
- expect(new Set(ids).size).toBe(7);
1768
+ expect(new Set(ids).size).toBe(9);
1701
1769
  });
1702
1770
 
1703
1771
  test("a method carries its id, its type, its mask and its verification - and nothing else", async () => {
1704
1772
  /*
1705
1773
  * The containment assertion. Every field on this object ships to every
1706
1774
  * administrator of the project, about a row that administrator is not
1707
- * allowed to read, so the key set is pinned exhaustively on all seven rather
1775
+ * allowed to read, so the key set is pinned exhaustively on all nine rather
1708
1776
  * than left to whatever the serialiser happens to copy. A `phone`, a
1709
- * `webhookUrl` or a `telegramChatId` appearing here is not a formatting
1710
- * change; it is the exposure this design was built to avoid.
1777
+ * `webhookUrl`, a `telegramChatId` or a `slackUserId` appearing here is not
1778
+ * a formatting change; it is the exposure this design was built to avoid.
1711
1779
  */
1712
1780
  const payload: Record<string, unknown> = await readUser();
1713
1781
 
@@ -1729,10 +1797,10 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1729
1797
  * Guards the guard, and it is not a formality here: the service drops any
1730
1798
  * method row it cannot find an id on, so a fixture regression that emptied
1731
1799
  * `methods` would satisfy every "not present" assertion below for the one
1732
- * reason that proves nothing at all. The seven masks have to be in the body
1800
+ * reason that proves nothing at all. The nine masks have to be in the body
1733
1801
  * before their absence of raw values means anything.
1734
1802
  */
1735
- expect(methodsOf(payload)).toHaveLength(7);
1803
+ expect(methodsOf(payload)).toHaveLength(9);
1736
1804
  expect(body).toContain(IDENTIFIER_MASK);
1737
1805
 
1738
1806
  /*
@@ -1747,6 +1815,8 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1747
1815
  RAW_WHATSAPP,
1748
1816
  RAW_HANDLE,
1749
1817
  RAW_DEVICE,
1818
+ RAW_SLACK_USERNAME,
1819
+ RAW_TEAMS_USERNAME,
1750
1820
  RAW_WEBHOOK_NAME,
1751
1821
  RAW_WEBHOOK_URL,
1752
1822
  // The notification address's local part on its own, and the hook's host.
@@ -1754,6 +1824,14 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1754
1824
  "hooks.slack.com",
1755
1825
  // The telegram chat id - the addressable target, never the label.
1756
1826
  "998877",
1827
+ // The workspace targets - the Slack member id and the Entra object id.
1828
+ RAW_SLACK_USER_ID,
1829
+ RAW_TEAMS_USER_ID,
1830
+ "MEMBERIDLEAK",
1831
+ "ENTRALEAK",
1832
+ // The workspace names' revealing middles, past what a handle mask keeps.
1833
+ "oncall.slack",
1834
+ "[Overnight]",
1757
1835
  /*
1758
1836
  * The unmasked tails of the four phone-shaped identifiers. maskIdentifier
1759
1837
  * keeps the last four digits on purpose, so the numbers themselves are
@@ -1795,7 +1873,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1795
1873
  const payload: Record<string, unknown> = await readUser();
1796
1874
  const methods: Array<Record<string, unknown>> = methodsOf(payload);
1797
1875
 
1798
- expect(methods).toHaveLength(7);
1876
+ expect(methods).toHaveLength(9);
1799
1877
 
1800
1878
  for (const method of methods) {
1801
1879
  expect(method["methodId"]).not.toBe(ruleId.toString());
@@ -1870,6 +1948,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
1870
1948
  expect(call.select?.["telegramUserHandle"]).toBe(true);
1871
1949
  expect(call.select?.["telegramChatId"]).toBeUndefined();
1872
1950
  });
1951
+
1952
+ test("the slack read takes the username, never the addressable member id", async () => {
1953
+ await readUser();
1954
+
1955
+ const call: CapturedFindBy = firstCall(userSlackFindBy);
1956
+
1957
+ expect(call.select?.["slackUserName"]).toBe(true);
1958
+ expect(call.select?.["slackUserId"]).toBeUndefined();
1959
+ });
1960
+
1961
+ test("the microsoft teams read takes the display name, never the addressable entra id", async () => {
1962
+ await readUser();
1963
+
1964
+ const call: CapturedFindBy = firstCall(userMicrosoftTeamsFindBy);
1965
+
1966
+ expect(call.select?.["microsoftTeamsUserName"]).toBe(true);
1967
+ expect(call.select?.["microsoftTeamsUserId"]).toBeUndefined();
1968
+ });
1873
1969
  });
1874
1970
 
1875
1971
  /*
@@ -1898,6 +1994,8 @@ interface StubRule {
1898
1994
  userEmailId?: ObjectID | undefined;
1899
1995
  userPushId?: ObjectID | undefined;
1900
1996
  userTelegramId?: ObjectID | undefined;
1997
+ userSlackId?: ObjectID | undefined;
1998
+ userMicrosoftTeamsId?: ObjectID | undefined;
1901
1999
  userWhatsAppId?: ObjectID | undefined;
1902
2000
  userWebhookId?: ObjectID | undefined;
1903
2001
  }
@@ -2037,17 +2135,22 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2037
2135
  ]);
2038
2136
  });
2039
2137
 
2040
- test("DEFECT closed: a rule on Telegram, WhatsApp or a webhook now counts", async () => {
2138
+ test("DEFECT closed: a rule on Telegram, WhatsApp, Slack, Teams or a webhook now counts", async () => {
2041
2139
  /*
2042
2140
  * The old check read userCallId/userSmsId/userEmailId/userPushId off the
2043
2141
  * rule row and treated a row carrying none of them as no rule at all, so a
2044
2142
  * responder reachable only on Telegram, WhatsApp or a webhook was reported
2045
2143
  * non-compliant while the runtime was quite happily paging them. A false RED
2046
- * teaches admins to ignore the table, which is worse than no table.
2144
+ * teaches admins to ignore the table, which is worse than no table. Slack
2145
+ * and Microsoft Teams arrived after the fix and are staged alongside so the
2146
+ * defect cannot return for the channels that never lived through it.
2047
2147
  *
2048
- * All three channels are staged onto one user at once: whichever column the
2148
+ * All five channels are staged onto one user at once: whichever column the
2049
2149
  * rule carries, the row is a rule.
2050
2150
  */
2151
+ const minor: StubSeverity = { id: ObjectID.generate(), name: "Minor" };
2152
+ const warn: StubSeverity = { id: ObjectID.generate(), name: "Warn" };
2153
+
2051
2154
  stage({
2052
2155
  settings: [
2053
2156
  { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
@@ -2069,6 +2172,13 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2069
2172
  incidentSeverityId: major.id,
2070
2173
  userWhatsAppId: ObjectID.generate(),
2071
2174
  },
2175
+ {
2176
+ _id: "rule-slack",
2177
+ userId: ada.id,
2178
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2179
+ incidentSeverityId: minor.id,
2180
+ userSlackId: ObjectID.generate(),
2181
+ },
2072
2182
  {
2073
2183
  _id: "rule-webhook",
2074
2184
  userId: ada.id,
@@ -2076,9 +2186,16 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2076
2186
  alertSeverityId: page.id,
2077
2187
  userWebhookId: ObjectID.generate(),
2078
2188
  },
2189
+ {
2190
+ _id: "rule-microsoft-teams",
2191
+ userId: ada.id,
2192
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
2193
+ alertSeverityId: warn.id,
2194
+ userMicrosoftTeamsId: ObjectID.generate(),
2195
+ },
2079
2196
  ],
2080
- incidentSeverities: [critical, major],
2081
- alertSeverities: [page],
2197
+ incidentSeverities: [critical, major, minor],
2198
+ alertSeverities: [page, warn],
2082
2199
  });
2083
2200
 
2084
2201
  const statuses: Array<Record<string, unknown>> = statusesOf(
@@ -2093,7 +2210,7 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2093
2210
  /*
2094
2211
  * The structural half of the fix. The three formerly-invisible channels were
2095
2212
  * invisible because they were never SELECTed; asserting that NONE of the
2096
- * seven is selected means no future edit can reintroduce a partial column
2213
+ * nine is selected means no future edit can reintroduce a partial column
2097
2214
  * list and quietly start under-counting again.
2098
2215
  */
2099
2216
  stage({
@@ -2116,6 +2233,8 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2116
2233
  "userEmailId",
2117
2234
  "userPushId",
2118
2235
  "userTelegramId",
2236
+ "userSlackId",
2237
+ "userMicrosoftTeamsId",
2119
2238
  "userWhatsAppId",
2120
2239
  "userWebhookId",
2121
2240
  ]) {
@@ -30,7 +30,7 @@ import { beforeEach, describe, expect, test } from "@jest/globals";
30
30
  * methods".
31
31
  *
32
32
  * This router is the ONLY gate under these routes, and that is not a stylistic
33
- * observation. The seven notification method models are scoped to the person
33
+ * observation. The nine notification method models are scoped to the person
34
34
  * who owns the device — the essay at the top of UserEmail.ts is the record of
35
35
  * what happened the one time that scope was widened — so the service behind
36
36
  * these handlers does every read and write with `isRoot: true`, which means the
@@ -362,7 +362,7 @@ describe("the routes it registers", () => {
362
362
  * Extending BaseAPI to inherit `this.router` would register a whole model's
363
363
  * CRUD routes as a side effect — dead code that is shadowed by the real
364
364
  * registration until the day the mount order changes. There is no model to
365
- * inherit here in any case: the seven method models stay owner-scoped and
365
+ * inherit here in any case: the nine method models stay owner-scoped and
366
366
  * this router deliberately never exposes them.
367
367
  */
368
368
  expect(UserNotificationMethodAdminAPI).toBe(mockRouter);
@@ -459,7 +459,17 @@ describe("LlmLogService autonomous-token lane isolation", () => {
459
459
  expect(sql).toContain('FROM "AIRun" AS "run"');
460
460
  expect(sql).toContain('"run"."triggeredByIncidentId" IS NOT NULL');
461
461
  expect(params[3]).toEqual(legacyIncidentFeatures);
462
- expect(params[4]).toEqual(legacyAlertFeatures);
462
+
463
+ /*
464
+ * The incident lane reads only its own legacy list, so it must bind
465
+ * exactly four parameters. Binding the unread alert list as $5 is what
466
+ * PostgreSQL rejected with "bind message supplies 5 parameters, but
467
+ * prepared statement "" requires 4", which silently disabled every capped
468
+ * incident investigation. LlmLogServiceTokenAggregateParams.test.ts holds
469
+ * the exhaustive placeholder/binding contract.
470
+ */
471
+ expect(params).toHaveLength(4);
472
+ expect(sql).not.toContain("$5");
463
473
  });
464
474
 
465
475
  test("subjectless totals exclude identifiable legacy incident and alert rows", async () => {