@oneuptime/common 12.0.12 → 12.0.13

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 (234) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  28. package/Server/Utils/LogRedaction.ts +576 -0
  29. package/Server/Utils/Logger.ts +123 -46
  30. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  31. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  32. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  33. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  34. package/Server/Utils/StartServer.ts +30 -9
  35. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  36. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  37. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  38. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  39. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  40. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  41. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  42. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  43. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  44. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  45. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  46. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  47. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  48. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  49. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  50. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  52. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  53. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  54. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  55. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  56. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  57. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  58. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  59. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  60. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  62. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  63. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  65. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  66. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  67. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  68. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  69. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  70. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  71. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  72. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  73. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  74. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  75. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  76. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  77. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  78. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  79. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  80. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  81. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  82. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  83. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  84. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  85. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  86. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  87. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  88. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  89. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  90. package/Types/API/Hostname.ts +79 -0
  91. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  92. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  93. package/Types/Database/DatabaseProperty.ts +38 -0
  94. package/Types/Database/TableColumn.ts +34 -7
  95. package/Types/Events/Recurring.ts +16 -1
  96. package/Types/Monitor/MonitorStep.ts +13 -1
  97. package/Types/Monitor/MonitorType.ts +345 -40
  98. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  99. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  100. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  101. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  102. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  103. package/UI/Components/Footer/Footer.tsx +32 -6
  104. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  105. package/UI/Components/Forms/Types/Field.ts +8 -0
  106. package/UI/Components/Loader/PageLoader.tsx +29 -8
  107. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  108. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  109. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  110. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  111. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  112. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  113. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  114. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  115. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  116. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  118. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  120. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  122. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  124. package/build/dist/Server/API/AIChatAPI.js +48 -35
  125. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  126. package/build/dist/Server/API/CommonAPI.js +59 -0
  127. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  128. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  129. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  130. package/build/dist/Server/API/SlackAPI.js +13 -10
  131. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  132. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  133. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  134. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  135. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  137. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  138. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  142. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  143. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  144. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  145. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  146. package/build/dist/Server/Services/DatabaseService.js +15 -6
  147. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  148. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  149. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  150. package/build/dist/Server/Services/LogService.js +16 -0
  151. package/build/dist/Server/Services/LogService.js.map +1 -1
  152. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  153. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  154. package/build/dist/Server/Services/SpanService.js +16 -0
  155. package/build/dist/Server/Services/SpanService.js.map +1 -1
  156. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  157. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  158. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  159. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  160. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  161. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  162. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  163. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  164. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  165. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  166. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  167. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  168. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  169. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  170. package/build/dist/Server/Utils/Logger.js +98 -45
  171. package/build/dist/Server/Utils/Logger.js.map +1 -1
  172. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  173. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  174. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  175. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  176. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  177. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  178. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  179. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  180. package/build/dist/Server/Utils/StartServer.js +10 -7
  181. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  182. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  183. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  184. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  185. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  186. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  187. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  188. package/build/dist/Types/API/Hostname.js +63 -0
  189. package/build/dist/Types/API/Hostname.js.map +1 -1
  190. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  191. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  192. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  193. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  194. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  195. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  196. package/build/dist/Types/Database/TableColumn.js +24 -6
  197. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  198. package/build/dist/Types/Events/Recurring.js +13 -1
  199. package/build/dist/Types/Events/Recurring.js.map +1 -1
  200. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  201. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  202. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  203. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  204. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  205. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  206. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  207. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  208. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  209. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  210. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  211. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  212. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  213. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  214. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  215. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  216. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  217. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  218. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  219. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  220. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  221. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  222. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  223. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  224. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  225. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  226. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  227. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  228. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  229. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  230. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  231. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  232. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  233. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  234. package/package.json +1 -1
