@oneuptime/common 10.4.12 → 10.4.14

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 (137) hide show
  1. package/Models/DatabaseModels/Alert.ts +2 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +1 -0
  3. package/Models/DatabaseModels/CallLog.ts +2 -0
  4. package/Models/DatabaseModels/DockerHost.ts +34 -0
  5. package/Models/DatabaseModels/EmailLog.ts +2 -0
  6. package/Models/DatabaseModels/Host.ts +34 -0
  7. package/Models/DatabaseModels/Incident.ts +1 -0
  8. package/Models/DatabaseModels/IncidentFeed.ts +1 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +34 -0
  10. package/Models/DatabaseModels/MonitorFeed.ts +1 -0
  11. package/Models/DatabaseModels/MonitorProbe.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +3 -0
  13. package/Models/DatabaseModels/SmsLog.ts +2 -0
  14. package/Models/DatabaseModels/StatusPageSubscriber.ts +2 -0
  15. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  16. package/Models/DatabaseModels/UserOnCallLog.ts +1 -0
  17. package/Models/DatabaseModels/WorkflowLog.ts +1 -0
  18. package/Server/API/ProjectAPI.ts +52 -15
  19. package/Server/EnvironmentConfig.ts +69 -0
  20. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +26 -1
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.ts +29 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.ts +160 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  24. package/Server/Infrastructure/PostgresDatabase.ts +2 -5
  25. package/Server/Middleware/ProjectAuthorization.ts +31 -53
  26. package/Server/Middleware/UserAuthorization.ts +106 -64
  27. package/Server/Services/ApiKeyService.ts +100 -1
  28. package/Server/Services/DockerHostService.ts +5 -0
  29. package/Server/Services/HostService.ts +6 -0
  30. package/Server/Services/KubernetesClusterService.ts +33 -10
  31. package/Server/Services/MonitorService.ts +10 -3
  32. package/Server/Services/ProjectService.ts +72 -1
  33. package/Server/Services/TeamMemberService.ts +36 -0
  34. package/Server/Services/UserService.ts +38 -0
  35. package/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.ts +108 -0
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +13 -0
  37. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +25 -0
  38. package/Types/Monitor/CriteriaFilter.ts +13 -0
  39. package/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.ts +69 -0
  40. package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
  41. package/Types/Monitor/MonitorStep.ts +39 -0
  42. package/Types/Monitor/MonitorStepDnssecMonitor.ts +59 -0
  43. package/Types/Monitor/MonitorType.ts +17 -1
  44. package/Types/Probe/ProbeMonitorResponse.ts +2 -0
  45. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +51 -0
  46. package/Utils/Monitor/MonitorMetricType.ts +1 -0
  47. package/build/dist/Models/DatabaseModels/Alert.js +3 -1
  48. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -1
  50. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/CallLog.js +4 -1
  52. package/build/dist/Models/DatabaseModels/CallLog.js.map +1 -1
  53. package/build/dist/Models/DatabaseModels/DockerHost.js +35 -0
  54. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/EmailLog.js +4 -1
  56. package/build/dist/Models/DatabaseModels/EmailLog.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/Host.js +35 -0
  58. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  59. package/build/dist/Models/DatabaseModels/Incident.js +2 -1
  60. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -1
  62. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +35 -0
  64. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/MonitorFeed.js +2 -1
  66. package/build/dist/Models/DatabaseModels/MonitorFeed.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/MonitorProbe.js +2 -0
  68. package/build/dist/Models/DatabaseModels/MonitorProbe.js.map +1 -1
  69. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +3 -0
  70. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  71. package/build/dist/Models/DatabaseModels/SmsLog.js +4 -1
  72. package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
  73. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +4 -1
  74. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  75. package/build/dist/Models/DatabaseModels/TelemetryException.js +3 -1
  76. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  77. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +1 -0
  78. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  79. package/build/dist/Models/DatabaseModels/WorkflowLog.js +2 -1
  80. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  81. package/build/dist/Server/API/ProjectAPI.js +42 -14
  82. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  83. package/build/dist/Server/EnvironmentConfig.js +41 -0
  84. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  85. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +20 -2
  86. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.js +16 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.js.map +1 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.js +63 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.js.map +1 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  93. package/build/dist/Server/Infrastructure/PostgresDatabase.js +2 -2
  94. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  95. package/build/dist/Server/Middleware/ProjectAuthorization.js +21 -39
  96. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  97. package/build/dist/Server/Middleware/UserAuthorization.js +83 -50
  98. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  99. package/build/dist/Server/Services/ApiKeyService.js +86 -0
  100. package/build/dist/Server/Services/ApiKeyService.js.map +1 -1
  101. package/build/dist/Server/Services/DockerHostService.js +5 -1
  102. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  103. package/build/dist/Server/Services/HostService.js +5 -1
  104. package/build/dist/Server/Services/HostService.js.map +1 -1
  105. package/build/dist/Server/Services/KubernetesClusterService.js +21 -11
  106. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  107. package/build/dist/Server/Services/MonitorService.js +8 -3
  108. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  109. package/build/dist/Server/Services/ProjectService.js +65 -1
  110. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  111. package/build/dist/Server/Services/TeamMemberService.js +24 -0
  112. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  113. package/build/dist/Server/Services/UserService.js +36 -0
  114. package/build/dist/Server/Services/UserService.js.map +1 -1
  115. package/build/dist/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.js +94 -0
  116. package/build/dist/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.js.map +1 -0
  117. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
  118. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  119. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -3
  120. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  121. package/build/dist/Types/Monitor/CriteriaFilter.js +12 -0
  122. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  123. package/build/dist/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.js +2 -0
  124. package/build/dist/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.js.map +1 -0
  125. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
  126. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  127. package/build/dist/Types/Monitor/MonitorStep.js +26 -0
  128. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  129. package/build/dist/Types/Monitor/MonitorStepDnssecMonitor.js +42 -0
  130. package/build/dist/Types/Monitor/MonitorStepDnssecMonitor.js.map +1 -0
  131. package/build/dist/Types/Monitor/MonitorType.js +15 -1
  132. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  133. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +47 -0
  134. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  135. package/build/dist/Utils/Monitor/MonitorMetricType.js +1 -0
  136. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  137. package/package.json +1 -1
