@oneuptime/common 9.5.13 → 10.0.1

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 (56) hide show
  1. package/Server/API/UserCallAPI.ts +19 -0
  2. package/Server/API/UserEmailAPI.ts +19 -0
  3. package/Server/API/UserPushAPI.ts +66 -22
  4. package/Server/API/UserSmsAPI.ts +19 -0
  5. package/Server/API/UserWhatsAppAPI.ts +19 -0
  6. package/Server/EnvironmentConfig.ts +15 -0
  7. package/Server/Services/PushNotificationService.ts +136 -12
  8. package/Server/Services/UserNotificationRuleService.ts +153 -111
  9. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +142 -0
  10. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +13 -0
  11. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +21 -0
  12. package/Server/Utils/VM/VMRunner.ts +214 -37
  13. package/Types/Monitor/CriteriaFilter.ts +9 -1
  14. package/Types/Monitor/DomainMonitor/DomainMonitorResponse.ts +15 -0
  15. package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
  16. package/Types/Monitor/MonitorStep.ts +32 -0
  17. package/Types/Monitor/MonitorStepDomainMonitor.ts +33 -0
  18. package/Types/Monitor/MonitorType.ts +16 -2
  19. package/Types/Probe/ProbeMonitorResponse.ts +2 -0
  20. package/build/dist/Server/API/UserCallAPI.js +17 -0
  21. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  22. package/build/dist/Server/API/UserEmailAPI.js +17 -0
  23. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  24. package/build/dist/Server/API/UserPushAPI.js +50 -12
  25. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  26. package/build/dist/Server/API/UserSmsAPI.js +17 -0
  27. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  28. package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
  29. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  30. package/build/dist/Server/EnvironmentConfig.js +5 -0
  31. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  32. package/build/dist/Server/Services/PushNotificationService.js +74 -12
  33. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  34. package/build/dist/Server/Services/UserNotificationRuleService.js +96 -109
  35. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  36. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +113 -0
  37. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -0
  38. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
  39. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  40. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +16 -0
  41. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  42. package/build/dist/Server/Utils/VM/VMRunner.js +182 -20
  43. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  44. package/build/dist/Types/Monitor/CriteriaFilter.js +8 -1
  45. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  46. package/build/dist/Types/Monitor/DomainMonitor/DomainMonitorResponse.js +2 -0
  47. package/build/dist/Types/Monitor/DomainMonitor/DomainMonitorResponse.js.map +1 -0
  48. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
  49. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  50. package/build/dist/Types/Monitor/MonitorStep.js +22 -0
  51. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  52. package/build/dist/Types/Monitor/MonitorStepDomainMonitor.js +24 -0
  53. package/build/dist/Types/Monitor/MonitorStepDomainMonitor.js.map +1 -0
  54. package/build/dist/Types/Monitor/MonitorType.js +14 -2
  55. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  56. package/package.json +2 -1
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import UserCallService, {
3
3
  Service as UserCallServiceType,
4
4
  } from "../Services/UserCallService";
5
+ import UserNotificationRuleService from "../Services/UserNotificationRuleService";
5
6
  import {
6
7
  ExpressRequest,
7
8
  ExpressResponse,
@@ -9,8 +10,10 @@ import {
9
10
  OneUptimeRequest,
10
11
  } from "../Utils/Express";
11
12
  import Response from "../Utils/Response";
13
+ import logger from "../Utils/Logger";
12
14
  import BaseAPI from "./BaseAPI";
13
15
  import BadDataException from "../../Types/Exception/BadDataException";
16
+ import ObjectID from "../../Types/ObjectID";
14
17
  import UserCall from "../../Models/DatabaseModels/UserCall";
15
18
  import UserSMS from "../../Models/DatabaseModels/UserSMS";
16
19
 
@@ -52,6 +55,7 @@ export default class UserCallAPI extends BaseAPI<
52
55
  },
53
56
  select: {
54
57
  userId: true,
58
+ projectId: true,
55
59
  verificationCode: true,
56
60
  },
57
61
  });
@@ -95,6 +99,21 @@ export default class UserCallAPI extends BaseAPI<
95
99
  },
96
100
  });
97
101
 
102
+ // Create default notification rules for this verified call number
103
+ try {
104
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
105
+ {
106
+ projectId: new ObjectID(item.projectId!.toString()),
107
+ userId: new ObjectID(item.userId!.toString()),
108
+ notificationMethod: {
109
+ userCallId: item.id!,
110
+ },
111
+ },
112
+ );
113
+ } catch (e) {
114
+ logger.error(e);
115
+ }
116
+
98
117
  return Response.sendEmptySuccessResponse(req, res);
