@oneuptime/common 10.2.2 → 10.2.3

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 (54) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/UserNotificationRule.ts +49 -0
  3. package/Models/DatabaseModels/UserNotificationSetting.ts +17 -0
  4. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +48 -0
  5. package/Models/DatabaseModels/UserWebhook.ts +290 -0
  6. package/Models/DatabaseModels/WebhookLog.ts +992 -0
  7. package/Server/API/UserWebhookAPI.ts +159 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1778506655291-AddProjectOIDC.ts +1 -1
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1778514515756-MigrationName.ts +259 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  11. package/Server/Services/UserNotificationRuleService.ts +213 -1
  12. package/Server/Services/UserNotificationSettingService.ts +95 -0
  13. package/Server/Services/UserWebhookService.ts +208 -0
  14. package/Server/Services/WebhookLogService.ts +15 -0
  15. package/Server/Services/WebhookService.ts +126 -0
  16. package/Types/Permission.ts +12 -0
  17. package/Types/Webhook/WebhookMessage.ts +8 -0
  18. package/Types/WebhookStatus.ts +6 -0
  19. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  20. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  21. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +49 -0
  22. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  23. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +19 -0
  24. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  25. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +48 -0
  26. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  27. package/build/dist/Models/DatabaseModels/UserWebhook.js +307 -0
  28. package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -0
  29. package/build/dist/Models/DatabaseModels/WebhookLog.js +1021 -0
  30. package/build/dist/Models/DatabaseModels/WebhookLog.js.map +1 -0
  31. package/build/dist/Server/API/UserWebhookAPI.js +99 -0
  32. package/build/dist/Server/API/UserWebhookAPI.js.map +1 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778506655291-AddProjectOIDC.js.map +1 -1
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778514515756-MigrationName.js +94 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778514515756-MigrationName.js.map +1 -0
  36. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  37. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  38. package/build/dist/Server/Services/UserNotificationRuleService.js +178 -21
  39. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  40. package/build/dist/Server/Services/UserNotificationSettingService.js +84 -1
  41. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  42. package/build/dist/Server/Services/UserWebhookService.js +190 -0
  43. package/build/dist/Server/Services/UserWebhookService.js.map +1 -0
  44. package/build/dist/Server/Services/WebhookLogService.js +13 -0
  45. package/build/dist/Server/Services/WebhookLogService.js.map +1 -0
  46. package/build/dist/Server/Services/WebhookService.js +92 -0
  47. package/build/dist/Server/Services/WebhookService.js.map +1 -0
  48. package/build/dist/Types/Permission.js +10 -0
  49. package/build/dist/Types/Permission.js.map +1 -1
  50. package/build/dist/Types/Webhook/WebhookMessage.js +2 -0
  51. package/build/dist/Types/Webhook/WebhookMessage.js.map +1 -0
  52. package/build/dist/Types/WebhookStatus.js +7 -0
  53. package/build/dist/Types/WebhookStatus.js.map +1 -0
  54. package/package.json +1 -1
@@ -10,6 +10,7 @@ import MailService from "./MailService";
10
10
  import ShortLinkService from "./ShortLinkService";
11
11
  import SmsService from "./SmsService";
12
12
  import TelegramService from "./TelegramService";
13
+ import WebhookService from "./WebhookService";
13
14
  import WhatsAppService from "./WhatsAppService";
14
15
  import UserEmailService from "./UserEmailService";
15
16
  import UserOnCallLogService from "./UserOnCallLogService";
@@ -28,6 +29,7 @@ import Email from "../../Types/Email";
28
29
  import EmailMessage from "../../Types/Email/EmailMessage";
29
30
  import EmailTemplateType from "../../Types/Email/EmailTemplateType";
30
31
  import BadDataException from "../../Types/Exception/BadDataException";
32
+ import { JSONObject } from "../../Types/JSON";
31
33
  import NotificationRuleType from "../../Types/NotificationRule/NotificationRuleType";
32
34
  import ObjectID from "../../Types/ObjectID";
33
35
  import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
