@oneuptime/common 12.0.14 → 12.0.15

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 (195) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/StatusPage.ts +7 -4
  6. package/Server/API/StatusPageAPI.ts +29 -4
  7. package/Server/EnvironmentConfig.ts +7 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/SlackAuthorization.ts +43 -6
  12. package/Server/Middleware/UserAuthorization.ts +236 -26
  13. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  14. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  15. package/Server/Services/GlobalOidcService.ts +132 -0
  16. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  17. package/Server/Services/GlobalSsoService.ts +132 -0
  18. package/Server/Services/UserEmailService.ts +4 -4
  19. package/Server/Utils/CronTab.ts +73 -0
  20. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  21. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  22. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  23. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  24. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  25. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  26. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  27. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  28. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  29. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  30. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  31. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  32. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
  33. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  34. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  35. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  37. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  38. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  39. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  40. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  41. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
  42. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  43. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  44. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  45. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  46. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  47. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  48. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  49. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  50. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  51. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  52. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  53. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  54. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  55. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  56. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  57. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  58. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  59. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  60. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  61. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  62. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  63. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  65. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  66. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  67. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  68. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  69. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  70. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  71. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  72. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  73. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  74. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  75. package/Tests/Utils/Analytics.test.ts +187 -0
  76. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  77. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  78. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  79. package/Types/Marketing/MarketingConversion.ts +12 -0
  80. package/Types/Monitor/CriteriaFilter.ts +9 -0
  81. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  82. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  83. package/UI/Components/Forms/BasicForm.tsx +13 -1
  84. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  85. package/UI/Components/Forms/ModelForm.tsx +10 -1
  86. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  87. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  88. package/Utils/Analytics.ts +55 -2
  89. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  90. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  91. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  92. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  93. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  95. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  97. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  99. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  101. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  102. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  103. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +6 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  113. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  114. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  115. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  116. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  117. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  119. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  120. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  121. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  122. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  123. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  125. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +4 -4
  127. package/build/dist/Server/Utils/CronTab.js +66 -0
  128. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  129. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  130. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  131. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  132. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  133. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  134. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  135. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  136. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  137. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  138. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  139. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  140. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  141. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  142. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  144. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  145. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  146. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  147. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  148. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  149. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  150. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  151. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  152. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
  154. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  156. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  158. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  160. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  161. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  162. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  163. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  164. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  165. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  166. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  167. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  168. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  169. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  170. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  171. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  172. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  173. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  174. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  175. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  176. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  177. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  178. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  179. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  180. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  182. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  183. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  184. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  185. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  186. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  187. package/build/dist/Utils/Analytics.js +50 -2
  188. package/build/dist/Utils/Analytics.js.map +1 -1
  189. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  190. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  191. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  192. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  193. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  194. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  195. package/package.json +1 -1
