@oneuptime/common 12.0.29 → 12.0.31

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 (664) hide show
  1. package/Models/AnalyticsModels/SecurityEvent.ts +48 -16
  2. package/Models/AnalyticsModels/ThreatIntelIndicator.ts +10 -5
  3. package/Models/DatabaseModels/AlertSeverity.ts +44 -0
  4. package/Models/DatabaseModels/DetectionRule.ts +16 -5
  5. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +14 -5
  6. package/Models/DatabaseModels/IncidentSeverity.ts +44 -0
  7. package/Models/DatabaseModels/Index.ts +13 -0
  8. package/Models/DatabaseModels/NetworkDevice.ts +113 -3
  9. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +88 -0
  10. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +22 -0
  11. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +499 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  14. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  15. package/Models/DatabaseModels/ThreatIntelFeed.ts +16 -5
  16. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  17. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  18. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  19. package/Server/API/GlobalConfigAPI.ts +362 -183
  20. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  21. package/Server/API/TelemetryAPI.ts +42 -4
  22. package/Server/EnvironmentConfig.ts +250 -0
  23. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.ts +59 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.ts +25 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  28. package/Server/Middleware/MultipartFormData.ts +118 -32
  29. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  30. package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +9 -1
  31. package/Server/Services/AlertEpisodeOnCallRuleEngineService.ts +9 -1
  32. package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +9 -1
  33. package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +9 -1
  34. package/Server/Services/AlertGroupingEngineService.ts +9 -1
  35. package/Server/Services/AlertLabelRuleEngineService.ts +9 -1
  36. package/Server/Services/AlertOnCallRuleEngineService.ts +9 -1
  37. package/Server/Services/AlertOwnerRuleEngineService.ts +9 -1
  38. package/Server/Services/AlertPrivacyRuleEngineService.ts +9 -1
  39. package/Server/Services/AlertReminderRuleService.ts +9 -1
  40. package/Server/Services/AutoRemediationRuleEngineService.ts +9 -1
  41. package/Server/Services/CephClusterLabelRuleEngineService.ts +9 -1
  42. package/Server/Services/CephClusterOwnerRuleEngineService.ts +9 -1
  43. package/Server/Services/CloudResourceLabelRuleEngineService.ts +9 -1
  44. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +9 -1
  45. package/Server/Services/DashboardLabelRuleEngineService.ts +9 -1
  46. package/Server/Services/DashboardOwnerRuleEngineService.ts +9 -1
  47. package/Server/Services/DockerHostLabelRuleEngineService.ts +9 -1
  48. package/Server/Services/DockerHostOwnerRuleEngineService.ts +9 -1
  49. package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +9 -1
  50. package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +9 -1
  51. package/Server/Services/HostLabelRuleEngineService.ts +9 -1
  52. package/Server/Services/HostOwnerRuleEngineService.ts +9 -1
  53. package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +9 -1
  54. package/Server/Services/IncidentEpisodeOnCallRuleEngineService.ts +9 -1
  55. package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +9 -1
  56. package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +9 -1
  57. package/Server/Services/IncidentGroupingEngineService.ts +9 -1
  58. package/Server/Services/IncidentLabelRuleEngineService.ts +9 -1
  59. package/Server/Services/IncidentOnCallRuleEngineService.ts +9 -1
  60. package/Server/Services/IncidentOwnerRuleEngineService.ts +9 -1
  61. package/Server/Services/IncidentPrivacyRuleEngineService.ts +9 -1
  62. package/Server/Services/IncidentReminderRuleService.ts +9 -1
  63. package/Server/Services/IncidentSlaRuleService.ts +9 -1
  64. package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +9 -1
  65. package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +9 -1
  66. package/Server/Services/Index.ts +14 -0
  67. package/Server/Services/IoTFleetLabelRuleEngineService.ts +9 -1
  68. package/Server/Services/IoTFleetOwnerRuleEngineService.ts +9 -1
  69. package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +9 -1
  70. package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +9 -1
  71. package/Server/Services/KubernetesResourceService.ts +5 -1
  72. package/Server/Services/MonitorLabelRuleEngineService.ts +9 -1
  73. package/Server/Services/MonitorOwnerRuleEngineService.ts +9 -1
  74. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +35 -0
  75. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +96 -0
  76. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +9 -1
  77. package/Server/Services/NetworkDeviceOidTemplateService.ts +201 -0
  78. package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +9 -1
  79. package/Server/Services/NetworkDeviceService.ts +209 -1
  80. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  81. package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +9 -1
  82. package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +9 -1
  83. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  84. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +9 -1
  85. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  86. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  87. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +9 -1
  88. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  89. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  90. package/Server/Services/PodmanHostLabelRuleEngineService.ts +9 -1
  91. package/Server/Services/PodmanHostOwnerRuleEngineService.ts +9 -1
  92. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  93. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +9 -1
  94. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +9 -1
  95. package/Server/Services/RumApplicationLabelRuleEngineService.ts +9 -1
  96. package/Server/Services/RumApplicationOwnerRuleEngineService.ts +9 -1
  97. package/Server/Services/RunbookLabelRuleEngineService.ts +9 -1
  98. package/Server/Services/RunbookOwnerRuleEngineService.ts +9 -1
  99. package/Server/Services/RunbookRuleEngineService.ts +9 -1
  100. package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +9 -1
  101. package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +9 -1
  102. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +9 -1
  103. package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +9 -1
  104. package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +9 -1
  105. package/Server/Services/ServiceLabelRuleEngineService.ts +9 -1
  106. package/Server/Services/ServiceOwnerRuleEngineService.ts +9 -1
  107. package/Server/Services/StatusPageLabelRuleEngineService.ts +9 -1
  108. package/Server/Services/StatusPageMonitorRuleEngineService.ts +7 -0
  109. package/Server/Services/StatusPageOwnerRuleEngineService.ts +9 -1
  110. package/Server/Services/TeamMemberService.ts +489 -1
  111. package/Server/Services/TeamService.ts +50 -0
  112. package/Server/Services/TelemetrySourceMapService.ts +266 -23
  113. package/Server/Services/UserNotificationSettingService.ts +44 -1
  114. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  115. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  116. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  117. package/Server/Services/UserService.ts +27 -0
  118. package/Server/Services/WorkflowLabelRuleEngineService.ts +9 -1
  119. package/Server/Services/WorkflowOwnerRuleEngineService.ts +9 -1
  120. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +186 -0
  121. package/Server/Utils/Monitor/CapturedMetricAttributeUtil.ts +245 -0
  122. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +21 -2
  123. package/Server/Utils/Monitor/MonitorMetricUtil.ts +55 -21
  124. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +32 -0
  125. package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +15 -2
  126. package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +18 -1
  127. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +10 -0
  128. package/Server/Utils/Monitor/SeriesResourceLabels.ts +34 -0
  129. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  130. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  131. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  132. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  133. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  134. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  135. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  136. package/Server/Utils/Response.ts +134 -0
  137. package/Server/Utils/Rules/RuleEngineRuleRead.ts +48 -0
  138. package/Server/Utils/StartServer.ts +10 -0
  139. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  140. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +14 -1
  141. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +14 -2
  142. package/Server/Utils/Telemetry/SourceMapResolver.ts +16 -5
  143. package/Server/Utils/Telemetry/StackTraceParser.ts +399 -0
  144. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +125 -0
  145. package/Tests/App/Dashboard/HeaderImageAssets.test.tsx +93 -0
  146. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  147. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  148. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  149. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  150. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  151. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3 -3
  152. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  153. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  154. package/Tests/App/Dashboard/UserElementEmail.test.tsx +12 -4
  155. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +1 -1
  156. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  157. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +14 -0
  158. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +469 -0
  159. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  160. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  161. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  162. package/Tests/Models/SecurityDomainAccessControl.test.ts +429 -0
  163. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  164. package/Tests/Server/API/GlobalConfigLicense.test.ts +657 -0
  165. package/Tests/Server/API/Helpers.ts +8 -0
  166. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  167. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +84 -6
  168. package/Tests/Server/API/TelemetryResolveStackTraceAPI.test.ts +805 -0
  169. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  170. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  171. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +394 -0
  172. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +1144 -0
  173. package/Tests/Server/Middleware/MultipartFormData.test.ts +618 -3
  174. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  175. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  176. package/Tests/Server/Services/MonitorLabelRuleEngineScale.test.ts +520 -0
  177. package/Tests/Server/Services/NetworkDeviceOidTemplateService.test.ts +332 -0
  178. package/Tests/Server/Services/NetworkDeviceOidTemplateWriteGuards.test.ts +409 -0
  179. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  180. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  181. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  182. package/Tests/Server/Services/RuleEngineRuleFetchLimit.test.ts +239 -0
  183. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  184. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  185. package/Tests/Server/Services/TelemetrySourceMapService.test.ts +1210 -2
  186. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  187. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  188. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +182 -0
  189. package/Tests/Server/Types/AnalyticsDatabase/SecurityEventModelPermission.test.ts +255 -0
  190. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  191. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +212 -0
  192. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +413 -0
  193. package/Tests/Server/Utils/CodeRepository.test.ts +689 -0
  194. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +495 -0
  195. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +77 -0
  196. package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +284 -0
  197. package/Tests/Server/Utils/Monitor/MonitorCustomMetricAttributes.test.ts +503 -0
  198. package/Tests/Server/Utils/Monitor/MonitorTemplatePrototypePollution.test.ts +106 -0
  199. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkLogWritePath.test.ts +33 -4
  200. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +14 -4
  201. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +44 -0
  202. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  203. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  204. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  205. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  206. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  207. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  208. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  209. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  210. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  211. package/Tests/Server/Utils/OpenAPISpec.test.ts +536 -0
  212. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  213. package/Tests/Server/Utils/Rules/RuleEngineRuleRead.test.ts +115 -0
  214. package/Tests/Server/Utils/StackTraceParser.test.ts +610 -0
  215. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  216. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  217. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  218. package/Tests/Server/Utils/Telemetry/SourceMapLimits.test.ts +656 -0
  219. package/Tests/Server/Utils/Telemetry/SourceMapResolver.test.ts +100 -0
  220. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  221. package/Tests/Types/Monitor/SnmpMonitor/SnmpOidListUtil.test.ts +454 -0
  222. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  223. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  224. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  225. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  226. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  227. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  228. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  229. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  230. package/Tests/Types/SecurityRolePermissions.test.ts +193 -0
  231. package/Tests/UI/Components/Charts/AnnotationLayout.test.ts +773 -0
  232. package/Tests/UI/Components/Charts/ChartAnnotationRail.test.tsx +789 -0
  233. package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +254 -0
  234. package/Tests/UI/Components/Charts/TimeAnnotation.test.ts +471 -0
  235. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +405 -0
  236. package/Tests/UI/Components/ErrorMessageRefreshControl.test.tsx +83 -0
  237. package/Tests/UI/Components/MarkdownConverters.test.ts +686 -0
  238. package/Tests/UI/Components/ModelTable/BaseModelTableDeleteAndEmptyStates.test.tsx +341 -0
  239. package/Tests/UI/Components/OrderedStatesList.test.tsx +5 -5
  240. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +1 -1
  241. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +195 -0
  242. package/Tests/UI/Components/XAxis.test.ts +270 -5
  243. package/Tests/UI/Components/XAxisDaylightSaving.test.ts +113 -0
  244. package/Tests/UI/Styles/ChartFocusRing.test.ts +153 -0
  245. package/Tests/UI/Styles/ChartFocusRingCoverage.test.tsx +377 -0
  246. package/Tests/UI/Styles/ThemeStylesheet.ts +162 -0
  247. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  248. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  249. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +507 -0
  250. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +951 -1
  251. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +238 -0
  252. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +1056 -0
  253. package/Tests/Utils/NetworkDiscovery/ReverseDnsNameUtil.test.ts +882 -0
  254. package/Types/Calendar/ICalendar.ts +442 -0
  255. package/Types/Date.ts +39 -0
  256. package/Types/Email/EmailTemplateType.ts +2 -0
  257. package/Types/Monitor/SnmpMonitor/SnmpOidListUtil.ts +333 -0
  258. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  259. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  260. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  261. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  262. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  263. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  264. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  265. package/Types/Permission.ts +96 -0
  266. package/Types/Rum/SessionReplay.ts +21 -0
  267. package/Types/Telemetry/SourceMap.ts +8 -0
  268. package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.tsx +176 -0
  269. package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.ts +745 -0
  270. package/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.tsx +576 -0
  271. package/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.tsx +204 -0
  272. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +491 -544
  273. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +324 -372
  274. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +487 -540
  275. package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +7 -3
  276. package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +14 -3
  277. package/UI/Components/Charts/Types/ChartEventKind.ts +13 -0
  278. package/UI/Components/Charts/Types/ReferenceRegionProps.ts +6 -0
  279. package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +20 -3
  280. package/UI/Components/Charts/Utils/TimeAnnotation.ts +69 -35
  281. package/UI/Components/Charts/Utils/XAxis.ts +125 -18
  282. package/UI/Components/EditionLabel/EditionLabel.tsx +280 -26
  283. package/UI/Components/ErrorMessage/ErrorMessage.tsx +21 -10
  284. package/UI/Components/Feed/FeedItem.tsx +4 -5
  285. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -2
  286. package/UI/Components/Header/UserProfile/UserProfile.tsx +1 -2
  287. package/UI/Components/Image/Image.tsx +20 -3
  288. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  289. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +1 -2
  290. package/UI/Components/ModelTable/BaseModelTable.tsx +150 -24
  291. package/UI/Components/Table/TableHeader.tsx +64 -48
  292. package/UI/Components/Table/TableRow.tsx +13 -3
  293. package/UI/Styles/Theme.css +38 -0
  294. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +271 -0
  295. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +6 -0
  296. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +141 -10
  297. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +55 -3
  298. package/Utils/NetworkDiscovery/ReverseDnsNameUtil.ts +151 -0
  299. package/Utils/Rules/RuleEngineLimits.ts +32 -0
  300. package/build/dist/Models/AnalyticsModels/SecurityEvent.js +48 -16
  301. package/build/dist/Models/AnalyticsModels/SecurityEvent.js.map +1 -1
  302. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js +10 -5
  303. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js.map +1 -1
  304. package/build/dist/Models/DatabaseModels/AlertSeverity.js +46 -1
  305. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  306. package/build/dist/Models/DatabaseModels/DetectionRule.js +16 -5
  307. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  308. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +14 -5
  309. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
  310. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +46 -1
  311. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  312. package/build/dist/Models/DatabaseModels/Index.js +13 -0
  313. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  314. package/build/dist/Models/DatabaseModels/NetworkDevice.js +115 -3
  315. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  316. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +88 -0
  317. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  318. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  319. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +519 -0
  320. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -0
  321. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  322. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  323. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  324. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  325. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  326. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  327. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +16 -5
  328. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js.map +1 -1
  329. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  330. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  331. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  332. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  333. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  334. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  335. package/build/dist/Server/API/GlobalConfigAPI.js +293 -176
  336. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  337. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  338. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  339. package/build/dist/Server/API/TelemetryAPI.js +31 -3
  340. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  341. package/build/dist/Server/EnvironmentConfig.js +185 -0
  342. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  343. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  344. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  345. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  346. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  347. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js +26 -0
  348. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js.map +1 -0
  349. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js +14 -0
  350. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js.map +1 -0
  351. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  352. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  353. package/build/dist/Server/Middleware/MultipartFormData.js +93 -28
  354. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  355. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  356. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  357. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +8 -1
  358. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
  359. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js +8 -1
  360. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js.map +1 -1
  361. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +8 -1
  362. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
  363. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +8 -1
  364. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
  365. package/build/dist/Server/Services/AlertGroupingEngineService.js +8 -1
  366. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  367. package/build/dist/Server/Services/AlertLabelRuleEngineService.js +8 -1
  368. package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
  369. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js +8 -1
  370. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js.map +1 -1
  371. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +8 -1
  372. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
  373. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +8 -1
  374. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
  375. package/build/dist/Server/Services/AlertReminderRuleService.js +8 -1
  376. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  377. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +8 -1
  378. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  379. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +8 -1
  380. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
  381. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +8 -1
  382. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
  383. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +8 -1
  384. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  385. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +8 -1
  386. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  387. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +8 -1
  388. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
  389. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +8 -1
  390. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
  391. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +8 -1
  392. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
  393. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +8 -1
  394. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
  395. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +8 -1
  396. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
  397. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +8 -1
  398. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
  399. package/build/dist/Server/Services/HostLabelRuleEngineService.js +8 -1
  400. package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
  401. package/build/dist/Server/Services/HostOwnerRuleEngineService.js +8 -1
  402. package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
  403. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +8 -1
  404. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
  405. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js +8 -1
  406. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js.map +1 -1
  407. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +8 -1
  408. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
  409. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +8 -1
  410. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
  411. package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -1
  412. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  413. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +8 -1
  414. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
  415. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js +8 -1
  416. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js.map +1 -1
  417. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +8 -1
  418. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
  419. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +8 -1
  420. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
  421. package/build/dist/Server/Services/IncidentReminderRuleService.js +8 -1
  422. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  423. package/build/dist/Server/Services/IncidentSlaRuleService.js +8 -1
  424. package/build/dist/Server/Services/IncidentSlaRuleService.js.map +1 -1
  425. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +8 -1
  426. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
  427. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +8 -1
  428. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
  429. package/build/dist/Server/Services/Index.js +14 -0
  430. package/build/dist/Server/Services/Index.js.map +1 -1
  431. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +8 -1
  432. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
  433. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +8 -1
  434. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
  435. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +8 -1
  436. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
  437. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +8 -1
  438. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
  439. package/build/dist/Server/Services/KubernetesResourceService.js +5 -1
  440. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  441. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +8 -1
  442. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
  443. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +8 -1
  444. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
  445. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +30 -21
  446. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  447. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +64 -0
  448. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  449. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +8 -1
  450. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  451. package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js +180 -0
  452. package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js.map +1 -0
  453. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +8 -1
  454. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
  455. package/build/dist/Server/Services/NetworkDeviceService.js +154 -1
  456. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  457. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  458. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  459. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +8 -1
  460. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
  461. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +8 -1
  462. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
  463. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  464. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  465. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +8 -1
  466. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
  467. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  468. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  469. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  470. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  471. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +8 -1
  472. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
  473. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  474. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  475. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  476. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  477. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +8 -1
  478. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
  479. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +8 -1
  480. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
  481. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  482. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  483. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +8 -1
  484. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
  485. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +8 -1
  486. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
  487. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +8 -1
  488. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
  489. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +8 -1
  490. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
  491. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +8 -1
  492. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
  493. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +8 -1
  494. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
  495. package/build/dist/Server/Services/RunbookRuleEngineService.js +8 -1
  496. package/build/dist/Server/Services/RunbookRuleEngineService.js.map +1 -1
  497. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +8 -1
  498. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
  499. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +8 -1
  500. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
  501. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +8 -1
  502. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  503. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +8 -1
  504. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
  505. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +8 -1
  506. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
  507. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +8 -1
  508. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
  509. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +8 -1
  510. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
  511. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +8 -1
  512. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
  513. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +6 -0
  514. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -1
  515. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +8 -1
  516. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
  517. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  518. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  519. package/build/dist/Server/Services/TeamService.js +43 -0
  520. package/build/dist/Server/Services/TeamService.js.map +1 -1
  521. package/build/dist/Server/Services/TelemetrySourceMapService.js +213 -19
  522. package/build/dist/Server/Services/TelemetrySourceMapService.js.map +1 -1
  523. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  524. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  525. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  526. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  527. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  528. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  529. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  530. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  531. package/build/dist/Server/Services/UserService.js +25 -0
  532. package/build/dist/Server/Services/UserService.js.map +1 -1
  533. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +8 -1
  534. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
  535. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +8 -1
  536. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
  537. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +145 -0
  538. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +1 -0
  539. package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js +201 -0
  540. package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js.map +1 -0
  541. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +19 -2
  542. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  543. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +42 -23
  544. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  545. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +29 -0
  546. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  547. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +11 -2
  548. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
  549. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +16 -1
  550. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
  551. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +10 -0
  552. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  553. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +33 -0
  554. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  555. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  556. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  557. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  558. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  559. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  560. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  561. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  562. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  563. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  564. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  565. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  566. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  567. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  568. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  569. package/build/dist/Server/Utils/Response.js +97 -0
  570. package/build/dist/Server/Utils/Response.js.map +1 -1
  571. package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js +37 -0
  572. package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js.map +1 -0
  573. package/build/dist/Server/Utils/StartServer.js +9 -1
  574. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  575. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  576. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  577. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +12 -1
  578. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -1
  579. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +14 -2
  580. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  581. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js +15 -5
  582. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js.map +1 -1
  583. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +307 -0
  584. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -1
  585. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  586. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  587. package/build/dist/Types/Date.js +27 -0
  588. package/build/dist/Types/Date.js.map +1 -1
  589. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  590. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  591. package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js +253 -0
  592. package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js.map +1 -0
  593. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  594. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  595. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  596. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  597. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  598. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  599. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  600. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  601. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  602. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  603. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  604. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  605. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  606. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  607. package/build/dist/Types/Permission.js +86 -0
  608. package/build/dist/Types/Permission.js.map +1 -1
  609. package/build/dist/Types/Rum/SessionReplay.js +19 -0
  610. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  611. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js +83 -0
  612. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js.map +1 -0
  613. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js +531 -0
  614. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js.map +1 -0
  615. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js +264 -0
  616. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js.map +1 -0
  617. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js +122 -0
  618. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js.map +1 -0
  619. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +213 -233
  620. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  621. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +139 -152
  622. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  623. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +212 -232
  624. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  625. package/build/dist/UI/Components/Charts/Types/ChartEventKind.js +14 -0
  626. package/build/dist/UI/Components/Charts/Types/ChartEventKind.js.map +1 -0
  627. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +57 -28
  628. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
  629. package/build/dist/UI/Components/Charts/Utils/XAxis.js +103 -17
  630. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  631. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +198 -23
  632. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  633. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +15 -5
  634. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
  635. package/build/dist/UI/Components/Feed/FeedItem.js +4 -5
  636. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  637. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -2
  638. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  639. package/build/dist/UI/Components/Header/UserProfile/UserProfile.js.map +1 -1
  640. package/build/dist/UI/Components/Image/Image.js +6 -1
  641. package/build/dist/UI/Components/Image/Image.js.map +1 -1
  642. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  643. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  644. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +1 -2
  645. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  646. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +104 -23
  647. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  648. package/build/dist/UI/Components/Table/TableHeader.js +20 -22
  649. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  650. package/build/dist/UI/Components/Table/TableRow.js +13 -4
  651. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  652. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +153 -0
  653. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -0
  654. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
  655. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +127 -7
  656. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
  657. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +50 -3
  658. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  659. package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js +132 -0
  660. package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js.map +1 -0
  661. package/build/dist/Utils/Rules/RuleEngineLimits.js +31 -0
  662. package/build/dist/Utils/Rules/RuleEngineLimits.js.map +1 -0
  663. package/jest.config.json +1 -0
  664. package/package.json +2 -2