99
118
  } catch (err) {
100
119
  return next(err);
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import UserEmailService, {
3
3
  Service as UserEmailServiceType,
4
4
  } from "../Services/UserEmailService";
5
+ import UserNotificationRuleService from "../Services/UserNotificationRuleService";
5
6
  import {
6
7
  ExpressRequest,
7
8
  ExpressResponse,
@@ -9,8 +10,10 @@ import {
9
10
  OneUptimeRequest,
10
11
  } from "../Utils/Express";
11
12
  import Response from "../Utils/Response";
13
+ import logger from "../Utils/Logger";
12
14
  import BaseAPI from "./BaseAPI";
13
15
  import BadDataException from "../../Types/Exception/BadDataException";
16
+ import ObjectID from "../../Types/ObjectID";
14
17
  import UserEmail from "../../Models/DatabaseModels/UserEmail";
15
18
 
16
19
  export default class UserEmailAPI extends BaseAPI<
@@ -51,6 +54,7 @@ export default class UserEmailAPI extends BaseAPI<
51
54
  },
52
55
  select: {
53
56
  userId: true,
57
+ projectId: true,
54
58
  verificationCode: true,
55
59
  },
56
60
  });
@@ -94,6 +98,21 @@ export default class UserEmailAPI extends BaseAPI<
94
98
  },
95
99
  });
96
100
 
101
+ // Create default notification rules for this verified email
102
+ try {
103
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
104
+ {
105
+ projectId: new ObjectID(item.projectId!.toString()),
106
+ userId: new ObjectID(item.userId!.toString()),
107
+ notificationMethod: {
108
+ userEmailId: item.id!,
109
+ },
110
+ },
111
+ );
112
+ } catch (e) {
113
+ logger.error(e);
114
+ }
115
+
97
116
  return Response.sendEmptySuccessResponse(req, res);