@@ -132,6 +132,7 @@ export class Service extends DatabaseService<Model> {
132
132
  extra?: {
133
133
  osType?: string | undefined;
134
134
  osVersion?: string | undefined;
135
+ agentVersion?: string | undefined;
135
136
  },
136
137
  ): Promise<void> {
137
138
  const cacheKey: string = hostId.toString();
@@ -141,6 +142,7 @@ export class Service extends DatabaseService<Model> {
141
142
  JSON.stringify({
142
143
  osType: extra?.osType ?? null,
143
144
  osVersion: extra?.osVersion ?? null,
145
+ agentVersion: extra?.agentVersion ?? null,
144
146
  }),
145
147
  )
146
148
  .digest("hex");
@@ -173,6 +175,9 @@ export class Service extends DatabaseService<Model> {
173
175
  if (extra?.osVersion) {
174
176
  data.osVersion = extra.osVersion;
175
177
  }
178
+ if (extra?.agentVersion) {
179
+ data.agentVersion = extra.agentVersion;
180
+ }
176
181
 
177
182
  await this.updateOneById({
178
183
  id: hostId,
@@ -135,6 +135,7 @@ export class Service extends DatabaseService<Model> {
135
135
  containerRuntime?: string | undefined;
136
136
  dockerHostId?: ObjectID | undefined;
137
137
  kubernetesClusterId?: ObjectID | undefined;
138
+ agentVersion?: string | undefined;
138
139
  },
139
140
  ): Promise<void> {
140
141
  /*
@@ -204,6 +205,9 @@ export class Service extends DatabaseService<Model> {
204
205
  if (extra?.kubernetesClusterId) {
205
206
  data.kubernetesClusterId = extra.kubernetesClusterId;
206
207
  }
208
+ if (extra?.agentVersion) {
209
+ data.agentVersion = extra.agentVersion;
210
+ }
207
211
 
208
212
  await this.updateOneById({
209
213
  id: hostId,
@@ -227,6 +231,7 @@ export class Service extends DatabaseService<Model> {
227
231
  containerRuntime?: string | undefined;
228
232
  dockerHostId?: ObjectID | undefined;
229
233
  kubernetesClusterId?: ObjectID | undefined;
234
+ agentVersion?: string | undefined;
230
235
  }): string {
231
236
  const normalized: Record<string, string | number | null> = {
232
237
  osType: extra?.osType ?? null,
@@ -241,6 +246,7 @@ export class Service extends DatabaseService<Model> {
241
246
  containerRuntime: extra?.containerRuntime ?? null,
242
247
  dockerHostId: extra?.dockerHostId?.toString() ?? null,
243
248
  kubernetesClusterId: extra?.kubernetesClusterId?.toString() ?? null,
249
+ agentVersion: extra?.agentVersion ?? null,
244
250
  };
245
251
 
246
252
  return crypto
@@ -127,28 +127,51 @@ export class Service extends DatabaseService<Model> {
127
127
  }
128
128
 
129
129
  @CaptureSpan()
130
- public async updateLastSeen(clusterId: ObjectID): Promise<void> {
130
+ public async updateLastSeen(
131
+ clusterId: ObjectID,
132
+ extra?: {
133
+ agentVersion?: string | undefined;
134
+ },
135
+ ): Promise<void> {
131
136
  const cacheKey: string = clusterId.toString();
137
+ const extrasFingerprint: string = crypto
138
+ .createHash("sha1")
139
+ .update(
140
+ JSON.stringify({
141
+ agentVersion: extra?.agentVersion ?? null,
142
+ }),
143
+ )
144
+ .digest("hex");
132
145
 
133
146
  const cached: string | null = await GlobalCache.getString(
134
147
  LAST_SEEN_CACHE_NAMESPACE,
135
148
  cacheKey,
136
149
  );
137
150
 
138
- if (cached) {
139
- return; // another pod already updated recently
151
+ if (cached === extrasFingerprint) {
152
+ return; // same data was written recently
140
153
  }
141
154
 
142
- await GlobalCache.setString(LAST_SEEN_CACHE_NAMESPACE, cacheKey, "1", {
143
- expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS,
144
- });
155
+ await GlobalCache.setString(
156
+ LAST_SEEN_CACHE_NAMESPACE,
157
+ cacheKey,
158
+ extrasFingerprint,
159
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
160
+ );
161
+
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ const data: any = {
164
+ lastSeenAt: OneUptimeDate.getCurrentDate(),
165
+ otelCollectorStatus: "connected",
166
+ };
167
+
168
+ if (extra?.agentVersion) {
169
+ data.agentVersion = extra.agentVersion;
170
+ }
145
171
 
146
172
  await this.updateOneById({
147
173
  id: clusterId,
148
- data: {
149
- lastSeenAt: OneUptimeDate.getCurrentDate(),
150
- otelCollectorStatus: "connected",
151
- },
174
+ data: data,
152
175
  props: {
153
176
  isRoot: true,
154
177
  },
@@ -1563,9 +1563,16 @@ ${createdItem.description?.trim() || "No description provided."}
1563
1563
  return;
1564
1564
  }
1565
1565
 
1566
- for (const monitorProbe of monitorProbes) {
1567
- await this.refreshMonitorProbeStatus(monitorProbe.monitorId!);
1568
- }
1566
+ /*
1567
+ * Each monitor appears at most once for a given probeId (composite
1568
+ * unique on MonitorProbe), so concurrent refreshes operate on
1569
+ * disjoint rows and are safe to run in parallel.
1570
+ */
1571
+ await Promise.all(
1572
+ monitorProbes.map((monitorProbe: MonitorProbe) => {
1573
+ return this.refreshMonitorProbeStatus(monitorProbe.monitorId!);
1574
+ }),
1575
+ );
1569
1576
  }
1570
1577
 
1571
1578
  @CaptureSpan()
@@ -82,6 +82,7 @@ import DatabaseConfig from "../DatabaseConfig";
82
82
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
83
83
  import PositiveNumber from "../../Types/PositiveNumber";
84
84
  import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
85
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
85
86
 
86
87
  export interface CurrentPlan {
87
88
  plan: PlanType | null;
@@ -89,6 +90,20 @@ export interface CurrentPlan {
89
90
  }
90
91
 
91
92
  export class ProjectService extends DatabaseService<Model> {
93
+ /*
94
+ * Suppresses repeated `lastActive` UPDATEs from a single API node. 60s of
95
+ * staleness on "last seen" is acceptable; an UPDATE per request is not.
96
+ */
97
+ private lastActiveCache: InMemoryTTLCache<true> = new InMemoryTTLCache(
98
+ 10_000,
99
+ );
100
+ /*
101
+ * Caches the `requireSsoForLogin` flag per project so middleware can skip a
102
+ * Postgres findOneById on every authenticated request.
103
+ */
104
+ private requireSsoForLoginCache: InMemoryTTLCache<boolean> =
105
+ new InMemoryTTLCache(10_000);
106
+
92
107
  public constructor() {
93
108
  super(Model);
94
109
  }
@@ -318,6 +333,14 @@ export class ProjectService extends DatabaseService<Model> {
318
333
  protected override async onBeforeUpdate(
319
334
  updateBy: UpdateBy<Model>,
320
335
  ): Promise<OnUpdate<Model>> {
336
+ /*
337
+ * Any project field could have changed; invalidate the in-process cache
338
+ * of the SSO flag. Cheap to refetch on the next request.
339
+ */
340
+ if (updateBy.data.requireSsoForLogin !== undefined) {
341
+ this.requireSsoForLoginCache.clear();
342
+ }
343
+
321
344
  if (IsBillingEnabled) {
322
345
  if (
323
346
  updateBy.data.businessDetails ||
@@ -1251,7 +1274,21 @@ export class ProjectService extends DatabaseService<Model> {
1251
1274
 
1252
1275
  @CaptureSpan()
1253
1276
  public async updateLastActive(projectId: ObjectID): Promise<void> {
1254
- await this.updateOneById({
1277
+ const key: string = projectId.toString();
1278
+ if (this.lastActiveCache.has(key)) {
1279
+ return;
1280
+ }
1281
+ /*
1282
+ * Set BEFORE the await so a burst of concurrent requests collapses to one
1283
+ * UPDATE per node per 60s window instead of all firing in parallel.
1284
+ */
1285
+ this.lastActiveCache.set(key, true, 60_000);
1286
+
1287
+ /*
1288
+ * Fire-and-forget — `lastActive` is a soft-real-time field and the
1289
+ * caller (auth middleware) shouldn't pay a Postgres round-trip for it.
1290
+ */
1291
+ void this.updateOneById({
1255
1292
  id: projectId,
1256
1293
  data: {
1257
1294
  lastActive: OneUptimeDate.getCurrentDate(),
@@ -1259,9 +1296,43 @@ export class ProjectService extends DatabaseService<Model> {
1259
1296
  props: {
1260
1297
  isRoot: true,
1261
1298
  },
1299
+ }).catch((err: Error) => {
1300
+ // Drop the cache entry so a retry can fire within the same TTL window.
1301
+ this.lastActiveCache.delete(key);
1302
+ logger.error(
1303
+ `Failed to update Project.lastActive for ${key}: ${err.message}`,
1304
+ );
1262
1305
  });
1263
1306
  }
1264
1307
 
1308
+ /**
1309
+ * Returns whether the given project requires SSO for login. Cached for
1310
+ * 60s in-process — middleware calls this on every authenticated request.
1311
+ */
1312
+ @CaptureSpan()
1313
+ public async getRequireSsoForLogin(projectId: ObjectID): Promise<boolean> {
1314
+ const key: string = projectId.toString();
1315
+ const cached: boolean | undefined = this.requireSsoForLoginCache.get(key);
1316
+ if (cached !== undefined) {
1317
+ return cached;
1318
+ }
1319
+
1320
+ const project: Model | null = await this.findOneById({
1321
+ id: projectId,
1322
+ select: { requireSsoForLogin: true },
1323
+ props: { isRoot: true },
1324
+ });
1325
+
1326
+ if (!project) {
1327
+ // Don't cache "not found" — let the caller decide how to handle it.
1328
+ throw new BadDataException("Project not found");
1329
+ }
1330
+
1331
+ const value: boolean = Boolean(project.requireSsoForLogin);
1332
+ this.requireSsoForLoginCache.set(key, value, 60_000);
1333
+ return value;
1334
+ }
1335
+
1265
1336
  @CaptureSpan()
1266
1337
  public async getOwners(projectId: ObjectID): Promise<Array<User>> {
1267
1338
  if (!projectId) {
@@ -37,8 +37,19 @@ import User from "../../Models/DatabaseModels/User";
37
37
  import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
38
38
  import OneUptimeDate from "../../Types/Date";
39
39
  import ProjectSCIMService from "./ProjectSCIMService";
40
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
40
41
 
41
42
  export class TeamMemberService extends DatabaseService<TeamMember> {
43
+ /*
44
+ * Caches the user's accepted team memberships per project. Auth middleware
45
+ * calls this on every authenticated request to evaluate the `Owned`
46
+ * permission scope; without the cache it's a Postgres findBy per request.
47
+ * 60s of staleness on team membership changes is acceptable; we also
48
+ * invalidate proactively when team membership writes happen.
49
+ */
50
+ private teamIdsForUserCache: InMemoryTTLCache<Array<string>> =
51
+ new InMemoryTTLCache(10_000);
52
+
42
53
  public constructor() {
43
54
  super(TeamMember);
44
55
  }
@@ -215,6 +226,14 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
215
226
  userId: ObjectID,
216
227
  projectId: ObjectID,
217
228
  ): Promise<void> {
229
+ /*
230
+ * Invalidate the in-process cache of this user's team memberships in
231
+ * this project — membership just changed.
232
+ */
233
+ this.teamIdsForUserCache.delete(
234
+ `${userId.toString()}:${projectId.toString()}`,
235
+ );
236
+
218
237
  /// Refresh tokens.
219
238
  await AccessTokenService.refreshUserGlobalAccessPermission(userId);
220
239
 
@@ -545,6 +564,15 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
545
564
  userId: ObjectID,
546
565
  projectId: ObjectID,
547
566
  ): Promise<Array<ObjectID>> {
567
+ const cacheKey: string = `${userId.toString()}:${projectId.toString()}`;
568
+ const cached: Array<string> | undefined =
569
+ this.teamIdsForUserCache.get(cacheKey);
570
+ if (cached !== undefined) {
571
+ return cached.map((id: string) => {
572
+ return new ObjectID(id);
573
+ });
574
+ }
575
+
548
576
  const members: Array<TeamMember> = await this.findBy({
549
577
  query: {
550
578
  userId: userId,
@@ -570,6 +598,14 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
570
598
  teamIds.push(id);
571
599
  }
572
600
  }
601
+
602
+ this.teamIdsForUserCache.set(
603
+ cacheKey,
604
+ teamIds.map((id: ObjectID) => {
605
+ return id.toString();
606
+ }),
607
+ 60_000,
608
+ );
573
609
  return teamIds;
574
610
  }
575
611
  }
@@ -39,12 +39,50 @@ import BadDataException from "../../Types/Exception/BadDataException";
39
39
  import Name from "../../Types/Name";
40
40
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
41
41
  import Timezone from "../../Types/Timezone";
42
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
42
43
 
43
44
  export class Service extends DatabaseService<Model> {
45
+ /*
46
+ * Suppresses repeated `lastActive` UPDATEs from a single API node. 60s of
47
+ * staleness on "last seen" is acceptable; an UPDATE per request is not.
48
+ */
49
+ private lastActiveCache: InMemoryTTLCache<true> = new InMemoryTTLCache(
50
+ 10_000,
51
+ );
52
+
44
53
  public constructor() {
45
54
  super(Model);
46
55
  }
47
56
 
57
+ /**
58
+ * Debounced fire-and-forget update of `User.lastActive`. The auth
59
+ * middleware calls this on every authenticated request; without the cache
60
+ * we'd issue one Postgres UPDATE per request per user.
61
+ */
62
+ @CaptureSpan()
63
+ public async updateLastActive(userId: ObjectID): Promise<void> {
64
+ const key: string = userId.toString();
65
+ if (this.lastActiveCache.has(key)) {
66
+ return;
67
+ }
68
+ /*
69
+ * Set BEFORE the await so a burst of concurrent requests collapses to one
70
+ * UPDATE per node per 60s window.
71
+ */
72
+ this.lastActiveCache.set(key, true, 60_000);
73
+
74
+ void this.updateOneById({
75
+ id: userId,
76
+ data: { lastActive: OneUptimeDate.getCurrentDate() },
77
+ props: { isRoot: true },
78
+ }).catch((err: Error) => {
79
+ this.lastActiveCache.delete(key);
80
+ logger.error(
81
+ `Failed to update User.lastActive for ${key}: ${err.message}`,
82
+ );
83
+ });
84
+ }
85
+
48
86
  @CaptureSpan()
49
87
  public async getUserMarkdownString(data: {
50
88
  userId: ObjectID;
@@ -0,0 +1,108 @@
1
+ import DataToProcess from "../DataToProcess";
2
+ import CompareCriteria from "./CompareCriteria";
3
+ import {
4
+ CheckOn,
5
+ CriteriaFilter,
6
+ FilterType,
7
+ } from "../../../../Types/Monitor/CriteriaFilter";
8
+ import DnssecMonitorResponse from "../../../../Types/Monitor/DnssecMonitor/DnssecMonitorResponse";
9
+ import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
10
+ import CaptureSpan from "../../Telemetry/CaptureSpan";
11
+
12
+ export default class DnssecMonitorCriteria {
13
+ @CaptureSpan()
14
+ public static async isMonitorInstanceCriteriaFilterMet(input: {
15
+ dataToProcess: DataToProcess;
16
+ criteriaFilter: CriteriaFilter;
17
+ }): Promise<string | null> {
18
+ const dataToProcess: ProbeMonitorResponse =
19
+ input.dataToProcess as ProbeMonitorResponse;
20
+
21
+ const dnssecResponse: DnssecMonitorResponse | undefined =
22
+ dataToProcess.dnssecResponse;
23
+
24
+ if (!dnssecResponse) {
25
+ return null;
26
+ }
27
+
28
+ const isTrue: boolean = input.criteriaFilter.filterType === FilterType.True;
29
+ const isFalse: boolean =
30
+ input.criteriaFilter.filterType === FilterType.False;
31
+
32
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecChainValid) {
33
+ if (dnssecResponse.isChainValid && isTrue) {
34
+ return `DNSSEC chain is valid for ${dnssecResponse.domainName}.`;
35
+ }
36
+ if (!dnssecResponse.isChainValid && isFalse) {
37
+ return `DNSSEC chain validation failed for ${dnssecResponse.domainName}.`;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecDnskeyExists) {
43
+ const exists: boolean = dnssecResponse.dnskeys.length > 0;
44
+ if (exists && isTrue) {
45
+ return `DNSKEY records present for ${dnssecResponse.domainName}.`;
46
+ }
47
+ if (!exists && isFalse) {
48
+ return `No DNSKEY records found for ${dnssecResponse.domainName}.`;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecDsExists) {
54
+ const exists: boolean = dnssecResponse.isParentDsPresent;
55
+ if (exists && isTrue) {
56
+ return `DS records present at parent zone for ${dnssecResponse.domainName}.`;
57
+ }
58
+ if (!exists && isFalse) {
59
+ return `No DS records found at the parent zone for ${dnssecResponse.domainName}.`;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecResolverConsensus) {
65
+ const consensus: boolean = dnssecResponse.resolverConsensusAd;
66
+ if (consensus && isTrue) {
67
+ return `All resolvers report DNSSEC-valid (AD flag) for ${dnssecResponse.domainName}.`;
68
+ }
69
+ if (!consensus && isFalse) {
70
+ return `Resolvers do not agree on DNSSEC validity for ${dnssecResponse.domainName}.`;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecNameserverConsistent) {
76
+ const consistent: boolean = dnssecResponse.isNameserverConsistent;
77
+ if (consistent && isTrue) {
78
+ return `Authoritative nameservers are consistent for ${dnssecResponse.domainName}.`;
79
+ }
80
+ if (!consistent && isFalse) {
81
+ return `Authoritative nameservers are inconsistent for ${dnssecResponse.domainName}.`;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ if (input.criteriaFilter.checkOn === CheckOn.DnssecSignatureExpiresInDays) {
87
+ const threshold: number | null = CompareCriteria.convertToNumber(
88
+ input.criteriaFilter.value,
89
+ );
90
+
91
+ if (threshold === null || threshold === undefined) {
92
+ return null;
93
+ }
94
+
95
+ if (dnssecResponse.daysUntilSignatureExpiry === undefined) {
96
+ return null;
97
+ }
98
+
99
+ return CompareCriteria.compareCriteriaNumbers({
100
+ value: dnssecResponse.daysUntilSignatureExpiry,
101
+ threshold: threshold,
102
+ criteriaFilter: input.criteriaFilter,
103
+ });
104
+ }
105
+
106
+ return null;
107
+ }
108
+ }
@@ -18,6 +18,7 @@ import ProfileMonitorCriteria from "./Criteria/ProfileMonitorCriteria";
18
18
  import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
19
19
  import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
20
20
  import DomainMonitorCriteria from "./Criteria/DomainMonitorCriteria";
21
+ import DnssecMonitorCriteria from "./Criteria/DnssecMonitorCriteria";
21
22
  import ExternalStatusPageMonitorCriteria from "./Criteria/ExternalStatusPageMonitorCriteria";
22
23
  import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
23
24
  import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
@@ -761,6 +762,18 @@ ${contextBlock}
761
762
  }
762
763
  }
763
764
 
765
+ if (input.monitor.monitorType === MonitorType.DNSSEC) {
766
+ const dnssecMonitorResult: string | null =
767
+ await DnssecMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
768
+ dataToProcess: input.dataToProcess,
769
+ criteriaFilter: input.criteriaFilter,
770
+ });
771
+
772
+ if (dnssecMonitorResult) {
773
+ return dnssecMonitorResult;
774
+ }
775
+ }
776
+
764
777
  if (input.monitor.monitorType === MonitorType.ExternalStatusPage) {
765
778
  const externalStatusPageResult: string | null =
766
779
  await ExternalStatusPageMonitorCriteria.isMonitorInstanceCriteriaFilterMet(
@@ -19,6 +19,7 @@ import DnsMonitorResponse, {
19
19
  DnsRecordResponse,
20
20
  } from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
21
21
  import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
22
+ import DnssecMonitorResponse from "../../../Types/Monitor/DnssecMonitor/DnssecMonitorResponse";
22
23
  import ExternalStatusPageMonitorResponse, {
23
24
  ExternalStatusPageComponentStatus,
24
25
  } from "../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
@@ -332,6 +333,30 @@ export default class MonitorTemplateUtil {
332
333
  } as JSONObject;
333
334
  }
334
335
 
336
+ if (data.monitorType === MonitorType.DNSSEC) {
337
+ const dnssecResponse: DnssecMonitorResponse | undefined = (
338
+ data.dataToProcess as ProbeMonitorResponse
339
+ ).dnssecResponse;
340
+
341
+ storageMap = {
342
+ isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline,
343
+ responseTimeInMs: dnssecResponse?.responseTimeInMs,
344
+ failureCause: dnssecResponse?.failureCause,
345
+ domainName: dnssecResponse?.domainName,
346
+ isZoneSigned: dnssecResponse?.isZoneSigned,
347
+ isParentDsPresent: dnssecResponse?.isParentDsPresent,
348
+ isChainValid: dnssecResponse?.isChainValid,
349
+ resolverConsensusAd: dnssecResponse?.resolverConsensusAd,
350
+ isNameserverConsistent: dnssecResponse?.isNameserverConsistent,
351
+ earliestSignatureExpiration:
352
+ dnssecResponse?.earliestSignatureExpiration,
353
+ daysUntilSignatureExpiry: dnssecResponse?.daysUntilSignatureExpiry,
354
+ dnskeyCount: dnssecResponse?.dnskeys?.length,
355
+ dsRecordCount: dnssecResponse?.parentDsRecords?.length,
356
+ rrsigCount: dnssecResponse?.rrsigs?.length,
357
+ } as JSONObject;
358
+ }
359
+
335
360
  if (
336
361
  data.monitorType === MonitorType.Metrics ||
337
362
  data.monitorType === MonitorType.Kubernetes ||
@@ -84,6 +84,14 @@ export enum CheckOn {
84
84
  DomainStatusCode = "Domain Status Code",
85
85
  DomainIsExpired = "Domain Is Expired",
86
86
 
87
+ // DNSSEC monitors.
88
+ DnssecChainValid = "DNSSEC Chain Is Valid",
89
+ DnssecDnskeyExists = "DNSSEC DNSKEY Record Exists",
90
+ DnssecDsExists = "DNSSEC DS Record Exists At Parent",
91
+ DnssecSignatureExpiresInDays = "DNSSEC Signature Expires In Days",
92
+ DnssecResolverConsensus = "DNSSEC Resolver Consensus (AD Flag)",
93
+ DnssecNameserverConsistent = "DNSSEC Nameservers Are Consistent",
94
+
87
95
  // External Status Page monitors.
88
96
  ExternalStatusPageIsOnline = "External Status Page Is Online",
89
97
  ExternalStatusPageOverallStatus = "External Status Page Overall Status",
@@ -279,6 +287,11 @@ export class CriteriaFilterUtil {
279
287
  checkOn === CheckOn.SnmpIsOnline ||
280
288
  checkOn === CheckOn.DnsIsOnline ||
281
289
  checkOn === CheckOn.DomainIsExpired ||
290
+ checkOn === CheckOn.DnssecChainValid ||
291
+ checkOn === CheckOn.DnssecDnskeyExists ||
292
+ checkOn === CheckOn.DnssecDsExists ||
293
+ checkOn === CheckOn.DnssecResolverConsensus ||
294
+ checkOn === CheckOn.DnssecNameserverConsistent ||
282
295
  checkOn === CheckOn.ExternalStatusPageIsOnline
283
296
  ) {
284
297
  return false;
@@ -0,0 +1,69 @@
1
+ export interface DnssecKeyRecord {
2
+ flags: number;
3
+ algorithm: number;
4
+ keyTag?: number | undefined;
5
+ }
6
+
7
+ export interface DnssecDsRecord {
8
+ keyTag: number;
9
+ algorithm: number;
10
+ digestType: number;
11
+ digest: string;
12
+ }
13
+
14
+ export interface DnssecRrsigRecord {
15
+ typeCovered: string;
16
+ algorithm: number;
17
+ signerName: string;
18
+ keyTag: number;
19
+ inception?: string | undefined;
20
+ expiration?: string | undefined;
21
+ }
22
+
23
+ export interface DnssecResolverCheck {
24
+ resolver: string;
25
+ adFlag: boolean;
26
+ servfailWhenValidating: boolean;
27
+ error?: string | undefined;
28
+ }
29
+
30
+ export interface DnssecNameserverCheck {
31
+ nameServer: string;
32
+ soaSerial?: string | undefined;
33
+ rrsigExpiration?: string | undefined;
34
+ error?: string | undefined;
35
+ }
36
+
37
+ export default interface DnssecMonitorResponse {
38
+ isOnline: boolean;
39
+ responseTimeInMs: number;
40
+ failureCause: string;
41
+ domainName: string;
42
+ isTimeout?: boolean | undefined;
43
+
44
+ // Zone signed?
45
+ isZoneSigned: boolean;
46
+
47
+ // DNSKEY presence
48
+ dnskeys: Array<DnssecKeyRecord>;
49
+
50
+ // DS at parent
51
+ parentDsRecords: Array<DnssecDsRecord>;
52
+ isParentDsPresent: boolean;
53
+
54
+ // RRSIG over the A record (zone apex by default)
55
+ rrsigs: Array<DnssecRrsigRecord>;
56
+ earliestSignatureExpiration?: string | undefined;
57
+ daysUntilSignatureExpiry?: number | undefined;
58
+
59
+ // Resolver consensus (AD flag + CD-bit SERVFAIL test)
60
+ resolverChecks: Array<DnssecResolverCheck>;
61
+ resolverConsensusAd: boolean;
62
+
63
+ // Primary/secondary nameserver consistency
64
+ nameserverChecks: Array<DnssecNameserverCheck>;
65
+ isNameserverConsistent: boolean;
66
+
67
+ // Overall chain validity (DNSKEY exists, DS exists, RRSIG valid, AD across resolvers)
68
+ isChainValid: boolean;
69
+ }