@@ -0,0 +1,2194 @@
1
+ import {
2
+ DisableOnCallCalendarFeed,
3
+ Host,
4
+ HttpProtocol,
5
+ ProvisionSsl,
6
+ TrustedProxyHops,
7
+ } from "../EnvironmentConfig";
8
+ import OnCallCalendarFeedCache from "../Infrastructure/OnCallCalendarFeedCache";
9
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
10
+ import OnCallCalendarFeedRateLimit from "../Middleware/OnCallCalendarFeedRateLimit";
11
+ import UserMiddleware from "../Middleware/UserAuthorization";
12
+ import OnCallDutyPolicyScheduleCalendarFeedService from "../Services/OnCallDutyPolicyScheduleCalendarFeedService";
13
+ import OnCallDutyPolicyScheduleService from "../Services/OnCallDutyPolicyScheduleService";
14
+ import ProjectOnCallCalendarFeedService from "../Services/ProjectOnCallCalendarFeedService";
15
+ import UserOnCallCalendarFeedService from "../Services/UserOnCallCalendarFeedService";
16
+ import Express, {
17
+ ExpressRequest,
18
+ ExpressResponse,
19
+ ExpressRouter,
20
+ NextFunction,
21
+ } from "../Utils/Express";
22
+ import logger from "../Utils/Logger";
23
+ import CalendarFeedToken, {
24
+ CalendarFeedRotation,
25
+ MintedCalendarFeedToken,
26
+ } from "../Utils/OnCall/CalendarFeedToken";
27
+ import OnCallCalendarFeedRenderer, {
28
+ FEED_DISABLED_REASON,
29
+ FeedRenderOutcome,
30
+ FeedRenderRequest,
31
+ FeedRenderStatus,
32
+ TOKEN_ROTATED_REASON,
33
+ UserShiftsResult,
34
+ } from "../Utils/OnCall/OnCallCalendarFeedRenderer";
35
+ import OnCallCalendarFeedUrls, {
36
+ FeedUrls,
37
+ } from "../Utils/OnCall/OnCallCalendarFeedUrls";
38
+ import Response from "../Utils/Response";
39
+ import CommonAPI from "./CommonAPI";
40
+ import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
41
+ import OnCallDutyPolicyScheduleCalendarFeed from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed";
42
+ import ProjectOnCallCalendarFeed from "../../Models/DatabaseModels/ProjectOnCallCalendarFeed";
43
+ import UserOnCallCalendarFeed from "../../Models/DatabaseModels/UserOnCallCalendarFeed";
44
+ import Protocol from "../../Types/API/Protocol";
45
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
46
+ import OneUptimeDate from "../../Types/Date";
47
+ import BadDataException from "../../Types/Exception/BadDataException";
48
+ import Exception from "../../Types/Exception/Exception";
49
+ import ExceptionCode from "../../Types/Exception/ExceptionCode";
50
+ import NotFoundException from "../../Types/Exception/NotFoundException";
51
+ import ServiceUnavailableException from "../../Types/Exception/ServiceUnavailableException";
52
+ import { JSONObject } from "../../Types/JSON";
53
+ import ObjectID from "../../Types/ObjectID";
54
+ import {
55
+ DEFAULT_FUTURE_DAYS,
56
+ DEFAULT_PAST_DAYS,
57
+ MAX_FUTURE_DAYS,
58
+ MAX_PAST_DAYS,
59
+ } from "../../Types/OnCallDutyPolicy/CalendarFeedWindow";
60
+ import MaterializedShiftUtil from "../../Types/OnCallDutyPolicy/MaterializedShift";
61
+ import { OnCallCalendarFeedKind } from "../../Types/OnCallDutyPolicy/OnCallCalendarFeedUtil";
62
+
63
+ /*
64
+ * The HTTP face of the on-call calendar feeds.
65
+ *
66
+ * Two very different kinds of route share this router, and the difference is
67
+ * the whole design:
68
+ *
69
+ * PUBLIC CAPABILITY ROUTES (no UserMiddleware). A calendar client sends no
70
+ * cookie and no bearer token; the 43-character token in the path IS the
71
+ * credential. UserMiddleware is deliberately absent: with no cookie it would
72
+ * add nothing, but a browser holding an expired session cookie would be
73
+ * answered 401 before the token was even read, and an `apikey` header -- even
74
+ * an empty one -- would hijack the request into API-key authentication. The
75
+ * pipeline is, in order: kill switch -> scheme guard -> rate limit -> shape
76
+ * guard -> token lookup (current hash, then the rotated-out hash inside its
77
+ * grace) -> the renderer, which re-derives authorisation on every fetch
78
+ * (a valid token is not authorisation) -> response -> bookkeeping.
79
+ *
80
+ * ONE RESPONSE RULE, applied everywhere:
81
+ * unknown / malformed / expired token -> 404, generic body
82
+ * disabled, rotated-out-in-grace, below plan,
83
+ * no eligible schedule -> 200, EMPTY VCALENDAR
84
+ * kill switch, or render cap with nothing cached -> 503 + Retry-After
85
+ * The empty-calendar cases are 200 on purpose: a client that gets 404 keeps
86
+ * showing the last copy it downloaded (or drops the subscription with an
87
+ * error), whereas an empty calendar makes it clear its copy.
88
+ *
89
+ * SESSION ROUTES (UserMiddleware + requireUserAuthentication). The settings
90
+ * pages and the mobile app read and rotate feeds here. /current is the ONLY
91
+ * place the encrypted `token` column is ever read back; the public routes
92
+ * never select it, so a rotated ENCRYPTION_SECRET can break the settings page
93
+ * (which says "Regenerate link") but never the subscribed calendars.
94
+ *
95
+ * The token never reaches a log line, an error message, a span attribute or
96
+ * a cache key. Everything that identifies a request in logs is the feed kind
97
+ * and the row id.
98
+ */
99
+ const router: ExpressRouter = Express.getRouter();
100
+
101
+ // -- Routes (exported so tests and the dashboard pin the same strings) ------
102
+
103
+ export const PERSONAL_FEED_ROUTE: string =
104
+ "/on-call-calendar/user/:token/shifts.ics";
105
+ export const SCHEDULE_FEED_ROUTE: string =
106
+ "/on-call-calendar/schedule/:token/schedule.ics";
107
+ export const PROJECT_FEED_ROUTE: string =
108
+ "/on-call-calendar/project/:token/project.ics";
109
+
110
+ /*
111
+ * Anything else under a token-bearing prefix: a link pasted without the
112
+ * trailing `shifts.ics`, a typo in the filename, a client or a scanner
113
+ * sending POST/PUT/DELETE to a feed URL. Without these the request falls
114
+ * through to the application's own catch-alls, which answer
115
+ * `Page not found - ${req.url}` -- the plaintext token, at ERROR level, in
116
+ * stdout, in the master-admin support bundle's recent-log buffer and in the
117
+ * OTel log exporter. The token is the credential; it must never be logged.
118
+ */
119
+ export const PERSONAL_FEED_FALLBACK_ROUTE: string = "/on-call-calendar/user/*";
120
+ export const SCHEDULE_FEED_FALLBACK_ROUTE: string =
121
+ "/on-call-calendar/schedule/*";
122
+ export const PROJECT_FEED_FALLBACK_ROUTE: string =
123
+ "/on-call-calendar/project/*";
124
+
125
+ export const FEED_CURRENT_ROUTE: string = "/on-call-calendar/feed/current";
126
+ export const FEED_ROTATE_ROUTE: string = "/on-call-calendar/feed/rotate";
127
+
128
+ export const SCHEDULE_FEED_CURRENT_ROUTE: string =
129
+ "/on-call-calendar/schedule-feed/:scheduleId/current";
130
+ export const SCHEDULE_FEED_PUBLISH_ROUTE: string =
131
+ "/on-call-calendar/schedule-feed/:scheduleId/publish";
132
+ export const SCHEDULE_FEED_ROTATE_ROUTE: string =
133
+ "/on-call-calendar/schedule-feed/:scheduleId/rotate";
134
+
135
+ export const PROJECT_FEED_CURRENT_ROUTE: string =
136
+ "/on-call-calendar/project-feed/current";
137
+ export const PROJECT_FEED_PUBLISH_ROUTE: string =
138
+ "/on-call-calendar/project-feed/publish";
139
+ export const PROJECT_FEED_ROTATE_ROUTE: string =
140
+ "/on-call-calendar/project-feed/rotate";
141
+
142
+ export const MY_SHIFTS_ROUTE: string = "/on-call-calendar/my-shifts";
143
+
144
+ // -- Constants --------------------------------------------------------------
145
+
146
+ export const KILL_SWITCH_RETRY_AFTER_SECONDS: number = 3600;
147
+
148
+ /* Bookkeeping is written at most this often per feed. */
149
+ export const BOOKKEEPING_INTERVAL_MS: number = 5 * 60 * 1000;
150
+
151
+ export const MY_SHIFTS_DEFAULT_DAYS: number = 30;
152
+ export const MY_SHIFTS_MAX_DAYS: number = 120;
153
+
154
+ export const ROTATE_LOCK_NAMESPACE: string = "OnCallCalendarFeed.rotate";
155
+ export const ROTATE_LOCK_TIMEOUT_MS: number = 10000;
156
+
157
+ const STALE_WARNING_HEADER: string = '110 - "Response is Stale"';
158
+
159
+ /* The body of every 404 on the public routes. Says nothing. */
160
+ const NOT_FOUND_MESSAGE: string = "Not found.";
161
+
162
+ // -- Wire shapes ------------------------------------------------------------
163
+
164
+ export interface FeedStatusSettings {
165
+ includeCoveringShifts?: boolean;
166
+ includeCoverageGaps?: boolean;
167
+ minimumGapMinutes?: number;
168
+ pastDays: number;
169
+ futureDays: number;
170
+ rotateWhenMemberLeaves?: boolean;
171
+ }
172
+
173
+ export interface FeedStatus {
174
+ exists: boolean;
175
+ feedId: string | null;
176
+ isEnabled: boolean;
177
+ needsRegeneration: boolean;
178
+ tokenHint: string | null;
179
+ rotatedAt: string | null;
180
+ previousTokenExpiresAt: string | null;
181
+ lastFetchedAt: string | null;
182
+ lastFetchedClient: string | null;
183
+ fetchCount: number;
184
+ lastRenderTruncated: boolean;
185
+ settings: FeedStatusSettings;
186
+ urls: FeedUrls | null;
187
+ hostWarning: string | null;
188
+ protocolWarning: string | null;
189
+ }
190
+
191
+ export interface MyShiftsResponse {
192
+ shifts: Array<JSONObject>;
193
+ truncated: boolean;
194
+ generatedAt: string;
195
+ }
196
+
197
+ /*
198
+ * The columns every feed row shares, as the status builder reads them. The
199
+ * three models are structurally alike here; typing the slice keeps the
200
+ * builder free of casts.
201
+ */
202
+ interface FeedRowLike {
203
+ id?: ObjectID | null | undefined;
204
+ _id?: string | undefined;
205
+ isEnabled?: boolean | undefined;
206
+ tokenHint?: string | undefined;
207
+ tokenHash?: string | undefined;
208
+ token?: string | undefined;
209
+ rotatedAt?: Date | undefined;
210
+ previousTokenExpiresAt?: Date | undefined;
211
+ lastFetchedAt?: Date | undefined;
212
+ lastFetchedClient?: string | undefined;
213
+ fetchCount?: number | undefined;
214
+ lastRenderTruncated?: boolean | undefined;
215
+ pastDays?: number | undefined;
216
+ futureDays?: number | undefined;
217
+ includeCoveringShifts?: boolean | undefined;
218
+ includeCoverageGaps?: boolean | undefined;
219
+ minimumGapMinutes?: number | undefined;
220
+ rotateWhenMemberLeaves?: boolean | undefined;
221
+ }
222
+
223
+ type AnyFeedRow =
224
+ | UserOnCallCalendarFeed
225
+ | OnCallDutyPolicyScheduleCalendarFeed
226
+ | ProjectOnCallCalendarFeed;
227
+
228
+ interface TokenLookupResult<TFeed> {
229
+ feed: TFeed;
230
+ /* Matched the rotated-out hash, still inside its grace period. */
231
+ viaPreviousToken: boolean;
232
+ }
233
+
234
+ /*
235
+ * 415. ExceptionCode has no member named for it; the numeric value is what
236
+ * res.status() needs and what the client sees.
237
+ */
238
+ class UnsupportedMediaTypeException extends Exception {
239
+ public constructor(message: string) {
240
+ super(415 as ExceptionCode, message);
241
+ }
242
+ }
243
+
244
+ /* 405, same reasoning as the 415 above. */
245
+ class MethodNotAllowedException extends Exception {
246
+ public constructor(message: string) {
247
+ super(405 as ExceptionCode, message);
248
+ }
249
+ }
250
+
251
+ // -- Public-route middleware ----------------------------------------------
252
+
253
+ /*
254
+ * Kill switch. Answered BEFORE the rate limiter so a switched-off install
255
+ * spends no Redis round trips per poll. 503 + Retry-After: clients keep the
256
+ * copy they have and come back later, which is the point of a switch that
257
+ * says "not now" rather than "never".
258
+ */
259
+ export function killSwitchMiddleware(
260
+ req: ExpressRequest,
261
+ res: ExpressResponse,
262
+ next: NextFunction,
263
+ ): void {
264
+ if (DisableOnCallCalendarFeed) {
265
+ res.set("Retry-After", String(KILL_SWITCH_RETRY_AFTER_SECONDS));
266
+ return Response.sendErrorResponse(
267
+ req,
268
+ res,
269
+ new ServiceUnavailableException(
270
+ "On-call calendar feeds are disabled on this OneUptime instance.",
271
+ ),
272
+ );
273
+ }
274
+
275
+ return next();
276
+ }
277
+
278
+ /*
279
+ * The X-Forwarded-Proto entry written by OUR proxy, read the way ClientIp
280
+ * reads X-Forwarded-For: counting in from the right by TRUSTED_PROXY_HOPS,
281
+ * never the leftmost (caller-supplied) entry. With no trusted proxies there
282
+ * is no trustworthy header at all, and undefined is returned.
283
+ */
284
+ export function resolveTrustedForwardedProto(
285
+ req: ExpressRequest,
286
+ options?: { trustedProxyHops?: number | undefined } | undefined,
287
+ ): string | undefined {
288
+ const configuredHops: number =
289
+ options?.trustedProxyHops === undefined
290
+ ? TrustedProxyHops
291
+ : options.trustedProxyHops;
292
+
293
+ const hops: number =
294
+ Number.isFinite(configuredHops) && configuredHops > 0
295
+ ? Math.floor(configuredHops)
296
+ : 0;
297
+
298
+ if (hops === 0) {
299
+ return undefined;
300
+ }
301
+
302
+ const header: string | Array<string> | undefined = req.headers?.[
303
+ "x-forwarded-proto"
304
+ ] as string | Array<string> | undefined;
305
+
306
+ if (header === undefined || header === null) {
307
+ return undefined;
308
+ }
309
+
310
+ const raw: string = Array.isArray(header) ? header.join(",") : header;
311
+
312
+ if (!raw.trim()) {
313
+ return undefined;
314
+ }
315
+
316
+ const entries: Array<string> = raw.split(",");
317
+
318
+ if (entries.length < hops) {
319
+ return undefined;
320
+ }
321
+
322
+ const entry: string | undefined = entries[entries.length - hops];
323
+
324
+ return entry ? entry.trim().toLowerCase() : undefined;
325
+ }
326
+
327
+ /*
328
+ * Scheme guard. An https instance whose proxy reports the request arrived
329
+ * over plain http answers 301 to the https URL, so a link pasted without the
330
+ * "s" is corrected once instead of served in the clear forever. The target
331
+ * is built from HOST, never from the request's own Host header (an open
332
+ * redirect otherwise). Nginx only does this itself when billing is on.
333
+ *
334
+ * It only fires when OUR nginx terminates TLS (PROVISION_SSL=true). Every
335
+ * proxying location in Nginx/default.conf.template sets
336
+ * `X-Forwarded-Proto $scheme`, REPLACING whatever an outer proxy sent, so on
337
+ * an install that terminates TLS on an external reverse proxy
338
+ * (PROVISION_SSL=false with HTTP_PROTOCOL=https -- the topology
339
+ * config.example.env documents) nginx always reports `http` even though the
340
+ * client spoke https. Redirecting there would 301 to the very URL the client
341
+ * just asked for: an endless loop, ERR_TOO_MANY_REDIRECTS in a browser and a
342
+ * dead subscription in every calendar client. Serving the feed is the right
343
+ * answer; the settings page still shows `protocolWarning` when the install
344
+ * really is plain http.
345
+ */
346
+ export function schemeGuardMiddleware(
347
+ req: ExpressRequest,
348
+ res: ExpressResponse,
349
+ next: NextFunction,
350
+ ): void {
351
+ if (HttpProtocol !== Protocol.HTTPS) {
352
+ return next();
353
+ }
354
+
355
+ if (!ProvisionSsl) {
356
+ return next();
357
+ }
358
+
359
+ const forwardedProto: string | undefined = resolveTrustedForwardedProto(req);
360
+
361
+ if (forwardedProto !== "http") {
362
+ return next();
363
+ }
364
+
365
+ const host: string = OnCallCalendarFeedUrls.normalizeHost(Host);
366
+
367
+ if (!host) {
368
+ return next();
369
+ }
370
+
371
+ const originalUrl: string = req.originalUrl || req.url || "/";
372
+
373
+ res.set("Cache-Control", "no-store");
374
+ res.redirect(301, `${Protocol.HTTPS}${host}${originalUrl}`);
375
+ }
376
+
377
+ // -- Helpers ----------------------------------------------------------------
378
+
379
+ function sendNotFound(req: ExpressRequest, res: ExpressResponse): void {
380
+ return Response.sendErrorResponse(
381
+ req,
382
+ res,
383
+ new NotFoundException(NOT_FOUND_MESSAGE),
384
+ );
385
+ }
386
+
387
+ function sendUnavailable(
388
+ req: ExpressRequest,
389
+ res: ExpressResponse,
390
+ retryAfterSeconds: number,
391
+ ): void {
392
+ res.set("Retry-After", String(retryAfterSeconds));
393
+
394
+ return Response.sendErrorResponse(
395
+ req,
396
+ res,
397
+ new ServiceUnavailableException(
398
+ "The calendar feed cannot be rendered right now. Please try again later.",
399
+ ),
400
+ );
401
+ }
402
+
403
+ /*
404
+ * The answer to every near miss on a token-bearing path. It says nothing
405
+ * about the URL it was asked for: the message is a constant, so neither the
406
+ * response body nor the error log line sendErrorResponse writes can carry
407
+ * the token. OPTIONS is answered the way OPTIONS should be; any other wrong
408
+ * method gets 405 + Allow (a client can fix that); a wrong or missing
409
+ * filename gets the same generic 404 an unknown token gets.
410
+ */
411
+ export const FEED_ALLOWED_METHODS: string = "GET, HEAD, OPTIONS";
412
+
413
+ export function feedFallbackHandler(
414
+ req: ExpressRequest,
415
+ res: ExpressResponse,
416
+ ): void {
417
+ const method: string = (req.method || "GET").toUpperCase();
418
+
419
+ if (method === "OPTIONS") {
420
+ res.set("Allow", FEED_ALLOWED_METHODS);
421
+
422
+ return Response.sendEmptySuccessResponse(req, res);
423
+ }
424
+
425
+ if (method !== "GET" && method !== "HEAD") {
426
+ res.set("Allow", FEED_ALLOWED_METHODS);
427
+
428
+ return Response.sendErrorResponse(
429
+ req,
430
+ res,
431
+ new MethodNotAllowedException(
432
+ "Calendar feeds are read with GET on the full feed URL.",
433
+ ),
434
+ );
435
+ }
436
+
437
+ return sendNotFound(req, res);
438
+ }
439
+
440
+ /*
441
+ * The `?schedule=` filter of the personal feed. Absent -> no filter. Present
442
+ * but not a UUID -> null, which the route answers 404: a URL the user
443
+ * believed was filtered must never quietly render the whole feed.
444
+ */
445
+ export function readScheduleFilter(
446
+ req: ExpressRequest,
447
+ ): ObjectID | undefined | null {
448
+ const raw: unknown = req.query?.["schedule"];
449
+
450
+ if (raw === undefined || raw === null) {
451
+ return undefined;
452
+ }
453
+
454
+ const value: string = Array.isArray(raw) ? String(raw[0] ?? "") : String(raw);
455
+
456
+ const trimmed: string = value.trim();
457
+
458
+ if (!trimmed) {
459
+ return undefined;
460
+ }
461
+
462
+ if (!ObjectID.isValidUUID(trimmed)) {
463
+ return null;
464
+ }
465
+
466
+ return new ObjectID(trimmed);
467
+ }
468
+
469
+ /*
470
+ * Coarse client family only, from the User-Agent. The raw string is never
471
+ * stored: it is high-cardinality, occasionally carries device identifiers,
472
+ * and the settings page only wants to say "Google Calendar, 12 minutes ago".
473
+ */
474
+ export function classifyCalendarClient(
475
+ userAgent: string | Array<string> | undefined,
476
+ ): string | null {
477
+ const raw: string = (
478
+ Array.isArray(userAgent) ? userAgent.join(" ") : userAgent || ""
479
+ ).trim();
480
+
481
+ if (!raw) {
482
+ return null;
483
+ }
484
+
485
+ const value: string = raw.toLowerCase();
486
+
487
+ if (value.includes("google-calendar") || value.includes("googlecalendar")) {
488
+ return "Google Calendar";
489
+ }
490
+
491
+ if (
492
+ value.includes("outlook") ||
493
+ value.includes("microsoft office") ||
494
+ value.includes("exchange") ||
495
+ value.includes("microsoft-cryptoapi")
496
+ ) {
497
+ return "Microsoft Outlook";
498
+ }
499
+
500
+ if (
501
+ value.includes("calendaragent") ||
502
+ value.includes("dataaccessd") ||
503
+ value.includes("ios/") ||
504
+ value.includes("macos/") ||
505
+ value.includes("darwin") ||
506
+ value.includes("ical")
507
+ ) {
508
+ return "Apple Calendar";
509
+ }
510
+
511
+ if (value.includes("thunderbird") || value.includes("lightning")) {
512
+ return "Thunderbird";
513
+ }
514
+
515
+ if (value.includes("okhttp") || value.includes("android")) {
516
+ return "Android app";
517
+ }
518
+
519
+ if (
520
+ value.includes("mozilla") ||
521
+ value.includes("chrome") ||
522
+ value.includes("safari") ||
523
+ value.includes("firefox") ||
524
+ value.includes("edge")
525
+ ) {
526
+ return "Browser";
527
+ }
528
+
529
+ if (value.includes("curl") || value.includes("wget")) {
530
+ return "Command line";
531
+ }
532
+
533
+ return "Other";
534
+ }
535
+
536
+ function toDate(value: unknown): Date | null {
537
+ if (value instanceof Date) {
538
+ return Number.isNaN(value.getTime()) ? null : value;
539
+ }
540
+
541
+ if (typeof value === "string" && value) {
542
+ const parsed: Date = OneUptimeDate.fromString(value);
543
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
544
+ }
545
+
546
+ return null;
547
+ }
548
+
549
+ function toIso(value: unknown): string | null {
550
+ const date: Date | null = toDate(value);
551
+ return date ? date.toISOString() : null;
552
+ }
553
+
554
+ /*
555
+ * Is a previousTokenHash match still honoured? Only while
556
+ * previousTokenExpiresAt lies in the future; a rotated-out hash past its
557
+ * grace is indistinguishable from an unknown one (404).
558
+ */
559
+ export function isPreviousTokenInGrace(
560
+ feed: { previousTokenExpiresAt?: Date | undefined },
561
+ now: Date,
562
+ ): boolean {
563
+ const expiresAt: Date | null = toDate(feed.previousTokenExpiresAt);
564
+
565
+ if (!expiresAt) {
566
+ return false;
567
+ }
568
+
569
+ return expiresAt.getTime() > now.getTime();
570
+ }
571
+
572
+ /*
573
+ * Bookkeeping is only worth a write once in a while, and only for a real
574
+ * fetch: a HEAD is a client checking whether anything changed, not a read.
575
+ */
576
+ export function shouldRecordFetch(data: {
577
+ method: string | undefined;
578
+ lastFetchedAt: Date | undefined | null;
579
+ now: Date;
580
+ }): boolean {
581
+ if ((data.method || "GET").toUpperCase() !== "GET") {
582
+ return false;
583
+ }
584
+
585
+ const last: Date | null = toDate(data.lastFetchedAt);
586
+
587
+ if (!last) {
588
+ return true;
589
+ }
590
+
591
+ return data.now.getTime() - last.getTime() >= BOOKKEEPING_INTERVAL_MS;
592
+ }
593
+
594
+ function readObjectIdParam(req: ExpressRequest, name: string): ObjectID {
595
+ const raw: unknown = req.params?.[name];
596
+
597
+ if (typeof raw !== "string" || !ObjectID.isValidUUID(raw.trim())) {
598
+ throw new BadDataException(`${name} must be a valid id.`);
599
+ }
600
+
601
+ return new ObjectID(raw.trim());
602
+ }
603
+
604
+ /*
605
+ * POST bodies on the session routes must be application/json, else 415. The
606
+ * routes carry no payload, so this is not about parsing: a JSON content type
607
+ * cannot be produced by a cross-site form post, which is what keeps a
608
+ * "rotate my link" request from being triggered by a page the user did not
609
+ * mean to visit.
610
+ */
611
+ export function assertJsonRequest(req: ExpressRequest): void {
612
+ const contentType: string | Array<string> | undefined = req.headers?.[
613
+ "content-type"
614
+ ] as string | Array<string> | undefined;
615
+
616
+ const value: string = (
617
+ Array.isArray(contentType) ? contentType[0] || "" : contentType || ""
618
+ )
619
+ .trim()
620
+ .toLowerCase();
621
+
622
+ if (!value.startsWith("application/json")) {
623
+ throw new UnsupportedMediaTypeException(
624
+ "This endpoint expects a JSON request (Content-Type: application/json).",
625
+ );
626
+ }
627
+ }
628
+
629
+ /*
630
+ * Parse ?from / ?to for /my-shifts. Defaults now -> +30 d; the span is
631
+ * capped at 120 d by moving `to` in, never by refusing the request.
632
+ *
633
+ * The window is then clamped into the same range the feeds themselves can
634
+ * address -- [now - MAX_PAST_DAYS, now + MAX_FUTURE_DAYS]. Without that,
635
+ * `?from=2500-01-01` was accepted: every distinct day-aligned window is a
636
+ * fresh schedule-cache entry AND a fresh LayerUtil expansion that walks one
637
+ * rotation period at a time from the layer's start to the window, which for a
638
+ * far-future start means the full 200,000-iteration cap per restricted layer
639
+ * -- seconds of synchronous CPU per request, holding one of the four
640
+ * per-process render slots the public feeds share. Clamping keeps a session
641
+ * request no more expensive than a feed poll and keeps it on the same cache
642
+ * keys.
643
+ */
644
+ export function readMyShiftsWindow(
645
+ req: ExpressRequest,
646
+ now: Date,
647
+ ): { from: Date; to: Date } {
648
+ const rawFrom: unknown = req.query?.["from"];
649
+ const rawTo: unknown = req.query?.["to"];
650
+
651
+ let from: Date = now;
652
+
653
+ if (typeof rawFrom === "string" && rawFrom.trim()) {
654
+ if (!OneUptimeDate.isValidDateString(rawFrom.trim())) {
655
+ throw new BadDataException("from must be an ISO 8601 date.");
656
+ }
657
+
658
+ from = OneUptimeDate.fromString(rawFrom.trim());
659
+ }
660
+
661
+ let to: Date = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_DEFAULT_DAYS);
662
+
663
+ if (typeof rawTo === "string" && rawTo.trim()) {
664
+ if (!OneUptimeDate.isValidDateString(rawTo.trim())) {
665
+ throw new BadDataException("to must be an ISO 8601 date.");
666
+ }
667
+
668
+ to = OneUptimeDate.fromString(rawTo.trim());
669
+ }
670
+
671
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
672
+ throw new BadDataException("from and to must be ISO 8601 dates.");
673
+ }
674
+
675
+ if (to.getTime() <= from.getTime()) {
676
+ throw new BadDataException("to must be after from.");
677
+ }
678
+
679
+ /*
680
+ * The addressable range. It is 240 days wide, so it always leaves room for
681
+ * a non-empty window whatever the caller asked for.
682
+ */
683
+ const earliestFrom: Date = OneUptimeDate.addRemoveDays(now, -MAX_PAST_DAYS);
684
+ const latestTo: Date = OneUptimeDate.addRemoveDays(now, MAX_FUTURE_DAYS);
685
+ const latestFrom: Date = OneUptimeDate.addRemoveDays(latestTo, -1);
686
+
687
+ if (from.getTime() < earliestFrom.getTime()) {
688
+ from = earliestFrom;
689
+ }
690
+
691
+ if (from.getTime() > latestFrom.getTime()) {
692
+ from = latestFrom;
693
+ }
694
+
695
+ /*
696
+ * `to` was validated against the RAW `from`; once `from` has moved, a `to`
697
+ * that now lies at or before it is answered with the default span rather
698
+ * than an empty window.
699
+ */
700
+ if (to.getTime() <= from.getTime()) {
701
+ to = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_DEFAULT_DAYS);
702
+ }
703
+
704
+ const spanCap: Date = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_MAX_DAYS);
705
+
706
+ if (to.getTime() > spanCap.getTime()) {
707
+ to = spanCap;
708
+ }
709
+
710
+ if (to.getTime() > latestTo.getTime()) {
711
+ to = latestTo;
712
+ }
713
+
714
+ return { from, to };
715
+ }
716
+
717
+ // -- FeedStatus -------------------------------------------------------------
718
+
719
+ function defaultSettings(kind: OnCallCalendarFeedKind): FeedStatusSettings {
720
+ if (kind === OnCallCalendarFeedKind.Personal) {
721
+ return {
722
+ includeCoveringShifts: true,
723
+ pastDays: DEFAULT_PAST_DAYS,
724
+ futureDays: DEFAULT_FUTURE_DAYS,
725
+ };
726
+ }
727
+
728
+ return {
729
+ includeCoverageGaps: false,
730
+ minimumGapMinutes: 60,
731
+ pastDays: DEFAULT_PAST_DAYS,
732
+ futureDays: DEFAULT_FUTURE_DAYS,
733
+ rotateWhenMemberLeaves: false,
734
+ };
735
+ }
736
+
737
+ /*
738
+ * FeedStatus for a feed that does not exist (yet).
739
+ */
740
+ export function buildAbsentFeedStatus(
741
+ kind: OnCallCalendarFeedKind,
742
+ ): FeedStatus {
743
+ return {
744
+ exists: false,
745
+ feedId: null,
746
+ isEnabled: false,
747
+ needsRegeneration: false,
748
+ tokenHint: null,
749
+ rotatedAt: null,
750
+ previousTokenExpiresAt: null,
751
+ lastFetchedAt: null,
752
+ lastFetchedClient: null,
753
+ fetchCount: 0,
754
+ lastRenderTruncated: false,
755
+ settings: defaultSettings(kind),
756
+ urls: null,
757
+ hostWarning: OnCallCalendarFeedUrls.getHostWarning(),
758
+ protocolWarning: OnCallCalendarFeedUrls.getProtocolWarning(),
759
+ };
760
+ }
761
+
762
+ /*
763
+ * FeedStatus for an existing row. `plaintextToken` is the token just minted
764
+ * by a rotate/publish (known without a decrypt); otherwise `decryptedToken`
765
+ * is what the root read handed back, verified against the stored hash. A
766
+ * token that fails to decrypt or does not hash to tokenHash means the
767
+ * ENCRYPTION_SECRET changed under the row: the URL cannot be shown and the
768
+ * UI offers "Regenerate link".
769
+ *
770
+ * The hash the decrypted token is checked against is `verifiedTokenHash` --
771
+ * the one readTokenForStatus read alongside the token -- falling back to the
772
+ * row's own `tokenHash`. The status rows the routes read do NOT carry
773
+ * tokenHash (STATUS_SELECT cannot select it: the column's read access list is
774
+ * empty, so the non-root schedule/project reads would be refused), and
775
+ * requiring it here is what made every /current and /publish answer
776
+ * `needsRegeneration: true, urls: null` for a perfectly good feed.
777
+ */
778
+ export function buildFeedStatus(data: {
779
+ kind: OnCallCalendarFeedKind;
780
+ feed: FeedRowLike;
781
+ plaintextToken?: string | undefined;
782
+ decryptedToken?: string | undefined;
783
+ decryptFailed?: boolean | undefined;
784
+ verifiedTokenHash?: string | undefined;
785
+ }): FeedStatus {
786
+ const feed: FeedRowLike = data.feed;
787
+
788
+ const expectedTokenHash: string | undefined =
789
+ feed.tokenHash || data.verifiedTokenHash;
790
+
791
+ let token: string | null = null;
792
+ let needsRegeneration: boolean = false;
793
+
794
+ if (
795
+ data.plaintextToken &&
796
+ CalendarFeedToken.isValidShape(data.plaintextToken)
797
+ ) {
798
+ token = data.plaintextToken;
799
+ } else if (data.decryptFailed) {
800
+ needsRegeneration = true;
801
+ } else if (
802
+ data.decryptedToken &&
803
+ CalendarFeedToken.isValidShape(data.decryptedToken) &&
804
+ expectedTokenHash &&
805
+ CalendarFeedToken.isHashEqual(
806
+ CalendarFeedToken.hash(data.decryptedToken),
807
+ expectedTokenHash,
808
+ )
809
+ ) {
810
+ token = data.decryptedToken;
811
+ } else {
812
+ needsRegeneration = true;
813
+ }
814
+
815
+ const settings: FeedStatusSettings =
816
+ data.kind === OnCallCalendarFeedKind.Personal
817
+ ? {
818
+ includeCoveringShifts: feed.includeCoveringShifts !== false,
819
+ pastDays: feed.pastDays ?? DEFAULT_PAST_DAYS,
820
+ futureDays: feed.futureDays ?? DEFAULT_FUTURE_DAYS,
821
+ }
822
+ : {
823
+ includeCoverageGaps: feed.includeCoverageGaps === true,
824
+ minimumGapMinutes: feed.minimumGapMinutes ?? 60,
825
+ pastDays: feed.pastDays ?? DEFAULT_PAST_DAYS,
826
+ futureDays: feed.futureDays ?? DEFAULT_FUTURE_DAYS,
827
+ rotateWhenMemberLeaves: feed.rotateWhenMemberLeaves === true,
828
+ };
829
+
830
+ const feedId: string | null = feed.id
831
+ ? feed.id.toString()
832
+ : feed._id
833
+ ? feed._id.toString()
834
+ : null;
835
+
836
+ return {
837
+ exists: true,
838
+ feedId,
839
+ isEnabled: feed.isEnabled !== false,
840
+ needsRegeneration,
841
+ tokenHint: feed.tokenHint || null,
842
+ rotatedAt: toIso(feed.rotatedAt),
843
+ previousTokenExpiresAt: toIso(feed.previousTokenExpiresAt),
844
+ lastFetchedAt: toIso(feed.lastFetchedAt),
845
+ lastFetchedClient: feed.lastFetchedClient || null,
846
+ fetchCount: typeof feed.fetchCount === "number" ? feed.fetchCount : 0,
847
+ lastRenderTruncated: feed.lastRenderTruncated === true,
848
+ settings,
849
+ urls: token
850
+ ? OnCallCalendarFeedUrls.buildFeedUrls({ kind: data.kind, token })
851
+ : null,
852
+ hostWarning: OnCallCalendarFeedUrls.getHostWarning(),
853
+ protocolWarning: OnCallCalendarFeedUrls.getProtocolWarning(),
854
+ };
855
+ }
856
+
857
+ /*
858
+ * The one decrypting read. Selecting `token` makes DatabaseService run
859
+ * Encryption.decrypt on the way out; with the wrong secret that either
860
+ * throws or yields bytes that do not hash to tokenHash. Both become
861
+ * needsRegeneration, never a 500.
862
+ */
863
+ async function readTokenForStatus(data: {
864
+ kind: OnCallCalendarFeedKind;
865
+ id: ObjectID;
866
+ }): Promise<{
867
+ decryptedToken?: string | undefined;
868
+ /* The tokenHash this read verified the token against. */
869
+ verifiedTokenHash?: string | undefined;
870
+ decryptFailed: boolean;
871
+ }> {
872
+ try {
873
+ let row: FeedRowLike | null = null;
874
+
875
+ if (data.kind === OnCallCalendarFeedKind.Personal) {
876
+ row = await UserOnCallCalendarFeedService.findOneById({
877
+ id: data.id,
878
+ select: { _id: true, token: true, tokenHash: true },
879
+ props: { isRoot: true, ignoreHooks: true },
880
+ });
881
+ } else if (data.kind === OnCallCalendarFeedKind.Schedule) {
882
+ row = await OnCallDutyPolicyScheduleCalendarFeedService.findOneById({
883
+ id: data.id,
884
+ select: { _id: true, token: true, tokenHash: true },
885
+ props: { isRoot: true, ignoreHooks: true },
886
+ });
887
+ } else {
888
+ row = await ProjectOnCallCalendarFeedService.findOneById({
889
+ id: data.id,
890
+ select: { _id: true, token: true, tokenHash: true },
891
+ props: { isRoot: true, ignoreHooks: true },
892
+ });
893
+ }
894
+
895
+ if (!row || !row.token) {
896
+ return { decryptFailed: true };
897
+ }
898
+
899
+ if (
900
+ !row.tokenHash ||
901
+ !CalendarFeedToken.isValidShape(row.token) ||
902
+ !CalendarFeedToken.isHashEqual(
903
+ CalendarFeedToken.hash(row.token),
904
+ row.tokenHash,
905
+ )
906
+ ) {
907
+ return { decryptFailed: true };
908
+ }
909
+
910
+ return {
911
+ decryptedToken: row.token,
912
+ verifiedTokenHash: row.tokenHash,
913
+ decryptFailed: false,
914
+ };
915
+ } catch (err) {
916
+ logger.warn(
917
+ `OnCallCalendarAPI: could not decrypt the ${data.kind} calendar feed token for feed ${data.id.toString()}; the link needs regenerating.`,
918
+ );
919
+ logger.warn(err);
920
+ return { decryptFailed: true };
921
+ }
922
+ }
923
+
924
+ async function buildStatusForRow(data: {
925
+ kind: OnCallCalendarFeedKind;
926
+ feed: AnyFeedRow;
927
+ plaintextToken?: string | undefined;
928
+ }): Promise<FeedStatus> {
929
+ if (data.plaintextToken) {
930
+ return buildFeedStatus({
931
+ kind: data.kind,
932
+ feed: data.feed as FeedRowLike,
933
+ plaintextToken: data.plaintextToken,
934
+ });
935
+ }
936
+
937
+ if (!data.feed.id) {
938
+ return buildFeedStatus({
939
+ kind: data.kind,
940
+ feed: data.feed as FeedRowLike,
941
+ decryptFailed: true,
942
+ });
943
+ }
944
+
945
+ const decrypted: {
946
+ decryptedToken?: string | undefined;
947
+ verifiedTokenHash?: string | undefined;
948
+ decryptFailed: boolean;
949
+ } = await readTokenForStatus({ kind: data.kind, id: data.feed.id });
950
+
951
+ return buildFeedStatus({
952
+ kind: data.kind,
953
+ feed: data.feed as FeedRowLike,
954
+ decryptedToken: decrypted.decryptedToken,
955
+ verifiedTokenHash: decrypted.verifiedTokenHash,
956
+ decryptFailed: decrypted.decryptFailed,
957
+ });
958
+ }
959
+
960
+ /* The non-secret columns a status read selects (any of the three models). */
961
+ const STATUS_SELECT: {
962
+ _id: true;
963
+ projectId: true;
964
+ isEnabled: true;
965
+ tokenHint: true;
966
+ rotatedAt: true;
967
+ previousTokenExpiresAt: true;
968
+ lastFetchedAt: true;
969
+ lastFetchedClient: true;
970
+ fetchCount: true;
971
+ lastRenderTruncated: true;
972
+ pastDays: true;
973
+ futureDays: true;
974
+ } = {
975
+ _id: true,
976
+ projectId: true,
977
+ isEnabled: true,
978
+ tokenHint: true,
979
+ rotatedAt: true,
980
+ previousTokenExpiresAt: true,
981
+ lastFetchedAt: true,
982
+ lastFetchedClient: true,
983
+ fetchCount: true,
984
+ lastRenderTruncated: true,
985
+ pastDays: true,
986
+ futureDays: true,
987
+ };
988
+
989
+ // -- Public feed lookup -----------------------------------------------------
990
+
991
+ /*
992
+ * The columns the PUBLIC path reads. `token` is never among them: the public
993
+ * routes must keep working after an ENCRYPTION_SECRET rotation, and a select
994
+ * that included the encrypted column would make DatabaseService decrypt it on
995
+ * every poll. Tests pin this.
996
+ */
997
+ const PUBLIC_COMMON_SELECT: {
998
+ _id: true;
999
+ projectId: true;
1000
+ isEnabled: true;
1001
+ pastDays: true;
1002
+ futureDays: true;
1003
+ lastFetchedAt: true;
1004
+ fetchCount: true;
1005
+ previousTokenExpiresAt: true;
1006
+ } = {
1007
+ _id: true,
1008
+ projectId: true,
1009
+ isEnabled: true,
1010
+ pastDays: true,
1011
+ futureDays: true,
1012
+ lastFetchedAt: true,
1013
+ fetchCount: true,
1014
+ previousTokenExpiresAt: true,
1015
+ };
1016
+
1017
+ async function lookupPersonalFeed(
1018
+ tokenHash: string,
1019
+ now: Date,
1020
+ ): Promise<TokenLookupResult<UserOnCallCalendarFeed> | null> {
1021
+ const select: typeof PUBLIC_COMMON_SELECT & {
1022
+ userId: true;
1023
+ includeCoveringShifts: true;
1024
+ } = {
1025
+ ...PUBLIC_COMMON_SELECT,
1026
+ userId: true,
1027
+ includeCoveringShifts: true,
1028
+ };
1029
+
1030
+ const current: UserOnCallCalendarFeed | null =
1031
+ await UserOnCallCalendarFeedService.findOneBy({
1032
+ query: { tokenHash },
1033
+ select,
1034
+ props: { isRoot: true, ignoreHooks: true },
1035
+ });
1036
+
1037
+ if (current) {
1038
+ return { feed: current, viaPreviousToken: false };
1039
+ }
1040
+
1041
+ const previous: UserOnCallCalendarFeed | null =
1042
+ await UserOnCallCalendarFeedService.findOneBy({
1043
+ query: { previousTokenHash: tokenHash },
1044
+ select,
1045
+ props: { isRoot: true, ignoreHooks: true },
1046
+ });
1047
+
1048
+ if (!previous || !isPreviousTokenInGrace(previous, now)) {
1049
+ return null;
1050
+ }
1051
+
1052
+ return { feed: previous, viaPreviousToken: true };
1053
+ }
1054
+
1055
+ async function lookupScheduleFeed(
1056
+ tokenHash: string,
1057
+ now: Date,
1058
+ ): Promise<TokenLookupResult<OnCallDutyPolicyScheduleCalendarFeed> | null> {
1059
+ const select: typeof PUBLIC_COMMON_SELECT & {
1060
+ onCallDutyPolicyScheduleId: true;
1061
+ includeCoverageGaps: true;
1062
+ minimumGapMinutes: true;
1063
+ } = {
1064
+ ...PUBLIC_COMMON_SELECT,
1065
+ onCallDutyPolicyScheduleId: true,
1066
+ includeCoverageGaps: true,
1067
+ minimumGapMinutes: true,
1068
+ };
1069
+
1070
+ const current: OnCallDutyPolicyScheduleCalendarFeed | null =
1071
+ await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
1072
+ query: { tokenHash },
1073
+ select,
1074
+ props: { isRoot: true, ignoreHooks: true },
1075
+ });
1076
+
1077
+ if (current) {
1078
+ return { feed: current, viaPreviousToken: false };
1079
+ }
1080
+
1081
+ const previous: OnCallDutyPolicyScheduleCalendarFeed | null =
1082
+ await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
1083
+ query: { previousTokenHash: tokenHash },
1084
+ select,
1085
+ props: { isRoot: true, ignoreHooks: true },
1086
+ });
1087
+
1088
+ if (!previous || !isPreviousTokenInGrace(previous, now)) {
1089
+ return null;
1090
+ }
1091
+
1092
+ return { feed: previous, viaPreviousToken: true };
1093
+ }
1094
+
1095
+ async function lookupProjectFeed(
1096
+ tokenHash: string,
1097
+ now: Date,
1098
+ ): Promise<TokenLookupResult<ProjectOnCallCalendarFeed> | null> {
1099
+ const select: typeof PUBLIC_COMMON_SELECT & {
1100
+ includeCoverageGaps: true;
1101
+ minimumGapMinutes: true;
1102
+ } = {
1103
+ ...PUBLIC_COMMON_SELECT,
1104
+ includeCoverageGaps: true,
1105
+ minimumGapMinutes: true,
1106
+ };
1107
+
1108
+ const current: ProjectOnCallCalendarFeed | null =
1109
+ await ProjectOnCallCalendarFeedService.findOneBy({
1110
+ query: { tokenHash },
1111
+ select,
1112
+ props: { isRoot: true, ignoreHooks: true },
1113
+ });
1114
+
1115
+ if (current) {
1116
+ return { feed: current, viaPreviousToken: false };
1117
+ }
1118
+
1119
+ const previous: ProjectOnCallCalendarFeed | null =
1120
+ await ProjectOnCallCalendarFeedService.findOneBy({
1121
+ query: { previousTokenHash: tokenHash },
1122
+ select,
1123
+ props: { isRoot: true, ignoreHooks: true },
1124
+ });
1125
+
1126
+ if (!previous || !isPreviousTokenInGrace(previous, now)) {
1127
+ return null;
1128
+ }
1129
+
1130
+ return { feed: previous, viaPreviousToken: true };
1131
+ }
1132
+
1133
+ // -- Bookkeeping ------------------------------------------------------------
1134
+
1135
+ /*
1136
+ * Fire-and-forget root update after a served GET: when it was fetched, by
1137
+ * what kind of client, how many times (read-modify-write of the value the
1138
+ * lookup already read, so approximate under concurrency and deliberately
1139
+ * so), and whether the last render was cut. Never awaited by the route, never
1140
+ * allowed to throw into it.
1141
+ */
1142
+ function recordFetch(data: {
1143
+ kind: OnCallCalendarFeedKind;
1144
+ feed: AnyFeedRow;
1145
+ req: ExpressRequest;
1146
+ now: Date;
1147
+ truncated: boolean;
1148
+ }): void {
1149
+ if (!data.feed.id) {
1150
+ return;
1151
+ }
1152
+
1153
+ if (
1154
+ !shouldRecordFetch({
1155
+ method: data.req.method,
1156
+ lastFetchedAt: data.feed.lastFetchedAt,
1157
+ now: data.now,
1158
+ })
1159
+ ) {
1160
+ return;
1161
+ }
1162
+
1163
+ const client: string | null = classifyCalendarClient(
1164
+ data.req.headers?.["user-agent"] as string | Array<string> | undefined,
1165
+ );
1166
+
1167
+ const previousCount: number =
1168
+ typeof data.feed.fetchCount === "number" &&
1169
+ Number.isFinite(data.feed.fetchCount)
1170
+ ? data.feed.fetchCount
1171
+ : Number(data.feed.fetchCount) || 0;
1172
+
1173
+ const update: {
1174
+ lastFetchedAt: Date;
1175
+ lastFetchedClient: string;
1176
+ fetchCount: number;
1177
+ lastRenderTruncated: boolean;
1178
+ } = {
1179
+ lastFetchedAt: data.now,
1180
+ lastFetchedClient: client || "Other",
1181
+ fetchCount: previousCount + 1,
1182
+ lastRenderTruncated: data.truncated,
1183
+ };
1184
+
1185
+ const id: ObjectID = data.feed.id;
1186
+
1187
+ let write: Promise<unknown>;
1188
+
1189
+ if (data.kind === OnCallCalendarFeedKind.Personal) {
1190
+ write = UserOnCallCalendarFeedService.updateOneById({
1191
+ id,
1192
+ data: update,
1193
+ props: { isRoot: true, ignoreHooks: true },
1194
+ });
1195
+ } else if (data.kind === OnCallCalendarFeedKind.Schedule) {
1196
+ write = OnCallDutyPolicyScheduleCalendarFeedService.updateOneById({
1197
+ id,
1198
+ data: update,
1199
+ props: { isRoot: true, ignoreHooks: true },
1200
+ });
1201
+ } else {
1202
+ write = ProjectOnCallCalendarFeedService.updateOneById({
1203
+ id,
1204
+ data: update,
1205
+ props: { isRoot: true, ignoreHooks: true },
1206
+ });
1207
+ }
1208
+
1209
+ write.catch((err: unknown) => {
1210
+ logger.warn(
1211
+ `OnCallCalendarAPI: bookkeeping for the ${data.kind} calendar feed ${id.toString()} failed.`,
1212
+ );
1213
+ logger.warn(err);
1214
+ });
1215
+ }
1216
+
1217
+ // -- Public routes ----------------------------------------------------------
1218
+
1219
+ /*
1220
+ * The shared tail of the three public routes, from "we have a lookup result"
1221
+ * to "bytes went out". Everything kind-specific is in the render request.
1222
+ */
1223
+ async function serveFeed(data: {
1224
+ kind: OnCallCalendarFeedKind;
1225
+ req: ExpressRequest;
1226
+ res: ExpressResponse;
1227
+ lookup: TokenLookupResult<AnyFeedRow>;
1228
+ request: FeedRenderRequest;
1229
+ now: Date;
1230
+ emptyNames?:
1231
+ | {
1232
+ scheduleName?: string | undefined;
1233
+ projectName?: string | undefined;
1234
+ }
1235
+ | undefined;
1236
+ }): Promise<void> {
1237
+ let outcome: FeedRenderOutcome;
1238
+
1239
+ if (data.lookup.viaPreviousToken) {
1240
+ outcome = OnCallCalendarFeedRenderer.buildEmptyOutcome({
1241
+ kind: data.kind,
1242
+ reason: TOKEN_ROTATED_REASON,
1243
+ now: data.now,
1244
+ });
1245
+ } else if (data.lookup.feed.isEnabled === false) {
1246
+ outcome = OnCallCalendarFeedRenderer.buildEmptyOutcome({
1247
+ kind: data.kind,
1248
+ reason: FEED_DISABLED_REASON,
1249
+ now: data.now,
1250
+ });
1251
+ } else {
1252
+ outcome = await OnCallCalendarFeedRenderer.render(data.request);
1253
+ }
1254
+
1255
+ if (outcome.status === FeedRenderStatus.Unavailable) {
1256
+ return sendUnavailable(
1257
+ data.req,
1258
+ data.res,
1259
+ outcome.retryAfterSeconds || KILL_SWITCH_RETRY_AFTER_SECONDS,
1260
+ );
1261
+ }
1262
+
1263
+ if (outcome.stale) {
1264
+ data.res.set("Warning", STALE_WARNING_HEADER);
1265
+ }
1266
+
1267
+ Response.sendCalendarResponse(data.req, data.res, {
1268
+ body: outcome.body,
1269
+ etag: outcome.etag,
1270
+ lastModified: outcome.lastModified,
1271
+ });
1272
+
1273
+ /*
1274
+ * Only a real, current, enabled feed is worth bookkeeping. An empty
1275
+ * calendar served for a rotated-out link or a disabled feed is not a
1276
+ * fetch the settings page should count -- the two conditions above say so
1277
+ * exactly. Everything else that went out as a 200 IS a fetch, including the
1278
+ * empty calendars the renderer produces for "you are not on a schedule yet"
1279
+ * or "this project is below plan": those are precisely the users staring at
1280
+ * an empty calendar and asking "is my calendar app even reaching the
1281
+ * server?", which lastFetchedAt/lastFetchedClient exist to answer. Only an
1282
+ * Unavailable outcome is skipped, and that one already returned above
1283
+ * without sending a body.
1284
+ */
1285
+ if (!data.lookup.viaPreviousToken && data.lookup.feed.isEnabled !== false) {
1286
+ recordFetch({
1287
+ kind: data.kind,
1288
+ feed: data.lookup.feed,
1289
+ req: data.req,
1290
+ now: data.now,
1291
+ truncated: outcome.truncated || outcome.stale,
1292
+ });
1293
+ }
1294
+ }
1295
+
1296
+ router.get(
1297
+ PERSONAL_FEED_ROUTE,
1298
+ killSwitchMiddleware,
1299
+ schemeGuardMiddleware,
1300
+ OnCallCalendarFeedRateLimit.getMiddleware(),
1301
+ async (
1302
+ req: ExpressRequest,
1303
+ res: ExpressResponse,
1304
+ next: NextFunction,
1305
+ ): Promise<void> => {
1306
+ try {
1307
+ const token: unknown = req.params?.["token"];
1308
+
1309
+ if (!CalendarFeedToken.isValidShape(token)) {
1310
+ return sendNotFound(req, res);
1311
+ }
1312
+
1313
+ const scheduleFilter: ObjectID | undefined | null =
1314
+ readScheduleFilter(req);
1315
+
1316
+ if (scheduleFilter === null) {
1317
+ return sendNotFound(req, res);
1318
+ }
1319
+
1320
+ const now: Date = OneUptimeDate.getCurrentDate();
1321
+ const tokenHash: string = CalendarFeedToken.hash(token);
1322
+
1323
+ const lookup: TokenLookupResult<UserOnCallCalendarFeed> | null =
1324
+ await lookupPersonalFeed(tokenHash, now);
1325
+
1326
+ if (!lookup || !lookup.feed.id || !lookup.feed.projectId) {
1327
+ return sendNotFound(req, res);
1328
+ }
1329
+
1330
+ if (!lookup.feed.userId) {
1331
+ return sendNotFound(req, res);
1332
+ }
1333
+
1334
+ return await serveFeed({
1335
+ kind: OnCallCalendarFeedKind.Personal,
1336
+ req,
1337
+ res,
1338
+ lookup,
1339
+ now,
1340
+ request: {
1341
+ kind: OnCallCalendarFeedKind.Personal,
1342
+ feedId: lookup.feed.id,
1343
+ projectId: lookup.feed.projectId,
1344
+ userId: lookup.feed.userId,
1345
+ tokenHash,
1346
+ includeCoveringShifts: lookup.feed.includeCoveringShifts !== false,
1347
+ pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
1348
+ futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
1349
+ scheduleFilterId: scheduleFilter,
1350
+ now,
1351
+ },
1352
+ });
1353
+ } catch (err) {
1354
+ next(err);
1355
+ }
1356
+ },
1357
+ );
1358
+
1359
+ router.get(
1360
+ SCHEDULE_FEED_ROUTE,
1361
+ killSwitchMiddleware,
1362
+ schemeGuardMiddleware,
1363
+ OnCallCalendarFeedRateLimit.getMiddleware(),
1364
+ async (
1365
+ req: ExpressRequest,
1366
+ res: ExpressResponse,
1367
+ next: NextFunction,
1368
+ ): Promise<void> => {
1369
+ try {
1370
+ const token: unknown = req.params?.["token"];
1371
+
1372
+ if (!CalendarFeedToken.isValidShape(token)) {
1373
+ return sendNotFound(req, res);
1374
+ }
1375
+
1376
+ const now: Date = OneUptimeDate.getCurrentDate();
1377
+ const tokenHash: string = CalendarFeedToken.hash(token);
1378
+
1379
+ const lookup: TokenLookupResult<OnCallDutyPolicyScheduleCalendarFeed> | null =
1380
+ await lookupScheduleFeed(tokenHash, now);
1381
+
1382
+ if (
1383
+ !lookup ||
1384
+ !lookup.feed.id ||
1385
+ !lookup.feed.projectId ||
1386
+ !lookup.feed.onCallDutyPolicyScheduleId
1387
+ ) {
1388
+ return sendNotFound(req, res);
1389
+ }
1390
+
1391
+ return await serveFeed({
1392
+ kind: OnCallCalendarFeedKind.Schedule,
1393
+ req,
1394
+ res,
1395
+ lookup,
1396
+ now,
1397
+ request: {
1398
+ kind: OnCallCalendarFeedKind.Schedule,
1399
+ feedId: lookup.feed.id,
1400
+ projectId: lookup.feed.projectId,
1401
+ scheduleId: lookup.feed.onCallDutyPolicyScheduleId,
1402
+ tokenHash,
1403
+ includeCoverageGaps: lookup.feed.includeCoverageGaps === true,
1404
+ minimumGapMinutes: lookup.feed.minimumGapMinutes ?? 60,
1405
+ pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
1406
+ futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
1407
+ now,
1408
+ },
1409
+ });
1410
+ } catch (err) {
1411
+ next(err);
1412
+ }
1413
+ },
1414
+ );
1415
+
1416
+ router.get(
1417
+ PROJECT_FEED_ROUTE,
1418
+ killSwitchMiddleware,
1419
+ schemeGuardMiddleware,
1420
+ OnCallCalendarFeedRateLimit.getMiddleware(),
1421
+ async (
1422
+ req: ExpressRequest,
1423
+ res: ExpressResponse,
1424
+ next: NextFunction,
1425
+ ): Promise<void> => {
1426
+ try {
1427
+ const token: unknown = req.params?.["token"];
1428
+
1429
+ if (!CalendarFeedToken.isValidShape(token)) {
1430
+ return sendNotFound(req, res);
1431
+ }
1432
+
1433
+ const now: Date = OneUptimeDate.getCurrentDate();
1434
+ const tokenHash: string = CalendarFeedToken.hash(token);
1435
+
1436
+ const lookup: TokenLookupResult<ProjectOnCallCalendarFeed> | null =
1437
+ await lookupProjectFeed(tokenHash, now);
1438
+
1439
+ if (!lookup || !lookup.feed.id || !lookup.feed.projectId) {
1440
+ return sendNotFound(req, res);
1441
+ }
1442
+
1443
+ return await serveFeed({
1444
+ kind: OnCallCalendarFeedKind.Project,
1445
+ req,
1446
+ res,
1447
+ lookup,
1448
+ now,
1449
+ request: {
1450
+ kind: OnCallCalendarFeedKind.Project,
1451
+ feedId: lookup.feed.id,
1452
+ projectId: lookup.feed.projectId,
1453
+ tokenHash,
1454
+ includeCoverageGaps: lookup.feed.includeCoverageGaps === true,
1455
+ minimumGapMinutes: lookup.feed.minimumGapMinutes ?? 60,
1456
+ pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
1457
+ futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
1458
+ now,
1459
+ },
1460
+ });
1461
+ } catch (err) {
1462
+ next(err);
1463
+ }
1464
+ },
1465
+ );
1466
+
1467
+ /*
1468
+ * Registered AFTER the three exact GET routes, so those still win, and
1469
+ * before the session routes, whose paths (`feed/`, `schedule-feed/`,
1470
+ * `project-feed/`, `my-shifts`) share no prefix with these.
1471
+ */
1472
+ router.all(PERSONAL_FEED_FALLBACK_ROUTE, feedFallbackHandler);
1473
+ router.all(SCHEDULE_FEED_FALLBACK_ROUTE, feedFallbackHandler);
1474
+ router.all(PROJECT_FEED_FALLBACK_ROUTE, feedFallbackHandler);
1475
+
1476
+ // -- Session routes: personal feed -----------------------------------------
1477
+
1478
+ /*
1479
+ * The caller's own feed row in the tenant project. Root read: the row is
1480
+ * addressed by (projectId, userId) where userId is the SESSION's user, so
1481
+ * the model's CurrentUser predicate is satisfied by construction and a
1482
+ * non-root read would only add a permission round trip.
1483
+ */
1484
+ async function findPersonalFeed(data: {
1485
+ projectId: ObjectID;
1486
+ userId: ObjectID;
1487
+ }): Promise<UserOnCallCalendarFeed | null> {
1488
+ return await UserOnCallCalendarFeedService.findOneBy({
1489
+ query: {
1490
+ projectId: data.projectId,
1491
+ userId: data.userId,
1492
+ },
1493
+ select: {
1494
+ ...STATUS_SELECT,
1495
+ userId: true,
1496
+ includeCoveringShifts: true,
1497
+ },
1498
+ props: { isRoot: true, ignoreHooks: true },
1499
+ });
1500
+ }
1501
+
1502
+ function requireUserId(props: DatabaseCommonInteractionProps): ObjectID {
1503
+ if (!props.userId) {
1504
+ throw new BadDataException("A logged-in user is required.");
1505
+ }
1506
+
1507
+ return props.userId;
1508
+ }
1509
+
1510
+ router.get(
1511
+ FEED_CURRENT_ROUTE,
1512
+ UserMiddleware.getUserMiddleware,
1513
+ UserMiddleware.requireUserAuthentication,
1514
+ async (
1515
+ req: ExpressRequest,
1516
+ res: ExpressResponse,
1517
+ next: NextFunction,
1518
+ ): Promise<void> => {
1519
+ try {
1520
+ const props: DatabaseCommonInteractionProps =
1521
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1522
+
1523
+ const projectId: ObjectID =
1524
+ CommonAPI.assertAuthenticatedProjectMember(props);
1525
+ const userId: ObjectID = requireUserId(props);
1526
+
1527
+ const feed: UserOnCallCalendarFeed | null = await findPersonalFeed({
1528
+ projectId,
1529
+ userId,
1530
+ });
1531
+
1532
+ if (!feed) {
1533
+ return Response.sendJsonObjectResponse(
1534
+ req,
1535
+ res,
1536
+ buildAbsentFeedStatus(
1537
+ OnCallCalendarFeedKind.Personal,
1538
+ ) as unknown as JSONObject,
1539
+ );
1540
+ }
1541
+
1542
+ const status: FeedStatus = await buildStatusForRow({
1543
+ kind: OnCallCalendarFeedKind.Personal,
1544
+ feed,
1545
+ });
1546
+
1547
+ return Response.sendJsonObjectResponse(
1548
+ req,
1549
+ res,
1550
+ status as unknown as JSONObject,
1551
+ );
1552
+ } catch (err) {
1553
+ next(err);
1554
+ }
1555
+ },
1556
+ );
1557
+
1558
+ /*
1559
+ * Mint-or-rotate under a per-(project, user) lock. The UNIQUE(projectId,
1560
+ * userId) index is the backstop; the lock is what turns a double click into
1561
+ * one new link rather than one new link and one 500. Without Redis the lock
1562
+ * cannot be taken, and the index alone has to do -- logged, not fatal.
1563
+ */
1564
+ async function withRotateLock<T>(
1565
+ key: string,
1566
+ work: () => Promise<T>,
1567
+ ): Promise<T> {
1568
+ let mutex: SemaphoreMutex | null = null;
1569
+
1570
+ try {
1571
+ mutex = await Semaphore.lock({
1572
+ key,
1573
+ namespace: ROTATE_LOCK_NAMESPACE,
1574
+ lockTimeout: ROTATE_LOCK_TIMEOUT_MS,
1575
+ });
1576
+ } catch (err) {
1577
+ logger.warn(
1578
+ "OnCallCalendarAPI: could not take the rotate lock; relying on the unique index.",
1579
+ );
1580
+ logger.warn(err);
1581
+ mutex = null;
1582
+ }
1583
+
1584
+ try {
1585
+ return await work();
1586
+ } finally {
1587
+ if (mutex) {
1588
+ try {
1589
+ await Semaphore.release(mutex);
1590
+ } catch (err) {
1591
+ logger.warn("OnCallCalendarAPI: releasing the rotate lock failed.");
1592
+ logger.warn(err);
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ router.post(
1599
+ FEED_ROTATE_ROUTE,
1600
+ UserMiddleware.getUserMiddleware,
1601
+ UserMiddleware.requireUserAuthentication,
1602
+ async (
1603
+ req: ExpressRequest,
1604
+ res: ExpressResponse,
1605
+ next: NextFunction,
1606
+ ): Promise<void> => {
1607
+ try {
1608
+ assertJsonRequest(req);
1609
+
1610
+ const props: DatabaseCommonInteractionProps =
1611
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1612
+
1613
+ const projectId: ObjectID =
1614
+ CommonAPI.assertAuthenticatedProjectMember(props);
1615
+ const userId: ObjectID = requireUserId(props);
1616
+
1617
+ const plaintextToken: string = await withRotateLock(
1618
+ `${projectId.toString()}-${userId.toString()}`,
1619
+ async (): Promise<string> => {
1620
+ const existing: UserOnCallCalendarFeed | null =
1621
+ await UserOnCallCalendarFeedService.findOneBy({
1622
+ query: { projectId, userId },
1623
+ select: { _id: true },
1624
+ props: { isRoot: true, ignoreHooks: true },
1625
+ });
1626
+
1627
+ if (!existing || !existing.id) {
1628
+ const created: {
1629
+ feed: UserOnCallCalendarFeed;
1630
+ minted: MintedCalendarFeedToken;
1631
+ } = await UserOnCallCalendarFeedService.createForUser({
1632
+ projectId,
1633
+ userId,
1634
+ });
1635
+
1636
+ return created.minted.token;
1637
+ }
1638
+
1639
+ const rotation: CalendarFeedRotation =
1640
+ await UserOnCallCalendarFeedService.rotateTokenById({
1641
+ id: existing.id,
1642
+ });
1643
+
1644
+ return rotation.token;
1645
+ },
1646
+ );
1647
+
1648
+ await OnCallCalendarFeedCache.purgeForUser(
1649
+ projectId.toString(),
1650
+ userId.toString(),
1651
+ );
1652
+
1653
+ const feed: UserOnCallCalendarFeed | null = await findPersonalFeed({
1654
+ projectId,
1655
+ userId,
1656
+ });
1657
+
1658
+ if (!feed) {
1659
+ throw new NotFoundException("Calendar feed not found.");
1660
+ }
1661
+
1662
+ const status: FeedStatus = await buildStatusForRow({
1663
+ kind: OnCallCalendarFeedKind.Personal,
1664
+ feed,
1665
+ plaintextToken,
1666
+ });
1667
+
1668
+ return Response.sendJsonObjectResponse(
1669
+ req,
1670
+ res,
1671
+ status as unknown as JSONObject,
1672
+ );
1673
+ } catch (err) {
1674
+ next(err);
1675
+ }
1676
+ },
1677
+ );
1678
+
1679
+ // -- Session routes: schedule feed -----------------------------------------
1680
+
1681
+ /*
1682
+ * NON-root read with the caller's props: the copied TableAccessControl and
1683
+ * the @CanAccessIfCanReadOn label scoping decide whether this caller may see
1684
+ * that the feed exists at all.
1685
+ */
1686
+ async function findScheduleFeedAsCaller(data: {
1687
+ scheduleId: ObjectID;
1688
+ projectId: ObjectID;
1689
+ props: DatabaseCommonInteractionProps;
1690
+ }): Promise<OnCallDutyPolicyScheduleCalendarFeed | null> {
1691
+ return await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
1692
+ query: {
1693
+ onCallDutyPolicyScheduleId: data.scheduleId,
1694
+ projectId: data.projectId,
1695
+ },
1696
+ select: {
1697
+ ...STATUS_SELECT,
1698
+ onCallDutyPolicyScheduleId: true,
1699
+ includeCoverageGaps: true,
1700
+ minimumGapMinutes: true,
1701
+ rotateWhenMemberLeaves: true,
1702
+ },
1703
+ props: data.props,
1704
+ });
1705
+ }
1706
+
1707
+ router.get(
1708
+ SCHEDULE_FEED_CURRENT_ROUTE,
1709
+ UserMiddleware.getUserMiddleware,
1710
+ UserMiddleware.requireUserAuthentication,
1711
+ async (
1712
+ req: ExpressRequest,
1713
+ res: ExpressResponse,
1714
+ next: NextFunction,
1715
+ ): Promise<void> => {
1716
+ try {
1717
+ const props: DatabaseCommonInteractionProps =
1718
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1719
+
1720
+ const projectId: ObjectID =
1721
+ CommonAPI.assertAuthenticatedProjectMember(props);
1722
+ const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
1723
+
1724
+ const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
1725
+ await findScheduleFeedAsCaller({ scheduleId, projectId, props });
1726
+
1727
+ if (!feed) {
1728
+ return Response.sendJsonObjectResponse(
1729
+ req,
1730
+ res,
1731
+ buildAbsentFeedStatus(
1732
+ OnCallCalendarFeedKind.Schedule,
1733
+ ) as unknown as JSONObject,
1734
+ );
1735
+ }
1736
+
1737
+ const status: FeedStatus = await buildStatusForRow({
1738
+ kind: OnCallCalendarFeedKind.Schedule,
1739
+ feed,
1740
+ });
1741
+
1742
+ return Response.sendJsonObjectResponse(
1743
+ req,
1744
+ res,
1745
+ status as unknown as JSONObject,
1746
+ );
1747
+ } catch (err) {
1748
+ next(err);
1749
+ }
1750
+ },
1751
+ );
1752
+
1753
+ /*
1754
+ * Publish = a NON-root create with the caller's props, so DatabaseService
1755
+ * runs the schedule's Create permission check and the service hook mints the
1756
+ * token (anything the request carried is discarded). A feed that already
1757
+ * exists is re-enabled through a non-root update instead -- same permission
1758
+ * gate, and the existing link keeps working for everyone who already has it.
1759
+ */
1760
+ router.post(
1761
+ SCHEDULE_FEED_PUBLISH_ROUTE,
1762
+ UserMiddleware.getUserMiddleware,
1763
+ UserMiddleware.requireUserAuthentication,
1764
+ async (
1765
+ req: ExpressRequest,
1766
+ res: ExpressResponse,
1767
+ next: NextFunction,
1768
+ ): Promise<void> => {
1769
+ try {
1770
+ assertJsonRequest(req);
1771
+
1772
+ const props: DatabaseCommonInteractionProps =
1773
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1774
+
1775
+ const projectId: ObjectID =
1776
+ CommonAPI.assertAuthenticatedProjectMember(props);
1777
+ const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
1778
+
1779
+ const existing: OnCallDutyPolicyScheduleCalendarFeed | null =
1780
+ await findScheduleFeedAsCaller({ scheduleId, projectId, props });
1781
+
1782
+ if (existing && existing.id) {
1783
+ await OnCallDutyPolicyScheduleCalendarFeedService.updateOneBy({
1784
+ query: { _id: existing.id, projectId },
1785
+ data: { isEnabled: true },
1786
+ props,
1787
+ });
1788
+ } else {
1789
+ /*
1790
+ * The create alone is not enough of a gate. Its permission check
1791
+ * (CreatePermission) looks at table and column permissions and at
1792
+ * ownership; @CanAccessIfCanReadOn -- the label scoping that decides
1793
+ * WHICH schedules an editor may touch -- is only applied to query
1794
+ * operations (read/update/delete), and the service's own check runs
1795
+ * as root. So a label-restricted editor who knows a schedule's id
1796
+ * could publish a feed for a schedule outside their labels. Reading
1797
+ * the schedule with the caller's props first applies exactly the
1798
+ * scoping /current and /rotate already get.
1799
+ */
1800
+ const schedule: OnCallDutyPolicySchedule | null =
1801
+ await OnCallDutyPolicyScheduleService.findOneBy({
1802
+ query: { _id: scheduleId, projectId },
1803
+ select: { _id: true },
1804
+ props,
1805
+ });
1806
+
1807
+ if (!schedule) {
1808
+ throw new NotFoundException("On-call schedule not found.");
1809
+ }
1810
+
1811
+ const model: OnCallDutyPolicyScheduleCalendarFeed =
1812
+ new OnCallDutyPolicyScheduleCalendarFeed();
1813
+ model.projectId = projectId;
1814
+ model.onCallDutyPolicyScheduleId = scheduleId;
1815
+
1816
+ await OnCallDutyPolicyScheduleCalendarFeedService.create({
1817
+ data: model,
1818
+ props,
1819
+ });
1820
+ }
1821
+
1822
+ await OnCallCalendarFeedCache.purgeForSchedule(scheduleId.toString());
1823
+
1824
+ const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
1825
+ await findScheduleFeedAsCaller({ scheduleId, projectId, props });
1826
+
1827
+ if (!feed) {
1828
+ throw new NotFoundException("Calendar feed not found.");
1829
+ }
1830
+
1831
+ const status: FeedStatus = await buildStatusForRow({
1832
+ kind: OnCallCalendarFeedKind.Schedule,
1833
+ feed,
1834
+ });
1835
+
1836
+ return Response.sendJsonObjectResponse(
1837
+ req,
1838
+ res,
1839
+ status as unknown as JSONObject,
1840
+ );
1841
+ } catch (err) {
1842
+ next(err);
1843
+ }
1844
+ },
1845
+ );
1846
+
1847
+ /*
1848
+ * Rotate = a NON-root update of an updatable column (isEnabled, written back
1849
+ * with its current value) so the Edit permission check runs, THEN the root
1850
+ * mint. The token columns themselves are root-only on update, which is why
1851
+ * the permission probe cannot be the rotation write itself.
1852
+ */
1853
+ router.post(
1854
+ SCHEDULE_FEED_ROTATE_ROUTE,
1855
+ UserMiddleware.getUserMiddleware,
1856
+ UserMiddleware.requireUserAuthentication,
1857
+ async (
1858
+ req: ExpressRequest,
1859
+ res: ExpressResponse,
1860
+ next: NextFunction,
1861
+ ): Promise<void> => {
1862
+ try {
1863
+ assertJsonRequest(req);
1864
+
1865
+ const props: DatabaseCommonInteractionProps =
1866
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1867
+
1868
+ const projectId: ObjectID =
1869
+ CommonAPI.assertAuthenticatedProjectMember(props);
1870
+ const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
1871
+
1872
+ const existing: OnCallDutyPolicyScheduleCalendarFeed | null =
1873
+ await findScheduleFeedAsCaller({ scheduleId, projectId, props });
1874
+
1875
+ if (!existing || !existing.id) {
1876
+ throw new NotFoundException(
1877
+ "This schedule has no shared calendar feed yet. Publish one first.",
1878
+ );
1879
+ }
1880
+
1881
+ await OnCallDutyPolicyScheduleCalendarFeedService.updateOneBy({
1882
+ query: { _id: existing.id, projectId },
1883
+ data: { isEnabled: existing.isEnabled !== false },
1884
+ props,
1885
+ });
1886
+
1887
+ const rotation: CalendarFeedRotation =
1888
+ await OnCallDutyPolicyScheduleCalendarFeedService.rotateTokenById({
1889
+ id: existing.id,
1890
+ });
1891
+
1892
+ await OnCallCalendarFeedCache.purgeForSchedule(scheduleId.toString());
1893
+
1894
+ const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
1895
+ await findScheduleFeedAsCaller({ scheduleId, projectId, props });
1896
+
1897
+ if (!feed) {
1898
+ throw new NotFoundException("Calendar feed not found.");
1899
+ }
1900
+
1901
+ const status: FeedStatus = await buildStatusForRow({
1902
+ kind: OnCallCalendarFeedKind.Schedule,
1903
+ feed,
1904
+ plaintextToken: rotation.token,
1905
+ });
1906
+
1907
+ return Response.sendJsonObjectResponse(
1908
+ req,
1909
+ res,
1910
+ status as unknown as JSONObject,
1911
+ );
1912
+ } catch (err) {
1913
+ next(err);
1914
+ }
1915
+ },
1916
+ );
1917
+
1918
+ // -- Session routes: project feed ------------------------------------------
1919
+
1920
+ async function findProjectFeedAsCaller(data: {
1921
+ projectId: ObjectID;
1922
+ props: DatabaseCommonInteractionProps;
1923
+ }): Promise<ProjectOnCallCalendarFeed | null> {
1924
+ return await ProjectOnCallCalendarFeedService.findOneBy({
1925
+ query: {
1926
+ projectId: data.projectId,
1927
+ },
1928
+ select: {
1929
+ ...STATUS_SELECT,
1930
+ includeCoverageGaps: true,
1931
+ minimumGapMinutes: true,
1932
+ rotateWhenMemberLeaves: true,
1933
+ },
1934
+ props: data.props,
1935
+ });
1936
+ }
1937
+
1938
+ router.get(
1939
+ PROJECT_FEED_CURRENT_ROUTE,
1940
+ UserMiddleware.getUserMiddleware,
1941
+ UserMiddleware.requireUserAuthentication,
1942
+ async (
1943
+ req: ExpressRequest,
1944
+ res: ExpressResponse,
1945
+ next: NextFunction,
1946
+ ): Promise<void> => {
1947
+ try {
1948
+ const props: DatabaseCommonInteractionProps =
1949
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1950
+
1951
+ const projectId: ObjectID =
1952
+ CommonAPI.assertAuthenticatedProjectMember(props);
1953
+
1954
+ const feed: ProjectOnCallCalendarFeed | null =
1955
+ await findProjectFeedAsCaller({ projectId, props });
1956
+
1957
+ if (!feed) {
1958
+ return Response.sendJsonObjectResponse(
1959
+ req,
1960
+ res,
1961
+ buildAbsentFeedStatus(
1962
+ OnCallCalendarFeedKind.Project,
1963
+ ) as unknown as JSONObject,
1964
+ );
1965
+ }
1966
+
1967
+ const status: FeedStatus = await buildStatusForRow({
1968
+ kind: OnCallCalendarFeedKind.Project,
1969
+ feed,
1970
+ });
1971
+
1972
+ return Response.sendJsonObjectResponse(
1973
+ req,
1974
+ res,
1975
+ status as unknown as JSONObject,
1976
+ );
1977
+ } catch (err) {
1978
+ next(err);
1979
+ }
1980
+ },
1981
+ );
1982
+
1983
+ router.post(
1984
+ PROJECT_FEED_PUBLISH_ROUTE,
1985
+ UserMiddleware.getUserMiddleware,
1986
+ UserMiddleware.requireUserAuthentication,
1987
+ async (
1988
+ req: ExpressRequest,
1989
+ res: ExpressResponse,
1990
+ next: NextFunction,
1991
+ ): Promise<void> => {
1992
+ try {
1993
+ assertJsonRequest(req);
1994
+
1995
+ const props: DatabaseCommonInteractionProps =
1996
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
1997
+
1998
+ const projectId: ObjectID =
1999
+ CommonAPI.assertAuthenticatedProjectMember(props);
2000
+
2001
+ const existing: ProjectOnCallCalendarFeed | null =
2002
+ await findProjectFeedAsCaller({ projectId, props });
2003
+
2004
+ if (existing && existing.id) {
2005
+ await ProjectOnCallCalendarFeedService.updateOneBy({
2006
+ query: { _id: existing.id, projectId },
2007
+ data: { isEnabled: true },
2008
+ props,
2009
+ });
2010
+ } else {
2011
+ const model: ProjectOnCallCalendarFeed =
2012
+ new ProjectOnCallCalendarFeed();
2013
+ model.projectId = projectId;
2014
+
2015
+ await ProjectOnCallCalendarFeedService.create({
2016
+ data: model,
2017
+ props,
2018
+ });
2019
+ }
2020
+
2021
+ await OnCallCalendarFeedCache.purgeForProject(projectId.toString());
2022
+
2023
+ const feed: ProjectOnCallCalendarFeed | null =
2024
+ await findProjectFeedAsCaller({ projectId, props });
2025
+
2026
+ if (!feed) {
2027
+ throw new NotFoundException("Calendar feed not found.");
2028
+ }
2029
+
2030
+ const status: FeedStatus = await buildStatusForRow({
2031
+ kind: OnCallCalendarFeedKind.Project,
2032
+ feed,
2033
+ });
2034
+
2035
+ return Response.sendJsonObjectResponse(
2036
+ req,
2037
+ res,
2038
+ status as unknown as JSONObject,
2039
+ );
2040
+ } catch (err) {
2041
+ next(err);
2042
+ }
2043
+ },
2044
+ );
2045
+
2046
+ router.post(
2047
+ PROJECT_FEED_ROTATE_ROUTE,
2048
+ UserMiddleware.getUserMiddleware,
2049
+ UserMiddleware.requireUserAuthentication,
2050
+ async (
2051
+ req: ExpressRequest,
2052
+ res: ExpressResponse,
2053
+ next: NextFunction,
2054
+ ): Promise<void> => {
2055
+ try {
2056
+ assertJsonRequest(req);
2057
+
2058
+ const props: DatabaseCommonInteractionProps =
2059
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
2060
+
2061
+ const projectId: ObjectID =
2062
+ CommonAPI.assertAuthenticatedProjectMember(props);
2063
+
2064
+ const existing: ProjectOnCallCalendarFeed | null =
2065
+ await findProjectFeedAsCaller({ projectId, props });
2066
+
2067
+ if (!existing || !existing.id) {
2068
+ throw new NotFoundException(
2069
+ "This project has no shared calendar feed yet. Publish one first.",
2070
+ );
2071
+ }
2072
+
2073
+ await ProjectOnCallCalendarFeedService.updateOneBy({
2074
+ query: { _id: existing.id, projectId },
2075
+ data: { isEnabled: existing.isEnabled !== false },
2076
+ props,
2077
+ });
2078
+
2079
+ const rotation: CalendarFeedRotation =
2080
+ await ProjectOnCallCalendarFeedService.rotateTokenById({
2081
+ id: existing.id,
2082
+ });
2083
+
2084
+ await OnCallCalendarFeedCache.purgeForProject(projectId.toString());
2085
+
2086
+ const feed: ProjectOnCallCalendarFeed | null =
2087
+ await findProjectFeedAsCaller({ projectId, props });
2088
+
2089
+ if (!feed) {
2090
+ throw new NotFoundException("Calendar feed not found.");
2091
+ }
2092
+
2093
+ const status: FeedStatus = await buildStatusForRow({
2094
+ kind: OnCallCalendarFeedKind.Project,
2095
+ feed,
2096
+ plaintextToken: rotation.token,
2097
+ });
2098
+
2099
+ return Response.sendJsonObjectResponse(
2100
+ req,
2101
+ res,
2102
+ status as unknown as JSONObject,
2103
+ );
2104
+ } catch (err) {
2105
+ next(err);
2106
+ }
2107
+ },
2108
+ );
2109
+
2110
+ // -- Session route: /my-shifts ---------------------------------------------
2111
+
2112
+ /*
2113
+ * The caller's own upcoming shifts as JSON. Scoped to the tenant project
2114
+ * when a `tenantid` header is present (the dashboard), to every project the
2115
+ * caller is rostered in otherwise (the mobile app). Same resolver and
2116
+ * schedule-level cache as the feeds; the per-process render cap applies, and
2117
+ * a capped request is a 503 the mobile app answers by falling back to its
2118
+ * roster-derived list.
2119
+ */
2120
+ router.get(
2121
+ MY_SHIFTS_ROUTE,
2122
+ UserMiddleware.getUserMiddleware,
2123
+ UserMiddleware.requireUserAuthentication,
2124
+ async (
2125
+ req: ExpressRequest,
2126
+ res: ExpressResponse,
2127
+ next: NextFunction,
2128
+ ): Promise<void> => {
2129
+ try {
2130
+ const props: DatabaseCommonInteractionProps =
2131
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
2132
+
2133
+ const userId: ObjectID = requireUserId(props);
2134
+
2135
+ let projectIds: Array<ObjectID> | undefined = undefined;
2136
+
2137
+ if (props.tenantId) {
2138
+ projectIds = [CommonAPI.assertAuthenticatedProjectMember(props)];
2139
+ }
2140
+
2141
+ const now: Date = OneUptimeDate.getCurrentDate();
2142
+ const window: { from: Date; to: Date } = readMyShiftsWindow(req, now);
2143
+
2144
+ /*
2145
+ * Session renders leave slots free for the public feeds. /my-shifts is
2146
+ * called by a logged-in client that can retry and that falls back to
2147
+ * its roster list; a calendar client that gets a 503 shows a stale or
2148
+ * empty calendar to somebody who may be on call. With the default
2149
+ * concurrency of 4 this route may hold at most 2 slots at once.
2150
+ */
2151
+ if (
2152
+ !OnCallCalendarFeedCache.tryAcquireRenderSlot({
2153
+ leaveFreeSlots: Math.floor(
2154
+ OnCallCalendarFeedCache.getRenderConcurrency() / 2,
2155
+ ),
2156
+ })
2157
+ ) {
2158
+ return sendUnavailable(req, res, 60);
2159
+ }
2160
+
2161
+ let result: UserShiftsResult;
2162
+
2163
+ try {
2164
+ result = await OnCallCalendarFeedRenderer.materializeUserShifts({
2165
+ userId,
2166
+ projectIds,
2167
+ from: window.from,
2168
+ to: window.to,
2169
+ now,
2170
+ });
2171
+ } finally {
2172
+ OnCallCalendarFeedCache.releaseRenderSlot();
2173
+ }
2174
+
2175
+ const payload: MyShiftsResponse = {
2176
+ shifts: MaterializedShiftUtil.toJSONArray(
2177
+ result.shifts,
2178
+ ) as unknown as Array<JSONObject>,
2179
+ truncated: result.truncated,
2180
+ generatedAt: result.generatedAt.toISOString(),
2181
+ };
2182
+
2183
+ return Response.sendJsonObjectResponse(
2184
+ req,
2185
+ res,
2186
+ payload as unknown as JSONObject,
2187
+ );
2188
+ } catch (err) {
2189
+ next(err);
2190
+ }
2191
+ },
2192
+ );
2193
+
2194
+ export default router;