@@ -78,6 +80,7 @@ export interface NotificationMethodDescriptor {
78
80
  userWhatsAppId?: ObjectID;
79
81
  userTelegramId?: ObjectID;
80
82
  userPushId?: ObjectID;
83
+ userWebhookId?: ObjectID;
81
84
  }
82
85
 
83
86
  export class Service extends DatabaseService<Model> {
@@ -173,6 +176,11 @@ export class Service extends DatabaseService<Model> {
173
176
  telegramUserHandle: true,
174
177
  isVerified: true,
175
178
  },
179
+ userWebhook: {
180
+ webhookUrl: true,
181
+ name: true,
182
+ secret: true,
183
+ },
176
184
  userEmail: {
177
185
  email: true,
178
186
  isVerified: true,
@@ -1082,6 +1090,202 @@ export class Service extends DatabaseService<Model> {
1082
1090
  });
1083
1091
  }
1084
1092
 
1093
+ // send webhook.
1094
+ if (notificationRuleItem.userWebhook?.webhookUrl) {
1095
+ const webhookUrl: string = notificationRuleItem.userWebhook.webhookUrl;
1096
+ const webhookSecret: string | undefined =
1097
+ notificationRuleItem.userWebhook.secret;
1098
+ const userWebhookId: ObjectID = notificationRuleItem.userWebhook.id!;
1099
+
1100
+ const dispatchWebhook: (params: {
1101
+ eventType: string;
1102
+ payload: JSONObject;
1103
+ entityId?: ObjectID;
1104
+ entityKind: "alert" | "incident" | "alertEpisode" | "incidentEpisode";
1105
+ }) => Promise<void> = async (params: {
1106
+ eventType: string;
1107
+ payload: JSONObject;
1108
+ entityId?: ObjectID;
1109
+ entityKind: "alert" | "incident" | "alertEpisode" | "incidentEpisode";
1110
+ }): Promise<void> => {
1111
+ logTimelineItem.status = UserNotificationStatus.Sending;
1112
+ logTimelineItem.statusMessage = `Sending webhook to ${webhookUrl}.`;
1113
+ logTimelineItem.userWebhookId = userWebhookId;
1114
+
1115
+ const updatedLog: UserOnCallLogTimeline =
1116
+ await UserOnCallLogTimelineService.create({
1117
+ data: logTimelineItem,
1118
+ props: {
1119
+ isRoot: true,
1120
+ },
1121
+ });
1122
+
1123
+ const callbacksByKind: {
1124
+ alert?: { alertId?: ObjectID };
1125
+ incident?: { incidentId?: ObjectID };
1126
+ } = {};
1127
+ if (params.entityKind === "alert" && params.entityId) {
1128
+ callbacksByKind.alert = { alertId: params.entityId };
1129
+ } else if (params.entityKind === "incident" && params.entityId) {
1130
+ callbacksByKind.incident = { incidentId: params.entityId };
1131
+ }
1132
+
1133
+ WebhookService.sendWebhook(
1134
+ {
1135
+ url: webhookUrl,
1136
+ eventType: params.eventType,
1137
+ payload: params.payload,
1138
+ secret: webhookSecret,
1139
+ },
1140
+ {
1141
+ projectId: options.projectId,
1142
+ userOnCallLogTimelineId: updatedLog.id!,
1143
+ userId: notificationRuleItem.userId!,
1144
+ onCallPolicyId: options.onCallPolicyId,
1145
+ onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
1146
+ teamId: options.userBelongsToTeamId,
1147
+ onCallDutyPolicyExecutionLogTimelineId:
1148
+ options.onCallDutyPolicyExecutionLogTimelineId,
1149
+ onCallScheduleId: options.onCallScheduleId,
1150
+ ...callbacksByKind.alert,
1151
+ ...callbacksByKind.incident,
1152
+ },
1153
+ ).catch(async (err: Error) => {
1154
+ await UserOnCallLogTimelineService.updateOneById({
1155
+ id: updatedLog.id!,
1156
+ data: {
1157
+ status: UserNotificationStatus.Error,
1158
+ statusMessage: err.message || "Error sending webhook.",
1159
+ },
1160
+ props: {
1161
+ isRoot: true,
1162
+ },
1163
+ });
1164
+ });
1165
+ };
1166
+
1167
+ if (
1168
+ options.userNotificationEventType ===
1169
+ UserNotificationEventType.AlertCreated &&
1170
+ alert
1171
+ ) {
1172
+ await dispatchWebhook({
1173
+ eventType: "on-call.alert.created",
1174
+ entityKind: "alert",
1175
+ entityId: alert.id!,
1176
+ payload: {
1177
+ eventType: "on-call.alert.created",
1178
+ timestamp: new Date().toISOString(),
1179
+ projectId: alert.projectId?.toString() || "",
1180
+ userId: notificationRuleItem.userId!.toString(),
1181
+ alert: {
1182
+ id: alert.id?.toString() || "",
1183
+ title: alert.title || "",
1184
+ description: alert.description || "",
1185
+ alertNumber: alert.alertNumber || null,
1186
+ alertNumberWithPrefix: alert.alertNumberWithPrefix || null,
1187
+ severity: alert.alertSeverity?.name || null,
1188
+ state: alert.currentAlertState?.name || null,
1189
+ },
1190
+ onCallPolicyId: options.onCallPolicyId?.toString() || null,
1191
+ onCallPolicyEscalationRuleId:
1192
+ options.onCallPolicyEscalationRuleId?.toString() || null,
1193
+ },
1194
+ });
1195
+ }
1196
+
1197
+ if (
1198
+ options.userNotificationEventType ===
1199
+ UserNotificationEventType.IncidentCreated &&
1200
+ incident
1201
+ ) {
1202
+ await dispatchWebhook({
1203
+ eventType: "on-call.incident.created",
1204
+ entityKind: "incident",
1205
+ entityId: incident.id!,
1206
+ payload: {
1207
+ eventType: "on-call.incident.created",
1208
+ timestamp: new Date().toISOString(),
1209
+ projectId: incident.projectId?.toString() || "",
1210
+ userId: notificationRuleItem.userId!.toString(),
1211
+ incident: {
1212
+ id: incident.id?.toString() || "",
1213
+ title: incident.title || "",
1214
+ description: incident.description || "",
1215
+ incidentNumber: incident.incidentNumber || null,
1216
+ incidentNumberWithPrefix:
1217
+ incident.incidentNumberWithPrefix || null,
1218
+ severity: incident.incidentSeverity?.name || null,
1219
+ state: incident.currentIncidentState?.name || null,
1220
+ },
1221
+ onCallPolicyId: options.onCallPolicyId?.toString() || null,
1222
+ onCallPolicyEscalationRuleId:
1223
+ options.onCallPolicyEscalationRuleId?.toString() || null,
1224
+ },
1225
+ });
1226
+ }
1227
+
1228
+ if (
1229
+ options.userNotificationEventType ===
1230
+ UserNotificationEventType.AlertEpisodeCreated &&
1231
+ alertEpisode
1232
+ ) {
1233
+ await dispatchWebhook({
1234
+ eventType: "on-call.alertEpisode.created",
1235
+ entityKind: "alertEpisode",
1236
+ payload: {
1237
+ eventType: "on-call.alertEpisode.created",
1238
+ timestamp: new Date().toISOString(),
1239
+ projectId: alertEpisode.projectId?.toString() || "",
1240
+ userId: notificationRuleItem.userId!.toString(),
1241
+ alertEpisode: {
1242
+ id: alertEpisode.id?.toString() || "",
1243
+ title: alertEpisode.title || "",
1244
+ description: alertEpisode.description || "",
1245
+ episodeNumber: alertEpisode.episodeNumber || null,
1246
+ episodeNumberWithPrefix:
1247
+ alertEpisode.episodeNumberWithPrefix || null,
1248
+ severity: alertEpisode.alertSeverity?.name || null,
1249
+ state: alertEpisode.currentAlertState?.name || null,
1250
+ },
1251
+ onCallPolicyId: options.onCallPolicyId?.toString() || null,
1252
+ onCallPolicyEscalationRuleId:
1253
+ options.onCallPolicyEscalationRuleId?.toString() || null,
1254
+ },
1255
+ });
1256
+ }
1257
+
1258
+ if (
1259
+ options.userNotificationEventType ===
1260
+ UserNotificationEventType.IncidentEpisodeCreated &&
1261
+ incidentEpisode
1262
+ ) {
1263
+ await dispatchWebhook({
1264
+ eventType: "on-call.incidentEpisode.created",
1265
+ entityKind: "incidentEpisode",
1266
+ payload: {
1267
+ eventType: "on-call.incidentEpisode.created",
1268
+ timestamp: new Date().toISOString(),
1269
+ projectId: incidentEpisode.projectId?.toString() || "",
1270
+ userId: notificationRuleItem.userId!.toString(),
1271
+ incidentEpisode: {
1272
+ id: incidentEpisode.id?.toString() || "",
1273
+ title: incidentEpisode.title || "",
1274
+ description: incidentEpisode.description || "",
1275
+ episodeNumber: incidentEpisode.episodeNumber || null,
1276
+ episodeNumberWithPrefix:
1277
+ incidentEpisode.episodeNumberWithPrefix || null,
1278
+ severity: incidentEpisode.incidentSeverity?.name || null,
1279
+ state: incidentEpisode.currentIncidentState?.name || null,
1280
+ },
1281
+ onCallPolicyId: options.onCallPolicyId?.toString() || null,
1282
+ onCallPolicyEscalationRuleId:
1283
+ options.onCallPolicyEscalationRuleId?.toString() || null,
1284
+ },
1285
+ });
1286
+ }
1287
+ }
1288
+
1085
1289
  // send call.