@@ -33,20 +33,29 @@ describe("UserMiddleware.doesSsoTokenForProjectExist - requiredSsoProviderId", (
33
33
  return u;
34
34
  };
35
35
 
36
- // Build a request whose cookies carry a real `sso-<projectId>` token.
36
+ /*
37
+ * Build a request whose cookies carry a real `sso-<projectId>` token.
38
+ *
39
+ * The type defaults to ProjectSSO because that is what a PROJECT SSO login
40
+ * actually mints, and the project-scoped slot now refuses Global-typed
41
+ * credentials outright - see the "legacy Global-typed token" block below for
42
+ * why, and for the tests that pin it.
43
+ */
37
44
  const buildRequestWithSsoToken: (data: {
38
45
  tokenProjectId: ObjectID;
39
46
  discriminatorProviderId?: ObjectID | undefined;
47
+ ssoProviderType?: SsoProviderType | undefined;
40
48
  }) => ExpressRequest = (data: {
41
49
  tokenProjectId: ObjectID;
42
50
  discriminatorProviderId?: ObjectID | undefined;
51
+ ssoProviderType?: SsoProviderType | undefined;
43
52
  }): ExpressRequest => {
44
53
  const token: string = CookieUtil.getSSOToken({
45
54
  user: buildUser(),
46
55
  projectId: data.tokenProjectId,
47
56
  ssoProviderId: data.discriminatorProviderId,
48
57
  ssoProviderType: data.discriminatorProviderId
49
- ? SsoProviderType.GlobalSSO
58
+ ? data.ssoProviderType || SsoProviderType.ProjectSSO
50
59
  : undefined,
51
60
  });
52
61
 
@@ -170,6 +179,86 @@ describe("UserMiddleware.doesSsoTokenForProjectExist - requiredSsoProviderId", (
170
179
  * a matching token. The token is sourced from the `global-sso-token` cookie
171
180
  * (web) or the `x-global-sso-token` header (mobile).
172
181
  */
182
+ /*
183
+ * A Global-typed credential must never be accepted from the per-project slot.
184
+ *
185
+ * Before the single-token redesign, the Global SSO router fanned out one
186
+ * per-project `sso-<projectId>` cookie per project the user belonged to, typed
187
+ * GlobalSSO and signed for 30 days. Those are still in browsers, and the
188
+ * mobile app still replays every non-expired stored token as `x-sso-tokens`.
189
+ * If the project slot accepted them, they would short-circuit past the
190
+ * provider-trust check - so disabling a provider would still not revoke them,
191
+ * for up to a month. They are refused here instead, which sends the user
192
+ * through a fresh global login that mints a properly-typed token.
193
+ */
194
+ describe("UserMiddleware.doesSsoTokenForProjectExist - legacy Global-typed token in the project slot", () => {
195
+ const legacyProjectId: ObjectID = ObjectID.generate();
196
+ const legacyUserId: ObjectID = ObjectID.generate();
197
+ const legacyProviderId: ObjectID = ObjectID.generate();
198
+
199
+ const buildLegacyRequest: (
200
+ ssoProviderType: SsoProviderType,
201
+ ) => ExpressRequest = (ssoProviderType: SsoProviderType): ExpressRequest => {
202
+ const user: User = new User();
203
+ user.id = legacyUserId;
204
+ user.email = new Email("legacy-sso-user@oneuptime.com");
205
+
206
+ const token: string = CookieUtil.getSSOToken({
207
+ user,
208
+ projectId: legacyProjectId,
209
+ ssoProviderId: legacyProviderId,
210
+ ssoProviderType,
211
+ });
212
+
213
+ return {
214
+ cookies: {
215
+ [CookieUtil.getUserSSOKey(legacyProjectId)]: token,
216
+ },
217
+ headers: {},
218
+ } as unknown as ExpressRequest;
219
+ };
220
+
221
+ test("a GlobalSSO-typed token in the project slot is refused", () => {
222
+ expect(
223
+ UserMiddleware.doesSsoTokenForProjectExist(
224
+ buildLegacyRequest(SsoProviderType.GlobalSSO),
225
+ legacyProjectId,
226
+ legacyUserId,
227
+ ),
228
+ ).toBe(false);
229
+ });
230
+
231
+ test("a GlobalOIDC-typed token in the project slot is refused", () => {
232
+ expect(
233
+ UserMiddleware.doesSsoTokenForProjectExist(
234
+ buildLegacyRequest(SsoProviderType.GlobalOIDC),
235
+ legacyProjectId,
236
+ legacyUserId,
237
+ ),
238
+ ).toBe(false);
239
+ });
240
+
241
+ test("the SAME token typed ProjectSSO is accepted, so the refusal is about the type and nothing else", () => {
242
+ expect(
243
+ UserMiddleware.doesSsoTokenForProjectExist(
244
+ buildLegacyRequest(SsoProviderType.ProjectSSO),
245
+ legacyProjectId,
246
+ legacyUserId,
247
+ ),
248
+ ).toBe(true);
249
+ });
250
+
251
+ test("a ProjectOIDC-typed token in the project slot is accepted", () => {
252
+ expect(
253
+ UserMiddleware.doesSsoTokenForProjectExist(
254
+ buildLegacyRequest(SsoProviderType.ProjectOIDC),
255
+ legacyProjectId,
256
+ legacyUserId,
257
+ ),
258
+ ).toBe(true);
259
+ });
260
+ });
261
+
173
262
  describe("UserMiddleware.doesSsoTokenForProjectExist - global SSO token", () => {
174
263
  const projectId: ObjectID = ObjectID.generate();
175
264
  const otherProjectId: ObjectID = ObjectID.generate();
@@ -0,0 +1,233 @@
1
+ import { describe, expect, test, beforeEach } from "@jest/globals";
2
+ import crypto from "crypto";
3
+ import WhatsAppAuthorization from "../../../Server/Middleware/WhatsAppAuthorization";
4
+ import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
5
+ import Response from "../../../Server/Utils/Response";
6
+ import {
7
+ ExpressResponse,
8
+ NextFunction,
9
+ OneUptimeRequest,
10
+ } from "../../../Server/Utils/Express";
11
+ import BadDataException from "../../../Types/Exception/BadDataException";
12
+
13
+ /*
14
+ * The config lookup is mocked so the signature branches can be exercised
15
+ * without Postgres; the middleware under test must never need a database.
16
+ */
17
+ jest.mock("../../../Server/Services/GlobalConfigService", () => {
18
+ return {
19
+ __esModule: true,
20
+ default: {
21
+ findOneBy: jest.fn(),
22
+ },
23
+ };
24
+ });
25
+
26
+ jest.mock("../../../Server/Utils/Response", () => {
27
+ return {
28
+ __esModule: true,
29
+ default: {
30
+ sendErrorResponse: jest.fn(),
31
+ },
32
+ };
33
+ });
34
+
35
+ /*
36
+ * Regression cover for the unguarded crypto.timingSafeEqual in
37
+ * isAuthorizedWhatsAppRequest. timingSafeEqual throws RangeError when the two
38
+ * buffers differ in byte length, and the compared value came straight off the
39
+ * attacker-controlled X-Hub-Signature-256 header with no try/catch anywhere
40
+ * above it. The endpoint is unauthenticated by design - the signature IS the
41
+ * authentication - so anyone could send `x-hub-signature-256: nope` and throw
42
+ * the middleware instead of receiving the intended 400.
43
+ */
44
+ describe("WhatsAppAuthorization.isAuthorizedWhatsAppRequest", () => {
45
+ const APP_SECRET: string = "meta-whatsapp-app-secret";
46
+ const RAW_BODY: string = JSON.stringify({
47
+ object: "whatsapp_business_account",
48
+ });
49
+
50
+ const findOneBy: jest.Mock =
51
+ GlobalConfigService.findOneBy as unknown as jest.Mock;
52
+ const sendErrorResponse: jest.Mock =
53
+ Response.sendErrorResponse as unknown as jest.Mock;
54
+
55
+ const res: ExpressResponse = {} as ExpressResponse;
56
+ let next: NextFunction;
57
+
58
+ type SignFunction = (body: string) => string;
59
+
60
+ const sign: SignFunction = (body: string): string => {
61
+ return `sha256=${crypto
62
+ .createHmac("sha256", APP_SECRET)
63
+ .update(body)
64
+ .digest("hex")}`;
65
+ };
66
+
67
+ type BuildRequestFunction = (
68
+ signature: string | undefined,
69
+ body?: string,
70
+ ) => OneUptimeRequest;
71
+
72
+ const buildRequest: BuildRequestFunction = (
73
+ signature: string | undefined,
74
+ body: string = RAW_BODY,
75
+ ): OneUptimeRequest => {
76
+ return {
77
+ headers:
78
+ signature === undefined ? {} : { "x-hub-signature-256": signature },
79
+ rawBody: body,
80
+ } as unknown as OneUptimeRequest;
81
+ };
82
+
83
+ beforeEach(() => {
84
+ jest.clearAllMocks();
85
+ next = jest.fn();
86
+ findOneBy.mockResolvedValue({ metaWhatsAppAppSecret: APP_SECRET });
87
+ });
88
+
89
+ test("calls next when the signature matches the body", async () => {
90
+ const req: OneUptimeRequest = buildRequest(sign(RAW_BODY));
91
+
92
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
93
+
94
+ expect(next).toHaveBeenCalled();
95
+ expect(sendErrorResponse).not.toHaveBeenCalled();
96
+ });
97
+
98
+ test("calls next when the signature matches but the digest is upper-case", async () => {
99
+ const signature: string = sign(RAW_BODY);
100
+ const req: OneUptimeRequest = buildRequest(
101
+ `sha256=${signature.slice("sha256=".length).toUpperCase()}`,
102
+ );
103
+
104
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
105
+
106
+ expect(next).toHaveBeenCalled();
107
+ expect(sendErrorResponse).not.toHaveBeenCalled();
108
+ });
109
+
110
+ test("rejects a well-formed signature computed over a different body", async () => {
111
+ const req: OneUptimeRequest = buildRequest(
112
+ sign(`${RAW_BODY} tampered`),
113
+ RAW_BODY,
114
+ );
115
+
116
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
117
+
118
+ expect(next).not.toHaveBeenCalled();
119
+ expect(sendErrorResponse).toHaveBeenCalledWith(
120
+ req,
121
+ res,
122
+ new BadDataException("WhatsApp webhook signature verification failed."),
123
+ );
124
+ });
125
+
126
+ test("rejects a well-formed signature computed with a different secret", async () => {
127
+ const req: OneUptimeRequest = buildRequest(
128
+ `sha256=${crypto
129
+ .createHmac("sha256", "some-other-secret")
130
+ .update(RAW_BODY)
131
+ .digest("hex")}`,
132
+ );
133
+
134
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
135
+
136
+ expect(next).not.toHaveBeenCalled();
137
+ expect(sendErrorResponse).toHaveBeenCalledWith(
138
+ req,
139
+ res,
140
+ new BadDataException("WhatsApp webhook signature verification failed."),
141
+ );
142
+ });
143
+
144
+ /*
145
+ * An empty header value is indistinguishable from an absent one, so both
146
+ * take the missing-header branch rather than the malformed-digest one.
147
+ */
148
+ test.each([
149
+ ["absent", undefined],
150
+ ["empty", ""],
151
+ ])(
152
+ "rejects an %s signature header",
153
+ async (_label: string, signature: string | undefined) => {
154
+ const req: OneUptimeRequest = buildRequest(signature);
155
+
156
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
157
+
158
+ expect(next).not.toHaveBeenCalled();
159
+ expect(sendErrorResponse).toHaveBeenCalledWith(
160
+ req,
161
+ res,
162
+ new BadDataException("Missing X-Hub-Signature-256 header."),
163
+ );
164
+ },
165
+ );
166
+
167
+ /*
168
+ * Every one of these is a byte length other than the 71 of a real
169
+ * "sha256=" + 64 hex header, which is exactly what used to reach
170
+ * timingSafeEqual and throw.
171
+ */
172
+ const malformedSignatures: Array<[string, string]> = [
173
+ ["a bare word", "nope"],
174
+ ["the prefix with no digest", "sha256="],
175
+ ["a short digest", "sha256=abc123"],
176
+ ["a long digest", `sha256=${"a".repeat(65)}`],
177
+ ["the right length but non-hex characters", `sha256=${"z".repeat(64)}`],
178
+ ["64 hex characters with no prefix", "a".repeat(64)],
179
+ ["the wrong prefix", `sha1=${"a".repeat(64)}`],
180
+ ["a whitespace-padded digest", ` sha256=${"a".repeat(64)}`],
181
+ ];
182
+
183
+ describe("rejects a malformed signature with 400 rather than throwing", () => {
184
+ test.each(malformedSignatures)(
185
+ "%s",
186
+ async (_label: string, signature: string) => {
187
+ const req: OneUptimeRequest = buildRequest(signature);
188
+
189
+ await expect(
190
+ WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next),
191
+ ).resolves.toBeUndefined();
192
+
193
+ expect(next).not.toHaveBeenCalled();
194
+ expect(sendErrorResponse).toHaveBeenCalledWith(
195
+ req,
196
+ res,
197
+ new BadDataException(
198
+ "WhatsApp webhook signature verification failed.",
199
+ ),
200
+ );
201
+ },
202
+ );
203
+ });
204
+
205
+ /*
206
+ * The shape check sits in front of the config lookup, so an unauthenticated
207
+ * flood of garbage signatures costs no database round trips.
208
+ */
209
+ test("does not query the global config for a malformed signature", async () => {
210
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(
211
+ buildRequest("nope"),
212
+ res,
213
+ next,
214
+ );
215
+
216
+ expect(findOneBy).not.toHaveBeenCalled();
217
+ });
218
+
219
+ test("rejects when the app secret is not configured", async () => {
220
+ findOneBy.mockResolvedValue({ metaWhatsAppAppSecret: " " });
221
+
222
+ const req: OneUptimeRequest = buildRequest(sign(RAW_BODY));
223
+
224
+ await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
225
+
226
+ expect(next).not.toHaveBeenCalled();
227
+ expect(sendErrorResponse).toHaveBeenCalledWith(
228
+ req,
229
+ res,
230
+ new BadDataException("Meta WhatsApp App Secret is not configured."),
231
+ );
232
+ });
233
+ });
@@ -8,7 +8,8 @@ import { describe, expect, test } from "@jest/globals";
8
8
  /*
9
9
  * The /probe-ingest/probe/discovery-scan/list route hands the requesting
10
10
  * probe its pending subnet scans and claims them (status "In Progress" +
11
- * startedAt) via DatabaseService.updateColumnsByIdWithoutHooks one raw
11
+ * startedAt, and a cleared statusMessage) via
12
+ * DatabaseService.updateColumnsByIdWithoutHooks — one raw
12
13
  * parameterized UPDATE that skips ALL on-update hooks: workflow HTTP
13
14
  * triggers, audit-log inserts, realtime events, service
14
15
  * onBeforeUpdate/onUpdateSuccess. The probe synchronously waits on this
@@ -18,8 +19,8 @@ import { describe, expect, test } from "@jest/globals";
18
19
  * pre-fetch SELECT + row re-fetch + save() transaction).
19
20
  *
20
21
  * - App/FeatureSet/Telemetry/API/ProbeIngest/DiscoveryScan.ts
21
- * NetworkDeviceDiscoveryScan.status/startedAt when a probe claims a
22
- * Pending scan
22
+ * NetworkDeviceDiscoveryScan.status/startedAt/statusMessage when a
23
+ * probe claims a Pending scan
23
24
  *
24
25
  * The conversion dropped NOTHING: the model declares no update workflow, no
25
26
  * audit logging and no realtime events. But the fast path skips hooks
@@ -36,8 +37,8 @@ import { describe, expect, test } from "@jest/globals";
36
37
  * be written by a root caller that bypasses the create-time check. That hook
37
38
  * is deliberately safe to skip here, and the assertions below pin exactly
38
39
  * why rather than merely restating that it exists: the claim write stamps
39
- * only `status` and `startedAt`, which the hook does not look at, and the
40
- * hook is a pass-through for that payload. If the hook ever grows to
40
+ * only `status`, `startedAt` and `statusMessage`, none of which the hook
41
+ * looks at, and the hook is a pass-through for that payload. If the hook ever grows to
41
42
  * validate a column the claim write touches — or the claim write grows to
42
43
  * touch `cidr` — these fail.
43
44
  *
@@ -79,7 +80,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
79
80
  */
80
81
  test("NetworkDeviceDiscoveryScan has every column the claim write stamps", () => {
81
82
  const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
82
- const fastPathColumns: Array<string> = ["status", "startedAt"];
83
+ const fastPathColumns: Array<string> = [
84
+ "status",
85
+ "startedAt",
86
+ "statusMessage",
87
+ ];
83
88
  for (const column of fastPathColumns) {
84
89
  expect(scan.isTableColumn(column)).toBe(true);
85
90
  }
@@ -130,7 +135,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
130
135
  * overlap shows up here.
131
136
  */
132
137
  test("the claim write's columns are disjoint from what onBeforeUpdate validates", () => {
133
- const claimWriteColumns: Array<string> = ["status", "startedAt"];
138
+ const claimWriteColumns: Array<string> = [
139
+ "status",
140
+ "startedAt",
141
+ "statusMessage",
142
+ ];
134
143
  const columnsValidatedByHook: Array<string> = ["cidr"];
135
144
 
136
145
  for (const column of claimWriteColumns) {
@@ -146,7 +155,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
146
155
  test("onBeforeUpdate is a pass-through for the exact claim payload", async () => {
147
156
  const claimUpdateBy: unknown = {
148
157
  query: { _id: "some-scan-id" },
149
- data: { status: "In Progress", startedAt: new Date(0) },
158
+ data: {
159
+ status: "In Progress",
160
+ startedAt: new Date(0),
161
+ statusMessage: null,
162
+ },
150
163
  props: { isRoot: true },
151
164
  limit: 1,
152
165
  skip: 0,