98
117
  } catch (err) {
99
118
  return next(err);
@@ -2,8 +2,10 @@ import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import UserPushService, {
3
3
  Service as UserPushServiceType,
4
4
  } from "../Services/UserPushService";
5
+ import UserNotificationRuleService from "../Services/UserNotificationRuleService";
5
6
  import PushNotificationService from "../Services/PushNotificationService";
6
7
  import PushNotificationUtil from "../Utils/PushNotificationUtil";
8
+ import logger from "../Utils/Logger";
7
9
  import {
8
10
  ExpressRequest,
9
11
  ExpressResponse,
@@ -13,11 +15,23 @@ import {
13
15
  import Response from "../Utils/Response";
14
16
  import BaseAPI from "./BaseAPI";
15
17
  import BadDataException from "../../Types/Exception/BadDataException";
18
+ import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
16
19
  import ObjectID from "../../Types/ObjectID";
17
20
  import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
18
21
  import UserPush from "../../Models/DatabaseModels/UserPush";
19
22
  import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
20
23
 
24
+ function getAuthenticatedUserId(req: ExpressRequest): ObjectID {
25
+ const userId: ObjectID | undefined = (req as OneUptimeRequest)
26
+ .userAuthorization?.userId;
27
+ if (!userId) {
28
+ throw new NotAuthenticatedException(
29
+ "You must be logged in to perform this action.",
30
+ );
31
+ }
32
+ return userId;
33
+ }
34
+
21
35
  export default class UserPushAPI extends BaseAPI<
22
36
  UserPush,
23
37
  UserPushServiceType
@@ -32,6 +46,8 @@ export default class UserPushAPI extends BaseAPI<
32
46
  try {
33
47
  req = req as OneUptimeRequest;
34
48
 
49
+ const userId: ObjectID = getAuthenticatedUserId(req);
50
+
35
51
  if (!req.body.deviceToken) {
36
52
  return Response.sendErrorResponse(
37
53
  req,
@@ -65,7 +81,7 @@ export default class UserPushAPI extends BaseAPI<
65
81
  // Check if device is already registered
66
82
  const existingDevice: UserPush | null = await this.service.findOneBy({
67
83
  query: {
68
- userId: (req as OneUptimeRequest).userAuthorization!.userId!,
84
+ userId: userId,
69
85
  projectId: new ObjectID(req.body.projectId),
70
86
  deviceToken: req.body.deviceToken,
71
87
  },
@@ -78,17 +94,18 @@ export default class UserPushAPI extends BaseAPI<
78
94
  });
79
95
 
80
96
  if (existingDevice) {
81
- // Mark as used and return a specific response indicating device was already registered
82
- throw new BadDataException(
83
- "This device is already registered for push notifications",
97
+ return Response.sendErrorResponse(
98
+ req,
99
+ res,
100
+ new BadDataException(
101
+ "This device is already registered for push notifications",
102
+ ),
84
103
  );
85
104
  }
86
105
 
87
106
  // Create new device registration
88
107
  const userPush: UserPush = new UserPush();
89
- userPush.userId = (
90
- req as OneUptimeRequest
91
- ).userAuthorization!.userId!;
108
+ userPush.userId = userId;
92
109
  userPush.projectId = new ObjectID(req.body.projectId);
93
110
  userPush.deviceToken = req.body.deviceToken;
94
111
  userPush.deviceType = req.body.deviceType;
@@ -102,6 +119,21 @@ export default class UserPushAPI extends BaseAPI<
102
119
  },
103
120
  });
104
121
 
122
+ // Create default notification rules for this registered push device
123
+ try {
124
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
125
+ {
126
+ projectId: new ObjectID(req.body.projectId),
127
+ userId,
128
+ notificationMethod: {
129
+ userPushId: savedDevice.id!,
130
+ },
131
+ },
132
+ );
133
+ } catch (e) {
134
+ logger.error(e);
135
+ }
136
+
105
137
  return Response.sendJsonObjectResponse(req, res, {
106
138
  success: true,
107
139
  deviceId: savedDevice._id!.toString(),
@@ -119,6 +151,8 @@ export default class UserPushAPI extends BaseAPI<
119
151
  try {
120
152
  req = req as OneUptimeRequest;
121
153
 
154
+ const userId: ObjectID = getAuthenticatedUserId(req);
155
+
122
156
  if (!req.body.deviceToken) {
123
157
  return Response.sendErrorResponse(
124
158
  req,
@@ -127,9 +161,6 @@ export default class UserPushAPI extends BaseAPI<
127
161
  );
128
162
  }
129
163
 
130
- const userId: ObjectID = (req as OneUptimeRequest).userAuthorization!
131
- .userId!;
132
-
133
164
  await this.service.deleteBy({
134
165
  query: {
135
166
  userId: userId,
@@ -159,6 +190,8 @@ export default class UserPushAPI extends BaseAPI<
159
190
  try {
160
191
  req = req as OneUptimeRequest;
161
192
 
193
+ const userId: ObjectID = getAuthenticatedUserId(req);
194
+
162
195
  if (!req.params["deviceId"]) {
163
196
  return Response.sendErrorResponse(
164
197
  req,
@@ -192,10 +225,7 @@ export default class UserPushAPI extends BaseAPI<
192
225
  }
193
226
 
194
227
  // Check if the device belongs to the current user
195
- if (
196
- device.userId?.toString() !==
197
- (req as OneUptimeRequest).userAuthorization!.userId!.toString()
198
- ) {
228
+ if (device.userId?.toString() !== userId.toString()) {
199
229
  return Response.sendErrorResponse(
200
230
  req,
201
231
  res,
@@ -264,6 +294,8 @@ export default class UserPushAPI extends BaseAPI<
264
294
  try {
265
295
  req = req as OneUptimeRequest;
266
296
 
297
+ const userId: ObjectID = getAuthenticatedUserId(req);
298
+
267
299
  if (!req.params["deviceId"]) {
268
300
  return Response.sendErrorResponse(
269
301
  req,
@@ -279,6 +311,7 @@ export default class UserPushAPI extends BaseAPI<
279
311
  },
280
312
  select: {
281
313
  userId: true,
314
+ projectId: true,
282
315
  },
283
316
  });
284
317
 
@@ -291,10 +324,7 @@ export default class UserPushAPI extends BaseAPI<
291
324
  }
292
325
 
293
326
  // Check if the device belongs to the current user
294
- if (
295
- device.userId?.toString() !==
296
- (req as OneUptimeRequest).userAuthorization!.userId!.toString()
297
- ) {
327
+ if (device.userId?.toString() !== userId.toString()) {
298
328
  return Response.sendErrorResponse(
299
329
  req,
300
330
  res,
@@ -304,6 +334,21 @@ export default class UserPushAPI extends BaseAPI<
304
334
 
305
335
  await this.service.verifyDevice(device._id!.toString());
306
336
 
337
+ // Create default notification rules for this verified push device
338
+ try {
339
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
340
+ {
341
+ projectId: new ObjectID(device.projectId!.toString()),
342
+ userId,
343
+ notificationMethod: {
344
+ userPushId: device.id!,
345
+ },
346
+ },
347
+ );
348
+ } catch (e) {
349
+ logger.error(e);
350
+ }
351
+
307
352
  return Response.sendEmptySuccessResponse(req, res);
308
353
  } catch (error) {
309
354
  return next(error);
@@ -318,6 +363,8 @@ export default class UserPushAPI extends BaseAPI<
318
363
  try {
319
364
  req = req as OneUptimeRequest;
320
365
 
366
+ const userId: ObjectID = getAuthenticatedUserId(req);
367
+
321
368
  if (!req.params["deviceId"]) {
322
369
  return Response.sendErrorResponse(
323
370
  req,
@@ -345,10 +392,7 @@ export default class UserPushAPI extends BaseAPI<
345
392
  }
346
393
 
347
394
  // Check if the device belongs to the current user
348
- if (
349
- device.userId?.toString() !==
350
- (req as OneUptimeRequest).userAuthorization!.userId!.toString()
351
- ) {
395
+ if (device.userId?.toString() !== userId.toString()) {
352
396
  return Response.sendErrorResponse(
353
397
  req,
354
398
  res,
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import UserSMSService, {
3
3
  Service as UserSMSServiceType,
4
4
  } from "../Services/UserSmsService";
5
+ import UserNotificationRuleService from "../Services/UserNotificationRuleService";
5
6
  import {
6
7
  ExpressRequest,
7
8
  ExpressResponse,
@@ -9,8 +10,10 @@ import {
9
10
  OneUptimeRequest,
10
11
  } from "../Utils/Express";
11
12
  import Response from "../Utils/Response";
13
+ import logger from "../Utils/Logger";
12
14
  import BaseAPI from "./BaseAPI";
13
15
  import BadDataException from "../../Types/Exception/BadDataException";
16
+ import ObjectID from "../../Types/ObjectID";
14
17
  import UserSMS from "../../Models/DatabaseModels/UserSMS";
15
18
 
16
19
  export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
@@ -48,6 +51,7 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
48
51
  },
49
52
  select: {
50
53
  userId: true,
54
+ projectId: true,
51
55
  verificationCode: true,
52
56
  },
53
57
  });
@@ -91,6 +95,21 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
91
95
  },
92
96
  });
93
97
 
98
+ // Create default notification rules for this verified SMS
99
+ try {
100
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
101
+ {
102
+ projectId: new ObjectID(item.projectId!.toString()),
103
+ userId: new ObjectID(item.userId!.toString()),
104
+ notificationMethod: {
105
+ userSmsId: item.id!,
106
+ },
107
+ },
108
+ );
109
+ } catch (e) {
110
+ logger.error(e);
111
+ }
112
+
94
113
  return Response.sendEmptySuccessResponse(req, res);
95
114
  } catch (err) {
96
115
  return next(err);
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import UserWhatsAppService, {
3
3
  Service as UserWhatsAppServiceType,
4
4
  } from "../Services/UserWhatsAppService";
5
+ import UserNotificationRuleService from "../Services/UserNotificationRuleService";
5
6
  import {
6
7
  ExpressRequest,
7
8
  ExpressResponse,
@@ -9,8 +10,10 @@ import {
9
10
  OneUptimeRequest,
10
11
  } from "../Utils/Express";
11
12
  import Response from "../Utils/Response";
13
+ import logger from "../Utils/Logger";
12
14
  import BaseAPI from "./BaseAPI";
13
15
  import BadDataException from "../../Types/Exception/BadDataException";
16
+ import ObjectID from "../../Types/ObjectID";
14
17
  import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
15
18
 
16
19
  export default class UserWhatsAppAPI extends BaseAPI<
@@ -50,6 +53,7 @@ export default class UserWhatsAppAPI extends BaseAPI<
50
53
  },
51
54
  select: {
52
55
  userId: true,
56
+ projectId: true,
53
57
  verificationCode: true,
54
58
  isVerified: true,
55
59
  },
@@ -100,6 +104,21 @@ export default class UserWhatsAppAPI extends BaseAPI<
100
104
  },
101
105
  });
102
106
 
107
+ // Create default notification rules for this verified WhatsApp number
108
+ try {
109
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
110
+ {
111
+ projectId: new ObjectID(item.projectId!.toString()),
112
+ userId: new ObjectID(item.userId!.toString()),
113
+ notificationMethod: {
114
+ userWhatsAppId: item.id!,
115
+ },
116
+ },
117
+ );
118
+ } catch (e) {
119
+ logger.error(e);
120
+ }
121
+
103
122
  return Response.sendEmptySuccessResponse(req, res);
104
123
  } catch (err) {
105
124
  return next(err);
@@ -161,6 +161,14 @@ export const ClusterKey: ObjectID = new ObjectID(
161
161
 
162
162
  export const HasClusterKey: boolean = Boolean(process.env["ONEUPTIME_SECRET"]);
163
163
 
164
+ export const RegisterProbeKey: ObjectID = new ObjectID(
165
+ process.env["REGISTER_PROBE_KEY"] || "secret",
166
+ );
167
+
168
+ export const HasRegisterProbeKey: boolean = Boolean(
169
+ process.env["REGISTER_PROBE_KEY"],
170
+ );
171
+
164
172
  export const AppApiHostname: Hostname = Hostname.fromString(
165
173
  `${process.env["SERVER_APP_HOSTNAME"] || "localhost"}:${
166
174
  process.env["APP_PORT"] || 80
@@ -529,6 +537,13 @@ export const VapidPrivateKey: string | undefined =
529
537
  export const VapidSubject: string =
530
538
  process.env["VAPID_SUBJECT"] || "mailto:support@oneuptime.com";
531
539
 
540
+ export const ExpoAccessToken: string | undefined =
541
+ process.env["EXPO_ACCESS_TOKEN"] || undefined;
542
+
543
+ export const PushNotificationRelayUrl: string =
544
+ process.env["PUSH_NOTIFICATION_RELAY_URL"] ||
545
+ "https://oneuptime.com/api/notification/push-relay/send";
546
+
532
547
  export const EnterpriseLicenseValidationUrl: URL = URL.fromString(
533
548
  "https://oneuptime.com/api/enterprise-license/validate",
534
549
  );
@@ -10,9 +10,16 @@ import {
10
10
  VapidPublicKey,
11
11
  VapidPrivateKey,
12
12
  VapidSubject,
13
+ ExpoAccessToken,
14
+ PushNotificationRelayUrl,
13
15
  } from "../EnvironmentConfig";
14
16
  import webpush from "web-push";
15
17
  import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
18
+ import API from "../../Utils/API";
19
+ import URL from "../../Types/API/URL";
20
+ import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
21
+ import HTTPResponse from "../../Types/API/HTTPResponse";
22
+ import { JSONObject } from "../../Types/JSON";
16
23
  import PushNotificationUtil from "../Utils/PushNotificationUtil";
17
24
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
18
25
  import UserPush from "../../Models/DatabaseModels/UserPush";
@@ -43,7 +50,9 @@ export interface PushNotificationOptions {
43
50
 
44
51
  export default class PushNotificationService {
45
52
  public static isWebPushInitialized = false;
46
- private static expoClient: Expo = new Expo();
53
+ private static expoClient: Expo = new Expo(
54
+ ExpoAccessToken ? { accessToken: ExpoAccessToken } : undefined,
55
+ );
47
56
 
48
57
  public static initializeWebPush(): void {
49
58
  if (this.isWebPushInitialized) {
@@ -340,20 +349,33 @@ export default class PushNotificationService {
340
349
  );
341
350
  }
342
351
 
343
- try {
344
- const dataPayload: { [key: string]: string } = {};
345
- if (message.data) {
346
- for (const key of Object.keys(message.data)) {
347
- dataPayload[key] = String(message.data[key]);
348
- }
349
- }
350
- if (message.url || message.clickAction) {
351
- dataPayload["url"] = message.url || message.clickAction || "";
352
+ const dataPayload: { [key: string]: string } = {};
353
+ if (message.data) {
354
+ for (const key of Object.keys(message.data)) {
355
+ dataPayload[key] = String(message.data[key]);
352
356
  }
357
+ }
358
+ if (message.url || message.clickAction) {
359
+ dataPayload["url"] = message.url || message.clickAction || "";
360
+ }
353
361
 
354
- const channelId: string =
355
- deviceType === PushDeviceType.Android ? "oncall_high" : "default";
362
+ const channelId: string =
363
+ deviceType === PushDeviceType.Android ? "oncall_high" : "default";
364
+
365
+ // If EXPO_ACCESS_TOKEN is not set, relay through the push notification gateway
366
+ if (!ExpoAccessToken) {
367
+ await this.sendViaRelay(
368
+ expoPushToken,
369
+ message,
370
+ dataPayload,
371
+ channelId,
372
+ deviceType,
373
+ );
374
+ return;
375
+ }
356
376
 
377
+ // Send directly via Expo SDK
378
+ try {
357
379
  const expoPushMessage: ExpoPushMessage = {
358
380
  to: expoPushToken,
359
381
  title: message.title,
@@ -403,6 +425,108 @@ export default class PushNotificationService {
403
425
  }
404
426
  }
405
427
 
428
+ private static async sendViaRelay(
429
+ expoPushToken: string,
430
+ message: PushNotificationMessage,
431
+ dataPayload: { [key: string]: string },
432
+ channelId: string,
433
+ deviceType: PushDeviceType,
434
+ ): Promise<void> {
435
+ logger.info(
436
+ `Sending ${deviceType} push notification via relay: ${PushNotificationRelayUrl}`,
437
+ );
438
+
439
+ try {
440
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
441
+ await API.post<JSONObject>({
442
+ url: URL.fromString(PushNotificationRelayUrl),
443
+ data: {
444
+ to: expoPushToken,
445
+ title: message.title || "",
446
+ body: message.body || "",
447
+ data: dataPayload,
448
+ sound: "default",
449
+ priority: "high",
450
+ channelId: channelId,
451
+ },
452
+ });
453
+
454
+ if (response instanceof HTTPErrorResponse) {
455
+ throw new Error(
456
+ `Push relay error: ${JSON.stringify(response.jsonData)}`,
457
+ );
458
+ }
459
+
460
+ logger.info(
461
+ `Push notification sent via relay successfully to ${deviceType} device`,
462
+ );
463
+ } catch (error: any) {
464
+ logger.error(
465
+ `Failed to send push notification via relay to ${deviceType} device: ${error.message}`,
466
+ );
467
+ throw error;
468
+ }
469
+ }
470
+
471
+ public static isValidExpoPushToken(token: string): boolean {
472
+ return Expo.isExpoPushToken(token);
473
+ }
474
+
475
+ public static hasExpoAccessToken(): boolean {
476
+ return Boolean(ExpoAccessToken);
477
+ }
478
+
479
+ public static async sendRelayPushNotification(data: {
480
+ to: string;
481
+ title?: string;
482
+ body?: string;
483
+ data?: { [key: string]: string };
484
+ sound?: string;
485
+ priority?: string;
486
+ channelId?: string;
487
+ }): Promise<void> {
488
+ if (!ExpoAccessToken) {
489
+ throw new Error(
490
+ "Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
491
+ );
492
+ }
493
+
494
+ const expoPushMessage: ExpoPushMessage = {
495
+ to: data.to,
496
+ title: data.title || "",
497
+ body: data.body || "",
498
+ data: data.data || {},
499
+ sound: (data.sound as "default" | null) || "default",
500
+ priority: (data.priority as "default" | "normal" | "high") || "high",
501
+ channelId: data.channelId || "default",
502
+ };
503
+
504
+ const tickets: ExpoPushTicket[] =
505
+ await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
506
+
507
+ const ticket: ExpoPushTicket | undefined = tickets[0];
508
+
509
+ if (ticket && ticket.status === "error") {
510
+ const errorTicket: ExpoPushTicket & {
511
+ message?: string;
512
+ details?: { error?: string };
513
+ } = ticket as ExpoPushTicket & {
514
+ message?: string;
515
+ details?: { error?: string };
516
+ };
517
+
518
+ logger.error(
519
+ `Push relay: Expo push notification error: ${errorTicket.message}`,
520
+ );
521
+
522
+ throw new Error(
523
+ `Failed to send push notification: ${errorTicket.message}`,
524
+ );
525
+ }
526
+
527
+ logger.info(`Push relay: notification sent successfully to ${data.to}`);
528
+ }
529
+
406
530
  public static async sendPushNotificationToUser(
407
531
  userId: ObjectID,
408
532
  projectId: ObjectID,