1086
1290
  if (
1087
1291
  notificationRuleItem.userCall?.phone &&
@@ -2631,12 +2835,14 @@ export class Service extends DatabaseService<Model> {
2631
2835
  !createBy.data.userWhatsAppId &&
2632
2836
  !createBy.data.userTelegram &&
2633
2837
  !createBy.data.userTelegramId &&
2838
+ !createBy.data.userWebhook &&
2839
+ !createBy.data.userWebhookId &&
2634
2840
  !createBy.data.userEmailId &&
2635
2841
  !createBy.data.userPushId &&
2636
2842
  !createBy.data.userPush
2637
2843
  ) {
2638
2844
  throw new BadDataException(
2639
- "Call, SMS, WhatsApp, Telegram, Email, or Push notification is required",
2845
+ "Call, SMS, WhatsApp, Telegram, Webhook, Email, or Push notification is required",
2640
2846
  );
2641
2847
  }
2642
2848
 
@@ -2701,6 +2907,9 @@ export class Service extends DatabaseService<Model> {
2701
2907
  if (descriptor.userTelegramId) {
2702
2908
  rule.userTelegramId = descriptor.userTelegramId;
2703
2909
  }
2910
+ if (descriptor.userWebhookId) {
2911
+ rule.userWebhookId = descriptor.userWebhookId;
2912
+ }
2704
2913
  if (descriptor.userPushId) {
2705
2914
  rule.userPushId = descriptor.userPushId;
2706
2915
  }
@@ -2725,6 +2934,9 @@ export class Service extends DatabaseService<Model> {
2725
2934
  if (descriptor.userTelegramId) {
2726
2935
  query["userTelegramId"] = descriptor.userTelegramId;
2727
2936
  }
2937
+ if (descriptor.userWebhookId) {
2938
+ query["userWebhookId"] = descriptor.userWebhookId;
2939
+ }
2728
2940
  if (descriptor.userPushId) {
2729
2941
  query["userPushId"] = descriptor.userPushId;
2730
2942
  }
@@ -12,7 +12,9 @@ import UserEmailService from "./UserEmailService";
12
12
  import UserSmsService from "./UserSmsService";
13
13
  import PushNotificationService from "./PushNotificationService";
14
14
  import UserTelegramService from "./UserTelegramService";
15
+ import UserWebhookService from "./UserWebhookService";
15
16
  import UserWhatsAppService from "./UserWhatsAppService";
17
+ import WebhookService from "./WebhookService";
16
18
  import WhatsAppService from "./WhatsAppService";
17
19
  import { CallRequestMessage } from "../../Types/Call/CallRequest";
18
20
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -29,11 +31,13 @@ import TelegramMessage, {
29
31
  import WhatsAppMessage, {
30
32
  WhatsAppMessagePayload,
31
33
  } from "../../Types/WhatsApp/WhatsAppMessage";
34
+ import { JSONObject } from "../../Types/JSON";
32
35
  import UserCall from "../../Models/DatabaseModels/UserCall";
33
36
  import UserEmail from "../../Models/DatabaseModels/UserEmail";
34
37
  import UserNotificationSetting from "../../Models/DatabaseModels/UserNotificationSetting";
35
38
  import UserSMS from "../../Models/DatabaseModels/UserSMS";
36
39
  import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
40
+ import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
37
41
  import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
38
42
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
39
43
  import { appendRecipientToWhatsAppMessage } from "../Utils/WhatsAppTemplateUtil";
@@ -89,6 +93,7 @@ export class Service extends DatabaseService<UserNotificationSetting> {
89
93
  alertByTelegram: true,
90
94
  alertByCall: true,
91
95
  alertByPush: true,
96
+ alertByWebhook: true,
92
97
  },
93
98
  props: {
94
99
  isRoot: true,
@@ -425,6 +430,96 @@ export class Service extends DatabaseService<UserNotificationSetting> {
425
430
  logger.error(err);
426
431
  });
427
432
  }
433
+
434
+ if (notificationSettings.alertByWebhook) {
435
+ const userWebhooks: Array<UserWebhook> =
436
+ await UserWebhookService.findBy({
437
+ query: {
438
+ userId: data.userId,
439
+ projectId: data.projectId,
440
+ },
441
+ select: {
442
+ webhookUrl: true,
443
+ secret: true,
444
+ name: true,
445
+ },
446
+ limit: LIMIT_PER_PROJECT,
447
+ skip: 0,
448
+ props: {
449
+ isRoot: true,
450
+ },
451
+ });
452
+
453
+ const webhookPayload: JSONObject = {
454
+ eventType: data.eventType,
455
+ timestamp: new Date().toISOString(),
456
+ projectId: data.projectId.toString(),
457
+ userId: data.userId.toString(),
458
+ subject: data.emailEnvelope?.subject || "",
459
+ message: data.smsMessage?.message || "",
460
+ };
461
+
462
+ if (data.incidentId) {
463
+ webhookPayload["incidentId"] = data.incidentId.toString();
464
+ }
465
+ if (data.alertId) {
466
+ webhookPayload["alertId"] = data.alertId.toString();
467
+ }
468
+ if (data.monitorId) {
469
+ webhookPayload["monitorId"] = data.monitorId.toString();
470
+ }
471
+ if (data.scheduledMaintenanceId) {
472
+ webhookPayload["scheduledMaintenanceId"] =
473
+ data.scheduledMaintenanceId.toString();
474
+ }
475
+ if (data.statusPageId) {
476
+ webhookPayload["statusPageId"] = data.statusPageId.toString();
477
+ }
478
+ if (data.statusPageAnnouncementId) {
479
+ webhookPayload["statusPageAnnouncementId"] =
480
+ data.statusPageAnnouncementId.toString();
481
+ }
482
+ if (data.onCallPolicyId) {
483
+ webhookPayload["onCallPolicyId"] = data.onCallPolicyId.toString();
484
+ }
485
+ if (data.onCallPolicyEscalationRuleId) {
486
+ webhookPayload["onCallPolicyEscalationRuleId"] =
487
+ data.onCallPolicyEscalationRuleId.toString();
488
+ }
489
+
490
+ for (const userWebhook of userWebhooks) {
491
+ if (!userWebhook.webhookUrl) {
492
+ continue;
493
+ }
494
+
495
+ WebhookService.sendWebhook(
496
+ {
497
+ url: userWebhook.webhookUrl,
498
+ eventType: data.eventType,
499
+ payload: webhookPayload,
500
+ secret: userWebhook.secret,
501
+ },
502
+ {
503
+ projectId: data.projectId,
504
+ incidentId: data.incidentId,
505
+ alertId: data.alertId,
506
+ monitorId: data.monitorId,
507
+ scheduledMaintenanceId: data.scheduledMaintenanceId,
508
+ statusPageId: data.statusPageId,
509
+ statusPageAnnouncementId: data.statusPageAnnouncementId,
510
+ userId: data.userId,
511
+ teamId: data.teamId,
512
+ onCallPolicyId: data.onCallPolicyId,
513
+ onCallPolicyEscalationRuleId: data.onCallPolicyEscalationRuleId,
514
+ onCallDutyPolicyExecutionLogTimelineId:
515
+ data.onCallDutyPolicyExecutionLogTimelineId,
516
+ onCallScheduleId: data.onCallScheduleId,
517
+ },
518
+ ).catch((err: Error) => {
519
+ logger.error(err);
520
+ });
521
+ }
522
+ }
428
523
  }
429
524
  }
430
525
 
@@ -0,0 +1,208 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
4
+ import DatabaseService from "./DatabaseService";
5
+ import UserNotificationRuleService from "./UserNotificationRuleService";
6
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
7
+ import BadDataException from "../../Types/Exception/BadDataException";
8
+ import Model from "../../Models/DatabaseModels/UserWebhook";
9
+ import URL from "../../Types/API/URL";
10
+ import logger from "../Utils/Logger";
11
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
12
+
13
+ export class Service extends DatabaseService<Model> {
14
+ public constructor() {
15
+ super(Model);
16
+ }
17
+
18
+ @CaptureSpan()
19
+ protected override async onBeforeCreate(
20
+ createBy: CreateBy<Model>,
21
+ ): Promise<OnCreate<Model>> {
22
+ if (!createBy.data.webhookUrl) {
23
+ throw new BadDataException("Webhook URL is required");
24
+ }
25
+
26
+ if (!createBy.data.name) {
27
+ throw new BadDataException("Webhook name is required");
28
+ }
29
+
30
+ this.validateWebhookUrl(createBy.data.webhookUrl);
31
+
32
+ return {
33
+ createBy,
34
+ carryForward: null,
35
+ };
36
+ }
37
+
38
+ @CaptureSpan()
39
+ protected override async onCreateSuccess(
40
+ _onCreate: OnCreate<Model>,
41
+ createdItem: Model,
42
+ ): Promise<Model> {
43
+ /* Webhooks skip verification, so default on-call rules are seeded at create time. */
44
+ if (createdItem.projectId && createdItem.userId && createdItem.id) {
45
+ try {
46
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
47
+ {
48
+ projectId: createdItem.projectId,
49
+ userId: createdItem.userId,
50
+ notificationMethod: {
51
+ userWebhookId: createdItem.id,
52
+ },
53
+ },
54
+ );
55
+ } catch (err) {
56
+ logger.error(err);
57
+ }
58
+ }
59
+
60
+ return createdItem;
61
+ }
62
+
63
+ @CaptureSpan()
64
+ protected override async onBeforeDelete(
65
+ deleteBy: DeleteBy<Model>,
66
+ ): Promise<OnDelete<Model>> {
67
+ const itemsToDelete: Array<Model> = await this.findBy({
68
+ query: deleteBy.query,
69
+ select: {
70
+ _id: true,
71
+ projectId: true,
72
+ },
73
+ skip: 0,
74
+ limit: LIMIT_MAX,
75
+ props: {
76
+ isRoot: true,
77
+ },
78
+ });
79
+
80
+ for (const item of itemsToDelete) {
81
+ await UserNotificationRuleService.deleteBy({
82
+ query: {
83
+ userWebhookId: item.id!,
84
+ projectId: item.projectId!,
85
+ },
86
+ limit: LIMIT_MAX,
87
+ skip: 0,
88
+ props: {
89
+ isRoot: true,
90
+ },
91
+ });
92
+ }
93
+
94
+ return {
95
+ deleteBy,
96
+ carryForward: null,
97
+ };
98
+ }
99
+
100
+ private validateWebhookUrl(rawUrl: string): void {
101
+ let parsed: URL;
102
+ try {
103
+ parsed = URL.fromString(rawUrl);
104
+ } catch {
105
+ throw new BadDataException("Webhook URL is not a valid URL");
106
+ }
107
+
108
+ const protocolValue: string = parsed.protocol.toString().toLowerCase();
109
+ if (protocolValue !== "http://" && protocolValue !== "https://") {
110
+ throw new BadDataException(
111
+ "Webhook URL must use http or https protocol.",
112
+ );
113
+ }
114
+
115
+ const hostname: string = parsed.hostname.hostname.toLowerCase();
116
+
117
+ if (!hostname) {
118
+ throw new BadDataException("Webhook URL must include a host.");
119
+ }
120
+
121
+ if (isBlockedHostnameLiteral(hostname)) {
122
+ throw new BadDataException(
123
+ "Webhook URL points to a private, loopback, or link-local address and is not allowed.",
124
+ );
125
+ }
126
+ }
127
+ }
128
+
129
+ function isBlockedHostnameLiteral(hostname: string): boolean {
130
+ if (
131
+ hostname === "localhost" ||
132
+ hostname.endsWith(".localhost") ||
133
+ hostname === "metadata.google.internal"
134
+ ) {
135
+ return true;
136
+ }
137
+
138
+ // IPv4 literal check
139
+ const ipv4Match: RegExpMatchArray | null = hostname.match(
140
+ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
141
+ );
142
+ if (ipv4Match) {
143
+ const octets: Array<number> = [
144
+ Number(ipv4Match[1]),
145
+ Number(ipv4Match[2]),
146
+ Number(ipv4Match[3]),
147
+ Number(ipv4Match[4]),
148
+ ];
149
+
150
+ if (
151
+ octets.some((o: number) => {
152
+ return o < 0 || o > 255;
153
+ })
154
+ ) {
155
+ return true;
156
+ }
157
+
158
+ // 0.0.0.0/8
159
+ if (octets[0] === 0) {
160
+ return true;
161
+ }
162
+ // 127.0.0.0/8 loopback
163
+ if (octets[0] === 127) {
164
+ return true;
165
+ }
166
+ // 10.0.0.0/8
167
+ if (octets[0] === 10) {
168
+ return true;
169
+ }
170
+ // 172.16.0.0/12
171
+ if (octets[0] === 172 && (octets[1]! & 0xf0) === 16) {
172
+ return true;
173
+ }
174
+ // 192.168.0.0/16
175
+ if (octets[0] === 192 && octets[1] === 168) {
176
+ return true;
177
+ }
178
+ // 169.254.0.0/16 link-local (incl. cloud metadata)
179
+ if (octets[0] === 169 && octets[1] === 254) {
180
+ return true;
181
+ }
182
+ // 100.64.0.0/10 carrier-grade NAT
183
+ if (octets[0] === 100 && (octets[1]! & 0xc0) === 64) {
184
+ return true;
185
+ }
186
+ return false;
187
+ }
188
+
189
+ // IPv6 literal — block loopback, link-local, unique-local
190
+ if (hostname.includes(":")) {
191
+ const stripped: string = hostname.replace(/^\[|\]$/g, "");
192
+ if (stripped === "::1" || stripped === "::") {
193
+ return true;
194
+ }
195
+ if (stripped.startsWith("fe80:") || stripped.startsWith("fe80::")) {
196
+ return true;
197
+ }
198
+ if (IPV6_UNIQUE_LOCAL_REGEX.test(stripped)) {
199
+ return true;
200
+ }
201
+ }
202
+
203
+ return false;
204
+ }
205
+
206
+ const IPV6_UNIQUE_LOCAL_REGEX: RegExp = /^f[cd][0-9a-f]{2}:/;
207
+
208
+ export default new Service();
@@ -0,0 +1,15 @@
1
+ import { IsBillingEnabled } from "../EnvironmentConfig";
2
+ import DatabaseService from "./DatabaseService";
3
+ import Model from "../../Models/DatabaseModels/WebhookLog";
4
+
5
+ export class Service extends DatabaseService<Model> {
6
+ public constructor() {
7
+ super(Model);
8
+
9
+ if (IsBillingEnabled) {
10
+ this.hardDeleteItemsOlderThanInDays("createdAt", 3);
11
+ }
12
+ }
13
+ }
14
+
15
+ export default new Service();