@@ -0,0 +1,207 @@
1
+ import ObjectID from "../../../../Types/ObjectID";
2
+
3
+ /*
4
+ * The policy cache is keyed by projectId plus a CLIENT-supplied appIdentifier,
5
+ * so anyone holding an ingestion key (which ships in public browser JS) can
6
+ * mint unlimited distinct keys. These tests pin that the store is bounded -
7
+ * the regression they guard is the unbounded module-level Map that grew until
8
+ * the ingest pod was OOM-killed - while preserving the store's contracts:
9
+ * negative (null-policy) caching, per-project prefix invalidation, and the
10
+ * kill marker that must always land even at capacity.
11
+ */
12
+
13
+ jest.mock("../../../../Server/Infrastructure/Redis", () => {
14
+ return {
15
+ __esModule: true,
16
+ default: {
17
+ getClient: jest.fn().mockReturnValue(null),
18
+ isConnected: jest.fn().mockReturnValue(false),
19
+ },
20
+ };
21
+ });
22
+
23
+ jest.mock("../../../../Server/Utils/Logger", () => {
24
+ return {
25
+ __esModule: true,
26
+ default: {
27
+ debug: jest.fn(),
28
+ info: jest.fn(),
29
+ warn: jest.fn(),
30
+ error: jest.fn(),
31
+ },
32
+ };
33
+ });
34
+
35
+ import SessionReplayGateCacheStore, {
36
+ MAX_KILL_KEY_CACHE_ENTRIES,
37
+ MAX_POLICY_CACHE_ENTRIES,
38
+ PolicyCacheEntry,
39
+ } from "../../../../Server/Utils/SessionReplay/SessionReplayGateCacheStore";
40
+
41
+ type TestPolicy = { name: string };
42
+
43
+ function policyKey(prefix: string, index: number): string {
44
+ return `${prefix}:app-${index}`;
45
+ }
46
+
47
+ function fillPolicyCache(prefix: string, count: number): void {
48
+ for (let i: number = 0; i < count; i++) {
49
+ SessionReplayGateCacheStore.setPolicyEntry<TestPolicy>(
50
+ policyKey(prefix, i),
51
+ { name: `policy-${i}` },
52
+ );
53
+ }
54
+ }
55
+
56
+ describe("SessionReplayGateCacheStore bounded policy cache", (): void => {
57
+ beforeEach((): void => {
58
+ jest.clearAllMocks();
59
+ /* Module-level Maps persist across tests. */
60
+ SessionReplayGateCacheStore.clearCache();
61
+ });
62
+
63
+ it("evicts the oldest entry instead of growing past the cap under a unique-key flood", (): void => {
64
+ fillPolicyCache("proj", MAX_POLICY_CACHE_ENTRIES + 1);
65
+
66
+ expect(
67
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
68
+ policyKey("proj", 0),
69
+ ),
70
+ ).toBeUndefined();
71
+
72
+ const last: PolicyCacheEntry<TestPolicy> | undefined =
73
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
74
+ policyKey("proj", MAX_POLICY_CACHE_ENTRIES),
75
+ );
76
+ expect(last).toBeDefined();
77
+ expect(last?.policy).toEqual({
78
+ name: `policy-${MAX_POLICY_CACHE_ENTRIES}`,
79
+ });
80
+ }, 15000);
81
+
82
+ it("rewriting an existing key at capacity evicts nothing", (): void => {
83
+ fillPolicyCache("proj", MAX_POLICY_CACHE_ENTRIES);
84
+
85
+ SessionReplayGateCacheStore.setPolicyEntry<TestPolicy>(
86
+ policyKey("proj", 5),
87
+ { name: "rewritten" },
88
+ );
89
+
90
+ for (let i: number = 0; i < MAX_POLICY_CACHE_ENTRIES; i++) {
91
+ expect(
92
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
93
+ policyKey("proj", i),
94
+ ),
95
+ ).toBeDefined();
96
+ }
97
+
98
+ expect(
99
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
100
+ policyKey("proj", 5),
101
+ )?.policy,
102
+ ).toEqual({ name: "rewritten" });
103
+ }, 15000);
104
+
105
+ it("a rewrite refreshes recency, so the next eviction takes the stale key", (): void => {
106
+ fillPolicyCache("proj", MAX_POLICY_CACHE_ENTRIES);
107
+
108
+ SessionReplayGateCacheStore.setPolicyEntry<TestPolicy>(
109
+ policyKey("proj", 0),
110
+ { name: "refreshed" },
111
+ );
112
+
113
+ SessionReplayGateCacheStore.setPolicyEntry<TestPolicy>("proj:app-new", {
114
+ name: "new",
115
+ });
116
+
117
+ expect(
118
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
119
+ policyKey("proj", 1),
120
+ ),
121
+ ).toBeUndefined();
122
+ expect(
123
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
124
+ policyKey("proj", 0),
125
+ )?.policy,
126
+ ).toEqual({ name: "refreshed" });
127
+ expect(
128
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>("proj:app-new"),
129
+ ).toBeDefined();
130
+ }, 15000);
131
+
132
+ it("still caches null policies - the hot-path refusal must stay cached", (): void => {
133
+ SessionReplayGateCacheStore.setPolicyEntry<TestPolicy>(
134
+ "proj:app-none",
135
+ null,
136
+ );
137
+
138
+ const entry: PolicyCacheEntry<TestPolicy> | undefined =
139
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>("proj:app-none");
140
+
141
+ expect(entry).toBeDefined();
142
+ expect(entry?.policy).toBeNull();
143
+ expect(typeof entry?.loadedAt).toBe("number");
144
+ });
145
+
146
+ it("clearCache(projectId) deletes exactly that project's prefixed entries after evictions", (): void => {
147
+ const projectA: ObjectID = ObjectID.generate();
148
+ const projectB: ObjectID = ObjectID.generate();
149
+
150
+ fillPolicyCache(projectA.toString(), MAX_POLICY_CACHE_ENTRIES);
151
+ /* These writes evict projectA's oldest entries. */
152
+ fillPolicyCache(projectB.toString(), 5);
153
+
154
+ SessionReplayGateCacheStore.clearCache(projectA);
155
+
156
+ for (let i: number = 0; i < MAX_POLICY_CACHE_ENTRIES; i++) {
157
+ expect(
158
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
159
+ policyKey(projectA.toString(), i),
160
+ ),
161
+ ).toBeUndefined();
162
+ }
163
+
164
+ for (let i: number = 0; i < 5; i++) {
165
+ expect(
166
+ SessionReplayGateCacheStore.getPolicyEntry<TestPolicy>(
167
+ policyKey(projectB.toString(), i),
168
+ ),
169
+ ).toBeDefined();
170
+ }
171
+ }, 15000);
172
+ });
173
+
174
+ describe("SessionReplayGateCacheStore bounded kill-key cache", (): void => {
175
+ beforeEach((): void => {
176
+ jest.clearAllMocks();
177
+ SessionReplayGateCacheStore.clearCache();
178
+ });
179
+
180
+ it("the local kill marker always lands, even at capacity", async (): Promise<void> => {
181
+ /* Redis is absent, so the fail-open path caches isDisabled:false. */
182
+ for (let i: number = 0; i < MAX_KILL_KEY_CACHE_ENTRIES; i++) {
183
+ await SessionReplayGateCacheStore.isProjectKilled(ObjectID.generate());
184
+ }
185
+
186
+ const killedProject: ObjectID = ObjectID.generate();
187
+ await SessionReplayGateCacheStore.markProjectDisabled(killedProject);
188
+
189
+ expect(
190
+ await SessionReplayGateCacheStore.isProjectKilled(killedProject),
191
+ ).toBe(true);
192
+ }, 30000);
193
+
194
+ it("an evicted kill entry re-resolves through the normal path", async (): Promise<void> => {
195
+ const firstProject: ObjectID = ObjectID.generate();
196
+ await SessionReplayGateCacheStore.isProjectKilled(firstProject);
197
+
198
+ /* Push the first project's entry out of the bounded map. */
199
+ for (let i: number = 0; i < MAX_KILL_KEY_CACHE_ENTRIES; i++) {
200
+ await SessionReplayGateCacheStore.isProjectKilled(ObjectID.generate());
201
+ }
202
+
203
+ await expect(
204
+ SessionReplayGateCacheStore.isProjectKilled(firstProject),
205
+ ).resolves.toBe(false);
206
+ }, 30000);
207
+ });
@@ -0,0 +1,335 @@
1
+ import {
2
+ jsonBodyParserOptions,
3
+ urlEncodedBodyParserOptions,
4
+ } from "../../../Server/Utils/StartServer";
5
+ import {
6
+ ExpressJson,
7
+ ExpressRequest,
8
+ ExpressResponse,
9
+ ExpressUrlEncoded,
10
+ OneUptimeRequest,
11
+ RequestHandler,
12
+ } from "../../../Server/Utils/Express";
13
+ import logger from "../../../Server/Utils/Logger";
14
+ import {
15
+ afterEach,
16
+ beforeEach,
17
+ describe,
18
+ expect,
19
+ jest,
20
+ test,
21
+ } from "@jest/globals";
22
+ import { Readable } from "stream";
23
+ import fs from "fs";
24
+ import path from "path";
25
+
26
+ /*
27
+ * The body-parser verify callbacks capture the raw request body for webhook
28
+ * signature verification (Slack, WhatsApp, GitHub, billing). Every consumer
29
+ * does a read-only string comparison against the captured value, so the
30
+ * urlencoded path must alias ONE decoded string to both rawBody and
31
+ * rawFormUrlEncodedBody instead of decoding the buffer twice, and neither
32
+ * path may embed the body (which carries signing material) in its debug
33
+ * message.
34
+ */
35
+
36
+ type SpyLike = {
37
+ mock: { calls: Array<Array<unknown>> };
38
+ mockRestore: () => void;
39
+ };
40
+
41
+ type MockFn = ReturnType<typeof jest.fn>;
42
+
43
+ const JSON_DEBUG_MESSAGE: string =
44
+ "Raw JSON Body for signature verification captured";
45
+ const URL_ENCODED_DEBUG_MESSAGE: string =
46
+ "Raw Form Url Encoded Body for signature verification captured";
47
+
48
+ let debugSpy: SpyLike;
49
+
50
+ beforeEach(() => {
51
+ debugSpy = jest
52
+ .spyOn(logger, "debug")
53
+ .mockImplementation((): void => {}) as unknown as SpyLike;
54
+ });
55
+
56
+ afterEach(() => {
57
+ debugSpy.mockRestore();
58
+ });
59
+
60
+ type VerifyInvoker = (req: OneUptimeRequest, buf: Buffer) => void;
61
+
62
+ const invokeUrlEncodedVerify: VerifyInvoker = (
63
+ req: OneUptimeRequest,
64
+ buf: Buffer,
65
+ ): void => {
66
+ urlEncodedBodyParserOptions.verify(
67
+ req as ExpressRequest,
68
+ {} as ExpressResponse,
69
+ buf,
70
+ );
71
+ };
72
+
73
+ const invokeJsonVerify: VerifyInvoker = (
74
+ req: OneUptimeRequest,
75
+ buf: Buffer,
76
+ ): void => {
77
+ jsonBodyParserOptions.verify(
78
+ req as ExpressRequest,
79
+ {} as ExpressResponse,
80
+ buf,
81
+ );
82
+ };
83
+
84
+ describe("urlencoded verify: single decode aliased to both fields", () => {
85
+ test("decodes the buffer exactly once", () => {
86
+ const toString: MockFn = jest.fn().mockReturnValue("a=1&b=2");
87
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
88
+
89
+ invokeUrlEncodedVerify(req, { toString } as unknown as Buffer);
90
+
91
+ expect(toString).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ test("both fields come from the SAME decode, not two independent copies", () => {
95
+ /*
96
+ * A buffer whose toString returns a different value on every call: with
97
+ * one decode both fields hold the first value; the old two-decode code
98
+ * would leave the second value on rawBody.
99
+ */
100
+ const toString: MockFn = jest
101
+ .fn()
102
+ .mockReturnValueOnce("first-decode")
103
+ .mockReturnValue("second-decode");
104
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
105
+
106
+ invokeUrlEncodedVerify(req, { toString } as unknown as Buffer);
107
+
108
+ expect(req.rawFormUrlEncodedBody).toBe("first-decode");
109
+ expect(req.rawBody).toBe("first-decode");
110
+ expect(req.rawBody).toBe(req.rawFormUrlEncodedBody);
111
+ });
112
+
113
+ test("captures an ASCII body byte-identical to buf.toString()", () => {
114
+ const body: string = "a=1&b=2&msg=hello+world&sig=abc123";
115
+ const buf: Buffer = Buffer.from(body);
116
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
117
+
118
+ invokeUrlEncodedVerify(req, buf);
119
+
120
+ expect(req.rawFormUrlEncodedBody).toBe(buf.toString());
121
+ expect(req.rawBody).toBe(buf.toString());
122
+ expect(req.rawBody).toBe(body);
123
+ });
124
+
125
+ test("captures a multi-byte UTF-8 body byte-identical to buf.toString()", () => {
126
+ const body: string = "name=Grüße&emoji=🚀🔥&city=東京&mixed=a™b";
127
+ const buf: Buffer = Buffer.from(body, "utf8");
128
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
129
+
130
+ invokeUrlEncodedVerify(req, buf);
131
+
132
+ expect(req.rawFormUrlEncodedBody).toBe(buf.toString());
133
+ expect(req.rawBody).toBe(buf.toString());
134
+ expect(req.rawBody).toBe(body);
135
+ });
136
+ });
137
+
138
+ describe("urlencoded verify: debug log is constant and body-free", () => {
139
+ test("logs the constant message with request attributes, never the body", () => {
140
+ const secret: string = "SlackSigningSecretMarker123";
141
+ const req: OneUptimeRequest = {
142
+ requestId: "req-123",
143
+ } as OneUptimeRequest;
144
+
145
+ invokeUrlEncodedVerify(req, Buffer.from(`token=${secret}&user=alice`));
146
+
147
+ expect(debugSpy.mock.calls).toHaveLength(1);
148
+
149
+ const message: unknown = debugSpy.mock.calls[0]![0];
150
+ const attributes: unknown = debugSpy.mock.calls[0]![1];
151
+
152
+ expect(message).toBe(URL_ENCODED_DEBUG_MESSAGE);
153
+ expect(message as string).not.toContain(secret);
154
+ expect(message as string).not.toContain("alice");
155
+ expect(attributes).toEqual({ requestId: "req-123" });
156
+ });
157
+ });
158
+
159
+ describe("json verify: regression guard", () => {
160
+ test("sets rawBody to the exact body and leaves rawFormUrlEncodedBody unset", () => {
161
+ const body: string = '{"alert":"CPU high","city":"東京","emoji":"🚀"}';
162
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
163
+
164
+ invokeJsonVerify(req, Buffer.from(body, "utf8"));
165
+
166
+ expect(req.rawBody).toBe(body);
167
+ expect(req.rawFormUrlEncodedBody).toBeUndefined();
168
+ });
169
+
170
+ test("decodes the buffer exactly once", () => {
171
+ const toString: MockFn = jest.fn().mockReturnValue("{}");
172
+ const req: OneUptimeRequest = {} as OneUptimeRequest;
173
+
174
+ invokeJsonVerify(req, { toString } as unknown as Buffer);
175
+
176
+ expect(toString).toHaveBeenCalledTimes(1);
177
+ expect(req.rawBody).toBe("{}");
178
+ });
179
+
180
+ test("logs the constant message with request attributes, never the body", () => {
181
+ const secret: string = "GitHubWebhookSecretMarker456";
182
+ const req: OneUptimeRequest = {
183
+ requestId: "req-456",
184
+ } as OneUptimeRequest;
185
+
186
+ invokeJsonVerify(req, Buffer.from(`{"secret":"${secret}"}`));
187
+
188
+ expect(debugSpy.mock.calls).toHaveLength(1);
189
+
190
+ const message: unknown = debugSpy.mock.calls[0]![0];
191
+ const attributes: unknown = debugSpy.mock.calls[0]![1];
192
+
193
+ expect(message).toBe(JSON_DEBUG_MESSAGE);
194
+ expect(message as string).not.toContain(secret);
195
+ expect(attributes).toEqual({ requestId: "req-456" });
196
+ });
197
+ });
198
+
199
+ /*
200
+ * The exported options through the REAL express parsers, so the verify
201
+ * contract (arguments, timing, the parsed body still coming out the other
202
+ * end) is pinned against body-parser itself and not just against how these
203
+ * tests call the callbacks.
204
+ */
205
+ describe("options through the real express parsers", () => {
206
+ type StreamRequestFactory = (
207
+ body: string,
208
+ contentType: string,
209
+ ) => OneUptimeRequest;
210
+
211
+ const createStreamRequest: StreamRequestFactory = (
212
+ body: string,
213
+ contentType: string,
214
+ ): OneUptimeRequest => {
215
+ const readable: Readable = new Readable({
216
+ read(): void {},
217
+ });
218
+ readable.push(Buffer.from(body, "utf8"));
219
+ readable.push(null);
220
+
221
+ const req: OneUptimeRequest = readable as unknown as OneUptimeRequest;
222
+ req.headers = {
223
+ "content-type": contentType,
224
+ "content-length": String(Buffer.byteLength(body, "utf8")),
225
+ };
226
+
227
+ return req;
228
+ };
229
+
230
+ type MiddlewareRunner = (
231
+ middleware: RequestHandler,
232
+ req: OneUptimeRequest,
233
+ ) => Promise<void>;
234
+
235
+ const runMiddleware: MiddlewareRunner = (
236
+ middleware: RequestHandler,
237
+ req: OneUptimeRequest,
238
+ ): Promise<void> => {
239
+ return new Promise<void>(
240
+ (resolve: () => void, reject: (reason: unknown) => void) => {
241
+ middleware(req, {} as ExpressResponse, (err?: unknown): void => {
242
+ if (err) {
243
+ reject(err);
244
+ return;
245
+ }
246
+ resolve();
247
+ });
248
+ },
249
+ );
250
+ };
251
+
252
+ test("urlencoded: parses the body AND captures one raw copy on both fields", async () => {
253
+ const body: string = "a=1&b=2&name=Gr%C3%BC%C3%9Fe";
254
+ const req: OneUptimeRequest = createStreamRequest(
255
+ body,
256
+ "application/x-www-form-urlencoded",
257
+ );
258
+ const middleware: RequestHandler = ExpressUrlEncoded(
259
+ urlEncodedBodyParserOptions,
260
+ ) as RequestHandler;
261
+
262
+ await runMiddleware(middleware, req);
263
+
264
+ expect(req.body).toEqual({ a: "1", b: "2", name: "Grüße" });
265
+ expect(req.rawBody).toBe(body);
266
+ expect(req.rawFormUrlEncodedBody).toBe(body);
267
+ expect(debugSpy.mock.calls).toHaveLength(1);
268
+ expect(debugSpy.mock.calls[0]![0]).toBe(URL_ENCODED_DEBUG_MESSAGE);
269
+ });
270
+
271
+ test("json: parses the body AND captures the raw copy on rawBody", async () => {
272
+ const body: string = '{"a":1,"city":"東京"}';
273
+ const req: OneUptimeRequest = createStreamRequest(body, "application/json");
274
+ const middleware: RequestHandler = ExpressJson(
275
+ jsonBodyParserOptions,
276
+ ) as RequestHandler;
277
+
278
+ await runMiddleware(middleware, req);
279
+
280
+ expect(req.body).toEqual({ a: 1, city: "東京" });
281
+ expect(req.rawBody).toBe(body);
282
+ expect(req.rawFormUrlEncodedBody).toBeUndefined();
283
+ expect(debugSpy.mock.calls).toHaveLength(1);
284
+ expect(debugSpy.mock.calls[0]![0]).toBe(JSON_DEBUG_MESSAGE);
285
+ });
286
+ });
287
+
288
+ /*
289
+ * Read rather than executed, following VendorAssets.test.ts: what matters is
290
+ * only that StartServer builds its middlewares from these exact exported
291
+ * option objects, so the behavioral tests above are testing what production
292
+ * actually runs.
293
+ */
294
+ describe("StartServer wires the exported options into its middlewares", () => {
295
+ const startServerSource: string = fs.readFileSync(
296
+ path.resolve(
297
+ __dirname,
298
+ "..",
299
+ "..",
300
+ "..",
301
+ "Server",
302
+ "Utils",
303
+ "StartServer.ts",
304
+ ),
305
+ "utf8",
306
+ );
307
+
308
+ test("json middleware is built from jsonBodyParserOptions", () => {
309
+ expect(startServerSource).toMatch(
310
+ /ExpressJson\(\s*jsonBodyParserOptions,?\s*\)/,
311
+ );
312
+ });
313
+
314
+ test("urlencoded middleware is built from urlEncodedBodyParserOptions", () => {
315
+ expect(startServerSource).toMatch(
316
+ /ExpressUrlEncoded\(\s*urlEncodedBodyParserOptions,?\s*\)/,
317
+ );
318
+ });
319
+
320
+ /*
321
+ * Large probe payloads depend on the 50mb limit; the express default is
322
+ * 100kb, so a transcription slip in these exported objects would reject
323
+ * real traffic while every tiny-body test here still passes.
324
+ */
325
+ test("both option objects keep the 50mb limit and extended parsing", () => {
326
+ for (const options of [
327
+ jsonBodyParserOptions,
328
+ urlEncodedBodyParserOptions,
329
+ ]) {
330
+ expect(options.limit).toBe("50mb");
331
+ expect(options.extended).toBe(true);
332
+ expect(typeof options.verify).toBe("function");
333
+ }
334
+ });
335
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The X-Robots-Tag half of the status page indexing opt-out.
3
+ *
4
+ * The meta tag in index.ejs only covers the HTML. This header is what carries
5
+ * the same instruction on the responses that have no <head> to put a tag in -
6
+ * the RSS feed and llms.txt - so it is tested on its own.
7
+ */
8
+
9
+ import applyStatusPageRobotsHeader from "../../../Server/Utils/StatusPageSearchEngineIndexing";
10
+ import { ExpressResponse } from "../../../Server/Utils/Express";
11
+ import { describe, expect, jest, test } from "@jest/globals";
12
+
13
+ type FakeResponse = {
14
+ set: ReturnType<typeof jest.fn>;
15
+ headersSent: boolean;
16
+ };
17
+
18
+ function fakeResponse(headersSent: boolean = false): FakeResponse {
19
+ return {
20
+ set: jest.fn(),
21
+ headersSent,
22
+ };
23
+ }
24
+
25
+ describe("applyStatusPageRobotsHeader", () => {
26
+ test("sends noindex, nofollow when the owner turned indexing off", () => {
27
+ const res: FakeResponse = fakeResponse();
28
+
29
+ applyStatusPageRobotsHeader(res as unknown as ExpressResponse, false);
30
+
31
+ expect(res.set).toHaveBeenCalledTimes(1);
32
+ expect(res.set).toHaveBeenCalledWith("X-Robots-Tag", "noindex, nofollow");
33
+ });
34
+
35
+ test("sends no header at all when indexing is on", () => {
36
+ /*
37
+ * Not "sends index, follow". Emitting the default explicitly would
38
+ * overwrite a noindex that an operator set in front of OneUptime - a CDN
39
+ * rule or an ingress annotation - because the last writer of a header
40
+ * wins. Silence leaves those in place.
41
+ */
42
+ const res: FakeResponse = fakeResponse();
43
+
44
+ applyStatusPageRobotsHeader(res as unknown as ExpressResponse, true);
45
+
46
+ expect(res.set).not.toHaveBeenCalled();
47
+ });
48
+
49
+ test("does not touch a response that has already been flushed", () => {
50
+ /*
51
+ * Node drops headers set after the response has gone out, and logs
52
+ * nothing. Guarding here keeps a late caller from thinking it succeeded.
53
+ */
54
+ const res: FakeResponse = fakeResponse(true);
55
+
56
+ applyStatusPageRobotsHeader(res as unknown as ExpressResponse, false);
57
+
58
+ expect(res.set).not.toHaveBeenCalled();
59
+ });
60
+ });