@spfn/notification 0.1.0-beta.13 → 0.1.0-beta.15
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.
- package/README.md +127 -0
- package/dist/config/index.d.ts +80 -1
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BAe1ZBYQ.d.ts → index-DHgXI5Eq.d.ts} +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/server.d.ts +279 -4
- package/dist/server.js +456 -72
- package/dist/server.js.map +1 -1
- package/migrations/0001_romantic_starjammers.sql +15 -0
- package/migrations/meta/0001_snapshot.json +393 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -3
package/dist/server.js
CHANGED
|
@@ -43,6 +43,29 @@ var notificationEnvSchema = defineEnvSchema({
|
|
|
43
43
|
examples: ["https://hooks.slack.com/services/xxx/xxx/xxx"]
|
|
44
44
|
})
|
|
45
45
|
},
|
|
46
|
+
// Tracking
|
|
47
|
+
SPFN_NOTIFICATION_TRACKING_ENABLED: {
|
|
48
|
+
...envString({
|
|
49
|
+
description: "Enable email engagement tracking (open/click)",
|
|
50
|
+
default: "false",
|
|
51
|
+
required: false,
|
|
52
|
+
examples: ["true", "false"]
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
SPFN_NOTIFICATION_TRACKING_SECRET: {
|
|
56
|
+
...envString({
|
|
57
|
+
description: "HMAC secret key for tracking token signing",
|
|
58
|
+
required: false,
|
|
59
|
+
sensitive: true
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
SPFN_NOTIFICATION_TRACKING_BASE_URL: {
|
|
63
|
+
...envString({
|
|
64
|
+
description: "Base URL for tracking endpoints",
|
|
65
|
+
required: false,
|
|
66
|
+
examples: ["https://api.example.com"]
|
|
67
|
+
})
|
|
68
|
+
},
|
|
46
69
|
// AWS (shared with other AWS services)
|
|
47
70
|
AWS_REGION: {
|
|
48
71
|
...envString({
|
|
@@ -93,6 +116,18 @@ function getAppName() {
|
|
|
93
116
|
function isHistoryEnabled() {
|
|
94
117
|
return globalConfig.enableHistory ?? false;
|
|
95
118
|
}
|
|
119
|
+
function isTrackingEnabled() {
|
|
120
|
+
if (globalConfig.tracking?.enabled != null) {
|
|
121
|
+
return globalConfig.tracking.enabled;
|
|
122
|
+
}
|
|
123
|
+
return env.SPFN_NOTIFICATION_TRACKING_ENABLED === "true";
|
|
124
|
+
}
|
|
125
|
+
function getTrackingSecret() {
|
|
126
|
+
return globalConfig.tracking?.secret ?? env.SPFN_NOTIFICATION_TRACKING_SECRET;
|
|
127
|
+
}
|
|
128
|
+
function getTrackingBaseUrl() {
|
|
129
|
+
return globalConfig.tracking?.baseUrl ?? env.SPFN_NOTIFICATION_TRACKING_BASE_URL;
|
|
130
|
+
}
|
|
96
131
|
|
|
97
132
|
// src/channels/email/providers/aws-ses.ts
|
|
98
133
|
import { logger } from "@spfn/core/logger";
|
|
@@ -168,6 +203,114 @@ var awsSesProvider = {
|
|
|
168
203
|
}
|
|
169
204
|
};
|
|
170
205
|
|
|
206
|
+
// src/tracking/token.ts
|
|
207
|
+
import { createHmac } from "crypto";
|
|
208
|
+
function toBase64Url(buffer) {
|
|
209
|
+
return buffer.toString("base64url");
|
|
210
|
+
}
|
|
211
|
+
function fromBase64Url(str) {
|
|
212
|
+
return Buffer.from(str, "base64url").toString("utf8");
|
|
213
|
+
}
|
|
214
|
+
function sign(payload) {
|
|
215
|
+
const secret = getTrackingSecret();
|
|
216
|
+
if (!secret) {
|
|
217
|
+
throw new Error("Tracking secret is not configured");
|
|
218
|
+
}
|
|
219
|
+
const payloadEncoded = toBase64Url(Buffer.from(payload, "utf8"));
|
|
220
|
+
const hmac = createHmac("sha256", secret).update(payload).digest();
|
|
221
|
+
const hmacEncoded = toBase64Url(hmac);
|
|
222
|
+
return `${payloadEncoded}.${hmacEncoded}`;
|
|
223
|
+
}
|
|
224
|
+
function verify(token) {
|
|
225
|
+
const secret = getTrackingSecret();
|
|
226
|
+
if (!secret) {
|
|
227
|
+
return { valid: false };
|
|
228
|
+
}
|
|
229
|
+
const dotIndex = token.indexOf(".");
|
|
230
|
+
if (dotIndex === -1) {
|
|
231
|
+
return { valid: false };
|
|
232
|
+
}
|
|
233
|
+
const payloadEncoded = token.substring(0, dotIndex);
|
|
234
|
+
const hmacEncoded = token.substring(dotIndex + 1);
|
|
235
|
+
const payload = fromBase64Url(payloadEncoded);
|
|
236
|
+
const expectedHmac = createHmac("sha256", secret).update(payload).digest();
|
|
237
|
+
const expectedHmacEncoded = toBase64Url(expectedHmac);
|
|
238
|
+
if (hmacEncoded !== expectedHmacEncoded) {
|
|
239
|
+
return { valid: false };
|
|
240
|
+
}
|
|
241
|
+
return { valid: true, payload };
|
|
242
|
+
}
|
|
243
|
+
function generateOpenToken(notificationId) {
|
|
244
|
+
return sign(`o:${notificationId}`);
|
|
245
|
+
}
|
|
246
|
+
function generateClickToken(notificationId, linkIndex) {
|
|
247
|
+
return sign(`c:${notificationId}:${linkIndex}`);
|
|
248
|
+
}
|
|
249
|
+
function verifyOpenToken(token) {
|
|
250
|
+
const result = verify(token);
|
|
251
|
+
if (!result.valid || !result.payload) {
|
|
252
|
+
return { valid: false };
|
|
253
|
+
}
|
|
254
|
+
const match = result.payload.match(/^o:(\d+)$/);
|
|
255
|
+
if (!match) {
|
|
256
|
+
return { valid: false };
|
|
257
|
+
}
|
|
258
|
+
return { valid: true, notificationId: Number(match[1]) };
|
|
259
|
+
}
|
|
260
|
+
function verifyClickToken(token) {
|
|
261
|
+
const result = verify(token);
|
|
262
|
+
if (!result.valid || !result.payload) {
|
|
263
|
+
return { valid: false };
|
|
264
|
+
}
|
|
265
|
+
const match = result.payload.match(/^c:(\d+):(\d+)$/);
|
|
266
|
+
if (!match) {
|
|
267
|
+
return { valid: false };
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
valid: true,
|
|
271
|
+
notificationId: Number(match[1]),
|
|
272
|
+
linkIndex: Number(match[2])
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/tracking/processor.ts
|
|
277
|
+
var SKIP_PROTOCOLS = ["mailto:", "tel:", "sms:", "javascript:"];
|
|
278
|
+
function shouldSkipUrl(url) {
|
|
279
|
+
const trimmed = url.trim();
|
|
280
|
+
if (trimmed === "" || trimmed === "#" || trimmed.startsWith("#")) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const lower = trimmed.toLowerCase();
|
|
284
|
+
return SKIP_PROTOCOLS.some((proto) => lower.startsWith(proto));
|
|
285
|
+
}
|
|
286
|
+
function processTrackingHtml(html, options) {
|
|
287
|
+
const { notificationId, baseUrl } = options;
|
|
288
|
+
const trackedLinks = [];
|
|
289
|
+
let linkIndex = 0;
|
|
290
|
+
const processedHtml = html.replace(
|
|
291
|
+
/<a\s([^>]*?)href\s*=\s*["']([^"']+)["']([^>]*?)>/gi,
|
|
292
|
+
(match, before, url, after) => {
|
|
293
|
+
if (shouldSkipUrl(url)) {
|
|
294
|
+
return match;
|
|
295
|
+
}
|
|
296
|
+
const currentIndex = linkIndex++;
|
|
297
|
+
const clickToken = generateClickToken(notificationId, currentIndex);
|
|
298
|
+
const trackingUrl = `${baseUrl}/_noti/t/c/${clickToken}?url=${encodeURIComponent(url)}`;
|
|
299
|
+
trackedLinks.push({ index: currentIndex, url });
|
|
300
|
+
return `<a ${before}href="${trackingUrl}"${after}>`;
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
const openToken = generateOpenToken(notificationId);
|
|
304
|
+
const pixel = `<img src="${baseUrl}/_noti/t/o/${openToken}" width="1" height="1" style="display:none" alt="" />`;
|
|
305
|
+
let finalHtml;
|
|
306
|
+
if (processedHtml.includes("</body>")) {
|
|
307
|
+
finalHtml = processedHtml.replace("</body>", `${pixel}</body>`);
|
|
308
|
+
} else {
|
|
309
|
+
finalHtml = processedHtml + pixel;
|
|
310
|
+
}
|
|
311
|
+
return { html: finalHtml, trackedLinks };
|
|
312
|
+
}
|
|
313
|
+
|
|
171
314
|
// src/templates/renderer.ts
|
|
172
315
|
var filters = {
|
|
173
316
|
/**
|
|
@@ -503,6 +646,47 @@ var notifications = notificationSchema.table(
|
|
|
503
646
|
]
|
|
504
647
|
);
|
|
505
648
|
|
|
649
|
+
// src/entities/tracking-events.ts
|
|
650
|
+
import { integer, text as text2, index as index2 } from "drizzle-orm/pg-core";
|
|
651
|
+
import { id as id2, timestamps as timestamps2 } from "@spfn/core/db";
|
|
652
|
+
var TRACKING_EVENT_TYPES = ["open", "click"];
|
|
653
|
+
var trackingEvents = notificationSchema.table(
|
|
654
|
+
"tracking_events",
|
|
655
|
+
{
|
|
656
|
+
id: id2(),
|
|
657
|
+
/**
|
|
658
|
+
* Reference to history.id (notification that was tracked)
|
|
659
|
+
*/
|
|
660
|
+
notificationId: integer("notification_id").notNull(),
|
|
661
|
+
/**
|
|
662
|
+
* Event type (open or click)
|
|
663
|
+
*/
|
|
664
|
+
type: text2("type", { enum: TRACKING_EVENT_TYPES }).notNull(),
|
|
665
|
+
/**
|
|
666
|
+
* Original URL (click events only)
|
|
667
|
+
*/
|
|
668
|
+
linkUrl: text2("link_url"),
|
|
669
|
+
/**
|
|
670
|
+
* Link position index in the email (click events only)
|
|
671
|
+
*/
|
|
672
|
+
linkIndex: integer("link_index"),
|
|
673
|
+
/**
|
|
674
|
+
* IP address of the requester
|
|
675
|
+
*/
|
|
676
|
+
ipAddress: text2("ip_address"),
|
|
677
|
+
/**
|
|
678
|
+
* User agent string of the requester
|
|
679
|
+
*/
|
|
680
|
+
userAgent: text2("user_agent"),
|
|
681
|
+
...timestamps2()
|
|
682
|
+
},
|
|
683
|
+
(table) => [
|
|
684
|
+
index2("te_notification_id_idx").on(table.notificationId),
|
|
685
|
+
index2("te_type_idx").on(table.type),
|
|
686
|
+
index2("te_created_at_idx").on(table.createdAt)
|
|
687
|
+
]
|
|
688
|
+
);
|
|
689
|
+
|
|
506
690
|
// src/services/notification.service.ts
|
|
507
691
|
async function createNotificationRecord(data) {
|
|
508
692
|
return await create(notifications, {
|
|
@@ -516,13 +700,13 @@ async function createScheduledNotification(data) {
|
|
|
516
700
|
status: "scheduled"
|
|
517
701
|
});
|
|
518
702
|
}
|
|
519
|
-
async function updateNotificationJobId(
|
|
520
|
-
return await updateOne(notifications, { id:
|
|
703
|
+
async function updateNotificationJobId(id3, jobId) {
|
|
704
|
+
return await updateOne(notifications, { id: id3 }, { jobId });
|
|
521
705
|
}
|
|
522
|
-
async function markNotificationSent(
|
|
706
|
+
async function markNotificationSent(id3, providerMessageId) {
|
|
523
707
|
return await updateOne(
|
|
524
708
|
notifications,
|
|
525
|
-
{ id:
|
|
709
|
+
{ id: id3 },
|
|
526
710
|
{
|
|
527
711
|
status: "sent",
|
|
528
712
|
sentAt: /* @__PURE__ */ new Date(),
|
|
@@ -530,27 +714,27 @@ async function markNotificationSent(id2, providerMessageId) {
|
|
|
530
714
|
}
|
|
531
715
|
);
|
|
532
716
|
}
|
|
533
|
-
async function markNotificationFailed(
|
|
717
|
+
async function markNotificationFailed(id3, errorMessage) {
|
|
534
718
|
return await updateOne(
|
|
535
719
|
notifications,
|
|
536
|
-
{ id:
|
|
720
|
+
{ id: id3 },
|
|
537
721
|
{
|
|
538
722
|
status: "failed",
|
|
539
723
|
errorMessage
|
|
540
724
|
}
|
|
541
725
|
);
|
|
542
726
|
}
|
|
543
|
-
async function markNotificationPending(
|
|
727
|
+
async function markNotificationPending(id3) {
|
|
544
728
|
return await updateOne(
|
|
545
729
|
notifications,
|
|
546
|
-
{ id:
|
|
730
|
+
{ id: id3 },
|
|
547
731
|
{ status: "pending" }
|
|
548
732
|
);
|
|
549
733
|
}
|
|
550
|
-
async function cancelScheduledNotification(
|
|
734
|
+
async function cancelScheduledNotification(id3) {
|
|
551
735
|
return await updateOne(
|
|
552
736
|
notifications,
|
|
553
|
-
{ id:
|
|
737
|
+
{ id: id3 },
|
|
554
738
|
{ status: "cancelled" }
|
|
555
739
|
);
|
|
556
740
|
}
|
|
@@ -653,7 +837,7 @@ function getProvider() {
|
|
|
653
837
|
async function sendEmail(params) {
|
|
654
838
|
const recipients = Array.isArray(params.to) ? params.to : [params.to];
|
|
655
839
|
let subject = params.subject;
|
|
656
|
-
let
|
|
840
|
+
let text3 = params.text;
|
|
657
841
|
let html = params.html;
|
|
658
842
|
if (params.template) {
|
|
659
843
|
if (!hasTemplate(params.template)) {
|
|
@@ -666,7 +850,7 @@ async function sendEmail(params) {
|
|
|
666
850
|
const rendered = renderTemplate(params.template, params.data || {}, "email");
|
|
667
851
|
if (rendered.email) {
|
|
668
852
|
subject = rendered.email.subject;
|
|
669
|
-
|
|
853
|
+
text3 = rendered.email.text;
|
|
670
854
|
html = rendered.email.html;
|
|
671
855
|
}
|
|
672
856
|
}
|
|
@@ -677,7 +861,7 @@ async function sendEmail(params) {
|
|
|
677
861
|
error: "Email subject is required"
|
|
678
862
|
};
|
|
679
863
|
}
|
|
680
|
-
if (!
|
|
864
|
+
if (!text3 && !html) {
|
|
681
865
|
log2.warn("Email content (text or html) is required", { to: recipients, subject });
|
|
682
866
|
return {
|
|
683
867
|
success: false,
|
|
@@ -689,7 +873,7 @@ async function sendEmail(params) {
|
|
|
689
873
|
from: params.from || getEmailFrom(),
|
|
690
874
|
replyTo: params.replyTo || getEmailReplyTo(),
|
|
691
875
|
subject,
|
|
692
|
-
text:
|
|
876
|
+
text: text3,
|
|
693
877
|
html
|
|
694
878
|
};
|
|
695
879
|
const provider = getProvider();
|
|
@@ -702,7 +886,7 @@ async function sendEmail(params) {
|
|
|
702
886
|
templateName: params.template,
|
|
703
887
|
templateData: params.data,
|
|
704
888
|
subject,
|
|
705
|
-
content:
|
|
889
|
+
content: text3,
|
|
706
890
|
providerName: provider.name
|
|
707
891
|
});
|
|
708
892
|
historyId = record.id;
|
|
@@ -710,6 +894,19 @@ async function sendEmail(params) {
|
|
|
710
894
|
log2.warn("Failed to create notification history record", error);
|
|
711
895
|
}
|
|
712
896
|
}
|
|
897
|
+
const shouldTrack = params.tracking ?? isTrackingEnabled();
|
|
898
|
+
const trackingBaseUrl = getTrackingBaseUrl();
|
|
899
|
+
if (shouldTrack && historyId && internalParams.html && trackingBaseUrl) {
|
|
900
|
+
try {
|
|
901
|
+
const { html: trackedHtml } = processTrackingHtml(internalParams.html, {
|
|
902
|
+
notificationId: historyId,
|
|
903
|
+
baseUrl: trackingBaseUrl
|
|
904
|
+
});
|
|
905
|
+
internalParams.html = trackedHtml;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
log2.warn("Failed to apply tracking to email HTML", error);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
713
910
|
const result = await provider.send(internalParams);
|
|
714
911
|
if (result.success) {
|
|
715
912
|
log2.info("Email sent", { to: recipients, subject, messageId: result.messageId });
|
|
@@ -977,7 +1174,7 @@ async function sendSlack(params) {
|
|
|
977
1174
|
error: "Slack webhook URL is required. Set SPFN_NOTIFICATION_SLACK_WEBHOOK_URL or pass webhookUrl."
|
|
978
1175
|
};
|
|
979
1176
|
}
|
|
980
|
-
let
|
|
1177
|
+
let text3 = params.text;
|
|
981
1178
|
let blocks = params.blocks;
|
|
982
1179
|
if (params.template) {
|
|
983
1180
|
if (!hasTemplate(params.template)) {
|
|
@@ -989,11 +1186,11 @@ async function sendSlack(params) {
|
|
|
989
1186
|
}
|
|
990
1187
|
const rendered = renderTemplate(params.template, params.data || {}, "slack");
|
|
991
1188
|
if (rendered.slack) {
|
|
992
|
-
|
|
1189
|
+
text3 = rendered.slack.text;
|
|
993
1190
|
blocks = rendered.slack.blocks;
|
|
994
1191
|
}
|
|
995
1192
|
}
|
|
996
|
-
if (!
|
|
1193
|
+
if (!text3 && !blocks) {
|
|
997
1194
|
log6.warn("Slack message requires text or blocks");
|
|
998
1195
|
return {
|
|
999
1196
|
success: false,
|
|
@@ -1002,7 +1199,7 @@ async function sendSlack(params) {
|
|
|
1002
1199
|
}
|
|
1003
1200
|
const internalParams = {
|
|
1004
1201
|
webhookUrl,
|
|
1005
|
-
text:
|
|
1202
|
+
text: text3,
|
|
1006
1203
|
blocks
|
|
1007
1204
|
};
|
|
1008
1205
|
const provider = getProvider3();
|
|
@@ -1014,7 +1211,7 @@ async function sendSlack(params) {
|
|
|
1014
1211
|
recipient: webhookUrl,
|
|
1015
1212
|
templateName: params.template,
|
|
1016
1213
|
templateData: params.data,
|
|
1017
|
-
content:
|
|
1214
|
+
content: text3,
|
|
1018
1215
|
providerName: provider.name
|
|
1019
1216
|
});
|
|
1020
1217
|
historyId = record.id;
|
|
@@ -1733,8 +1930,8 @@ function Array2(items, options) {
|
|
|
1733
1930
|
}
|
|
1734
1931
|
|
|
1735
1932
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs
|
|
1736
|
-
function Argument(
|
|
1737
|
-
return CreateType({ [Kind]: "Argument", index:
|
|
1933
|
+
function Argument(index3) {
|
|
1934
|
+
return CreateType({ [Kind]: "Argument", index: index3 });
|
|
1738
1935
|
}
|
|
1739
1936
|
|
|
1740
1937
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/async-iterator/async-iterator.mjs
|
|
@@ -1813,28 +2010,28 @@ var TemplateLiteralParserError = class extends TypeBoxError {
|
|
|
1813
2010
|
function Unescape(pattern) {
|
|
1814
2011
|
return pattern.replace(/\\\$/g, "$").replace(/\\\*/g, "*").replace(/\\\^/g, "^").replace(/\\\|/g, "|").replace(/\\\(/g, "(").replace(/\\\)/g, ")");
|
|
1815
2012
|
}
|
|
1816
|
-
function IsNonEscaped(pattern,
|
|
1817
|
-
return pattern[
|
|
2013
|
+
function IsNonEscaped(pattern, index3, char) {
|
|
2014
|
+
return pattern[index3] === char && pattern.charCodeAt(index3 - 1) !== 92;
|
|
1818
2015
|
}
|
|
1819
|
-
function IsOpenParen(pattern,
|
|
1820
|
-
return IsNonEscaped(pattern,
|
|
2016
|
+
function IsOpenParen(pattern, index3) {
|
|
2017
|
+
return IsNonEscaped(pattern, index3, "(");
|
|
1821
2018
|
}
|
|
1822
|
-
function IsCloseParen(pattern,
|
|
1823
|
-
return IsNonEscaped(pattern,
|
|
2019
|
+
function IsCloseParen(pattern, index3) {
|
|
2020
|
+
return IsNonEscaped(pattern, index3, ")");
|
|
1824
2021
|
}
|
|
1825
|
-
function IsSeparator(pattern,
|
|
1826
|
-
return IsNonEscaped(pattern,
|
|
2022
|
+
function IsSeparator(pattern, index3) {
|
|
2023
|
+
return IsNonEscaped(pattern, index3, "|");
|
|
1827
2024
|
}
|
|
1828
2025
|
function IsGroup(pattern) {
|
|
1829
2026
|
if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1)))
|
|
1830
2027
|
return false;
|
|
1831
2028
|
let count2 = 0;
|
|
1832
|
-
for (let
|
|
1833
|
-
if (IsOpenParen(pattern,
|
|
2029
|
+
for (let index3 = 0; index3 < pattern.length; index3++) {
|
|
2030
|
+
if (IsOpenParen(pattern, index3))
|
|
1834
2031
|
count2 += 1;
|
|
1835
|
-
if (IsCloseParen(pattern,
|
|
2032
|
+
if (IsCloseParen(pattern, index3))
|
|
1836
2033
|
count2 -= 1;
|
|
1837
|
-
if (count2 === 0 &&
|
|
2034
|
+
if (count2 === 0 && index3 !== pattern.length - 1)
|
|
1838
2035
|
return false;
|
|
1839
2036
|
}
|
|
1840
2037
|
return true;
|
|
@@ -1844,19 +2041,19 @@ function InGroup(pattern) {
|
|
|
1844
2041
|
}
|
|
1845
2042
|
function IsPrecedenceOr(pattern) {
|
|
1846
2043
|
let count2 = 0;
|
|
1847
|
-
for (let
|
|
1848
|
-
if (IsOpenParen(pattern,
|
|
2044
|
+
for (let index3 = 0; index3 < pattern.length; index3++) {
|
|
2045
|
+
if (IsOpenParen(pattern, index3))
|
|
1849
2046
|
count2 += 1;
|
|
1850
|
-
if (IsCloseParen(pattern,
|
|
2047
|
+
if (IsCloseParen(pattern, index3))
|
|
1851
2048
|
count2 -= 1;
|
|
1852
|
-
if (IsSeparator(pattern,
|
|
2049
|
+
if (IsSeparator(pattern, index3) && count2 === 0)
|
|
1853
2050
|
return true;
|
|
1854
2051
|
}
|
|
1855
2052
|
return false;
|
|
1856
2053
|
}
|
|
1857
2054
|
function IsPrecedenceAnd(pattern) {
|
|
1858
|
-
for (let
|
|
1859
|
-
if (IsOpenParen(pattern,
|
|
2055
|
+
for (let index3 = 0; index3 < pattern.length; index3++) {
|
|
2056
|
+
if (IsOpenParen(pattern, index3))
|
|
1860
2057
|
return true;
|
|
1861
2058
|
}
|
|
1862
2059
|
return false;
|
|
@@ -1864,16 +2061,16 @@ function IsPrecedenceAnd(pattern) {
|
|
|
1864
2061
|
function Or(pattern) {
|
|
1865
2062
|
let [count2, start] = [0, 0];
|
|
1866
2063
|
const expressions = [];
|
|
1867
|
-
for (let
|
|
1868
|
-
if (IsOpenParen(pattern,
|
|
2064
|
+
for (let index3 = 0; index3 < pattern.length; index3++) {
|
|
2065
|
+
if (IsOpenParen(pattern, index3))
|
|
1869
2066
|
count2 += 1;
|
|
1870
|
-
if (IsCloseParen(pattern,
|
|
2067
|
+
if (IsCloseParen(pattern, index3))
|
|
1871
2068
|
count2 -= 1;
|
|
1872
|
-
if (IsSeparator(pattern,
|
|
1873
|
-
const range2 = pattern.slice(start,
|
|
2069
|
+
if (IsSeparator(pattern, index3) && count2 === 0) {
|
|
2070
|
+
const range2 = pattern.slice(start, index3);
|
|
1874
2071
|
if (range2.length > 0)
|
|
1875
2072
|
expressions.push(TemplateLiteralParse(range2));
|
|
1876
|
-
start =
|
|
2073
|
+
start = index3 + 1;
|
|
1877
2074
|
}
|
|
1878
2075
|
}
|
|
1879
2076
|
const range = pattern.slice(start);
|
|
@@ -1886,40 +2083,40 @@ function Or(pattern) {
|
|
|
1886
2083
|
return { type: "or", expr: expressions };
|
|
1887
2084
|
}
|
|
1888
2085
|
function And(pattern) {
|
|
1889
|
-
function Group(value,
|
|
1890
|
-
if (!IsOpenParen(value,
|
|
2086
|
+
function Group(value, index3) {
|
|
2087
|
+
if (!IsOpenParen(value, index3))
|
|
1891
2088
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
|
|
1892
2089
|
let count2 = 0;
|
|
1893
|
-
for (let scan =
|
|
2090
|
+
for (let scan = index3; scan < value.length; scan++) {
|
|
1894
2091
|
if (IsOpenParen(value, scan))
|
|
1895
2092
|
count2 += 1;
|
|
1896
2093
|
if (IsCloseParen(value, scan))
|
|
1897
2094
|
count2 -= 1;
|
|
1898
2095
|
if (count2 === 0)
|
|
1899
|
-
return [
|
|
2096
|
+
return [index3, scan];
|
|
1900
2097
|
}
|
|
1901
2098
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
|
|
1902
2099
|
}
|
|
1903
|
-
function Range(pattern2,
|
|
1904
|
-
for (let scan =
|
|
2100
|
+
function Range(pattern2, index3) {
|
|
2101
|
+
for (let scan = index3; scan < pattern2.length; scan++) {
|
|
1905
2102
|
if (IsOpenParen(pattern2, scan))
|
|
1906
|
-
return [
|
|
2103
|
+
return [index3, scan];
|
|
1907
2104
|
}
|
|
1908
|
-
return [
|
|
2105
|
+
return [index3, pattern2.length];
|
|
1909
2106
|
}
|
|
1910
2107
|
const expressions = [];
|
|
1911
|
-
for (let
|
|
1912
|
-
if (IsOpenParen(pattern,
|
|
1913
|
-
const [start, end] = Group(pattern,
|
|
2108
|
+
for (let index3 = 0; index3 < pattern.length; index3++) {
|
|
2109
|
+
if (IsOpenParen(pattern, index3)) {
|
|
2110
|
+
const [start, end] = Group(pattern, index3);
|
|
1914
2111
|
const range = pattern.slice(start, end + 1);
|
|
1915
2112
|
expressions.push(TemplateLiteralParse(range));
|
|
1916
|
-
|
|
2113
|
+
index3 = end;
|
|
1917
2114
|
} else {
|
|
1918
|
-
const [start, end] = Range(pattern,
|
|
2115
|
+
const [start, end] = Range(pattern, index3);
|
|
1919
2116
|
const range = pattern.slice(start, end);
|
|
1920
2117
|
if (range.length > 0)
|
|
1921
2118
|
expressions.push(TemplateLiteralParse(range));
|
|
1922
|
-
|
|
2119
|
+
index3 = end - 1;
|
|
1923
2120
|
}
|
|
1924
2121
|
}
|
|
1925
2122
|
return expressions.length === 0 ? { type: "const", const: "" } : expressions.length === 1 ? expressions[0] : { type: "and", expr: expressions };
|
|
@@ -2611,13 +2808,13 @@ function FromBoolean(left, right) {
|
|
|
2611
2808
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsBoolean(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2612
2809
|
}
|
|
2613
2810
|
function FromConstructor(left, right) {
|
|
2614
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2811
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index3) => IntoBooleanResult(Visit3(right.parameters[index3], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2615
2812
|
}
|
|
2616
2813
|
function FromDate(left, right) {
|
|
2617
2814
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsDate(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2618
2815
|
}
|
|
2619
2816
|
function FromFunction(left, right) {
|
|
2620
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2817
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index3) => IntoBooleanResult(Visit3(right.parameters[index3], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2621
2818
|
}
|
|
2622
2819
|
function FromIntegerRight(left, right) {
|
|
2623
2820
|
return type_exports.IsLiteral(left) && value_exports.IsNumber(left.const) ? ExtendsResult.True : type_exports.IsNumber(left) || type_exports.IsInteger(left) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -2777,7 +2974,7 @@ function FromTupleRight(left, right) {
|
|
|
2777
2974
|
return type_exports.IsNever(left) ? ExtendsResult.True : type_exports.IsUnknown(left) ? ExtendsResult.False : type_exports.IsAny(left) ? ExtendsResult.Union : ExtendsResult.False;
|
|
2778
2975
|
}
|
|
2779
2976
|
function FromTuple3(left, right) {
|
|
2780
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema,
|
|
2977
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index3) => Visit3(schema, right.items[index3]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
|
|
2781
2978
|
}
|
|
2782
2979
|
function FromUint8Array(left, right) {
|
|
2783
2980
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsUint8Array(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -3726,7 +3923,7 @@ var sendScheduledSmsJob = job2("notification.send-scheduled-sms").input(SendSche
|
|
|
3726
3923
|
async function scheduleEmail(params, options) {
|
|
3727
3924
|
const recipients = Array.isArray(params.to) ? params.to : [params.to];
|
|
3728
3925
|
let subject = params.subject;
|
|
3729
|
-
let
|
|
3926
|
+
let text3 = params.text;
|
|
3730
3927
|
let html = params.html;
|
|
3731
3928
|
if (params.template) {
|
|
3732
3929
|
if (!hasTemplate(params.template)) {
|
|
@@ -3738,7 +3935,7 @@ async function scheduleEmail(params, options) {
|
|
|
3738
3935
|
const rendered = renderTemplate(params.template, params.data || {}, "email");
|
|
3739
3936
|
if (rendered.email) {
|
|
3740
3937
|
subject = rendered.email.subject;
|
|
3741
|
-
|
|
3938
|
+
text3 = rendered.email.text;
|
|
3742
3939
|
html = rendered.email.html;
|
|
3743
3940
|
}
|
|
3744
3941
|
}
|
|
@@ -3748,7 +3945,7 @@ async function scheduleEmail(params, options) {
|
|
|
3748
3945
|
error: "Email subject is required"
|
|
3749
3946
|
};
|
|
3750
3947
|
}
|
|
3751
|
-
if (!
|
|
3948
|
+
if (!text3 && !html) {
|
|
3752
3949
|
return {
|
|
3753
3950
|
success: false,
|
|
3754
3951
|
error: "Email content (text or html) is required"
|
|
@@ -3761,7 +3958,7 @@ async function scheduleEmail(params, options) {
|
|
|
3761
3958
|
templateName: params.template,
|
|
3762
3959
|
templateData: params.data,
|
|
3763
3960
|
subject,
|
|
3764
|
-
content:
|
|
3961
|
+
content: text3,
|
|
3765
3962
|
providerName: "pending",
|
|
3766
3963
|
// Will be set when job runs
|
|
3767
3964
|
scheduledAt: options.scheduledAt,
|
|
@@ -3896,12 +4093,12 @@ async function cancelNotification(notificationId) {
|
|
|
3896
4093
|
}
|
|
3897
4094
|
async function cancelNotificationsByReference(referenceType, referenceId) {
|
|
3898
4095
|
const { findMany: findMany2 } = await import("@spfn/core/db");
|
|
3899
|
-
const { eq:
|
|
4096
|
+
const { eq: eq3, and: and3 } = await import("drizzle-orm");
|
|
3900
4097
|
const scheduledNotifications = await findMany2(notifications, {
|
|
3901
|
-
where:
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4098
|
+
where: and3(
|
|
4099
|
+
eq3(notifications.referenceType, referenceType),
|
|
4100
|
+
eq3(notifications.referenceId, referenceId),
|
|
4101
|
+
eq3(notifications.status, "scheduled")
|
|
3905
4102
|
)
|
|
3906
4103
|
});
|
|
3907
4104
|
let cancelled = 0;
|
|
@@ -4045,11 +4242,189 @@ function createErrorSlackNotifier(options = {}) {
|
|
|
4045
4242
|
};
|
|
4046
4243
|
}
|
|
4047
4244
|
|
|
4245
|
+
// src/tracking/routes.ts
|
|
4246
|
+
import { route } from "@spfn/core/route";
|
|
4247
|
+
import { defineRouter } from "@spfn/core/route";
|
|
4248
|
+
|
|
4249
|
+
// src/tracking/tracking.service.ts
|
|
4250
|
+
import { create as create2, getDatabase } from "@spfn/core/db";
|
|
4251
|
+
import { eq as eq2, and as and2, gte as gte2, lte as lte2, count as drizzleCount, countDistinct } from "drizzle-orm";
|
|
4252
|
+
import { logger as logger7 } from "@spfn/core/logger";
|
|
4253
|
+
var log7 = logger7.child("@spfn/notification:tracking");
|
|
4254
|
+
function recordOpenEvent(notificationId, meta) {
|
|
4255
|
+
create2(trackingEvents, {
|
|
4256
|
+
notificationId,
|
|
4257
|
+
type: "open",
|
|
4258
|
+
ipAddress: meta?.ipAddress,
|
|
4259
|
+
userAgent: meta?.userAgent
|
|
4260
|
+
}).catch((error) => {
|
|
4261
|
+
log7.warn("Failed to record open event", error);
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
function recordClickEvent(notificationId, linkIndex, linkUrl, meta) {
|
|
4265
|
+
create2(trackingEvents, {
|
|
4266
|
+
notificationId,
|
|
4267
|
+
type: "click",
|
|
4268
|
+
linkUrl,
|
|
4269
|
+
linkIndex,
|
|
4270
|
+
ipAddress: meta?.ipAddress,
|
|
4271
|
+
userAgent: meta?.userAgent
|
|
4272
|
+
}).catch((error) => {
|
|
4273
|
+
log7.warn("Failed to record click event", error);
|
|
4274
|
+
});
|
|
4275
|
+
}
|
|
4276
|
+
async function getTrackingStats(notificationId) {
|
|
4277
|
+
const db = getDatabase("read");
|
|
4278
|
+
const rows = await db.select({
|
|
4279
|
+
type: trackingEvents.type,
|
|
4280
|
+
total: drizzleCount(),
|
|
4281
|
+
unique: countDistinct(trackingEvents.ipAddress)
|
|
4282
|
+
}).from(trackingEvents).where(eq2(trackingEvents.notificationId, notificationId)).groupBy(trackingEvents.type);
|
|
4283
|
+
const openRow = rows.find((r) => r.type === "open");
|
|
4284
|
+
const clickRow = rows.find((r) => r.type === "click");
|
|
4285
|
+
return {
|
|
4286
|
+
totalOpens: Number(openRow?.total ?? 0),
|
|
4287
|
+
uniqueOpens: Number(openRow?.unique ?? 0),
|
|
4288
|
+
totalClicks: Number(clickRow?.total ?? 0),
|
|
4289
|
+
uniqueClicks: Number(clickRow?.unique ?? 0)
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
async function getEngagementStats(options = {}) {
|
|
4293
|
+
const db = getDatabase("read");
|
|
4294
|
+
const sentConditions = [eq2(notifications.status, "sent")];
|
|
4295
|
+
if (options.channel) {
|
|
4296
|
+
sentConditions.push(eq2(notifications.channel, options.channel));
|
|
4297
|
+
}
|
|
4298
|
+
if (options.from) {
|
|
4299
|
+
sentConditions.push(gte2(notifications.createdAt, options.from));
|
|
4300
|
+
}
|
|
4301
|
+
if (options.to) {
|
|
4302
|
+
sentConditions.push(lte2(notifications.createdAt, options.to));
|
|
4303
|
+
}
|
|
4304
|
+
const [sentResult] = await db.select({ count: drizzleCount() }).from(notifications).where(and2(...sentConditions));
|
|
4305
|
+
const sent = Number(sentResult?.count ?? 0);
|
|
4306
|
+
if (sent === 0) {
|
|
4307
|
+
return { sent: 0, opened: 0, clicked: 0, openRate: 0, clickRate: 0 };
|
|
4308
|
+
}
|
|
4309
|
+
const eventConditions = [];
|
|
4310
|
+
if (options.from) {
|
|
4311
|
+
eventConditions.push(gte2(trackingEvents.createdAt, options.from));
|
|
4312
|
+
}
|
|
4313
|
+
if (options.to) {
|
|
4314
|
+
eventConditions.push(lte2(trackingEvents.createdAt, options.to));
|
|
4315
|
+
}
|
|
4316
|
+
const openConditions = [
|
|
4317
|
+
eq2(trackingEvents.type, "open"),
|
|
4318
|
+
...eventConditions
|
|
4319
|
+
];
|
|
4320
|
+
const clickConditions = [
|
|
4321
|
+
eq2(trackingEvents.type, "click"),
|
|
4322
|
+
...eventConditions
|
|
4323
|
+
];
|
|
4324
|
+
const [[openResult], [clickResult]] = await Promise.all([
|
|
4325
|
+
db.select({ count: countDistinct(trackingEvents.notificationId) }).from(trackingEvents).where(and2(...openConditions)),
|
|
4326
|
+
db.select({ count: countDistinct(trackingEvents.notificationId) }).from(trackingEvents).where(and2(...clickConditions))
|
|
4327
|
+
]);
|
|
4328
|
+
const opened = Number(openResult?.count ?? 0);
|
|
4329
|
+
const clicked = Number(clickResult?.count ?? 0);
|
|
4330
|
+
return {
|
|
4331
|
+
sent,
|
|
4332
|
+
opened,
|
|
4333
|
+
clicked,
|
|
4334
|
+
openRate: sent > 0 ? Number((opened / sent * 100).toFixed(2)) : 0,
|
|
4335
|
+
clickRate: sent > 0 ? Number((clicked / sent * 100).toFixed(2)) : 0
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
async function getClickDetails(notificationId) {
|
|
4339
|
+
const db = getDatabase("read");
|
|
4340
|
+
const rows = await db.select({
|
|
4341
|
+
linkUrl: trackingEvents.linkUrl,
|
|
4342
|
+
linkIndex: trackingEvents.linkIndex,
|
|
4343
|
+
totalClicks: drizzleCount(),
|
|
4344
|
+
uniqueClicks: countDistinct(trackingEvents.ipAddress)
|
|
4345
|
+
}).from(trackingEvents).where(
|
|
4346
|
+
and2(
|
|
4347
|
+
eq2(trackingEvents.notificationId, notificationId),
|
|
4348
|
+
eq2(trackingEvents.type, "click")
|
|
4349
|
+
)
|
|
4350
|
+
).groupBy(trackingEvents.linkUrl, trackingEvents.linkIndex).orderBy(trackingEvents.linkIndex);
|
|
4351
|
+
return rows.map((row) => ({
|
|
4352
|
+
linkUrl: row.linkUrl ?? "",
|
|
4353
|
+
linkIndex: row.linkIndex ?? 0,
|
|
4354
|
+
totalClicks: Number(row.totalClicks),
|
|
4355
|
+
uniqueClicks: Number(row.uniqueClicks)
|
|
4356
|
+
}));
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
// src/tracking/routes.ts
|
|
4360
|
+
import { logger as logger8 } from "@spfn/core/logger";
|
|
4361
|
+
var log8 = logger8.child("@spfn/notification:tracking:routes");
|
|
4362
|
+
var TRANSPARENT_GIF = Buffer.from(
|
|
4363
|
+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
4364
|
+
"base64"
|
|
4365
|
+
);
|
|
4366
|
+
var trackOpen = route.get("/_noti/t/o/:token").input({
|
|
4367
|
+
params: Type.Object({
|
|
4368
|
+
token: Type.String()
|
|
4369
|
+
})
|
|
4370
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
4371
|
+
const { params } = await c.data();
|
|
4372
|
+
const result = verifyOpenToken(params.token);
|
|
4373
|
+
if (result.valid && result.notificationId) {
|
|
4374
|
+
recordOpenEvent(result.notificationId, {
|
|
4375
|
+
ipAddress: c.raw.req.header("x-forwarded-for") ?? c.raw.req.header("x-real-ip"),
|
|
4376
|
+
userAgent: c.raw.req.header("user-agent")
|
|
4377
|
+
});
|
|
4378
|
+
} else {
|
|
4379
|
+
log8.warn("Invalid open tracking token");
|
|
4380
|
+
}
|
|
4381
|
+
return new Response(TRANSPARENT_GIF, {
|
|
4382
|
+
status: 200,
|
|
4383
|
+
headers: {
|
|
4384
|
+
"Content-Type": "image/gif",
|
|
4385
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
4386
|
+
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
});
|
|
4390
|
+
var trackClick = route.get("/_noti/t/c/:token").input({
|
|
4391
|
+
params: Type.Object({
|
|
4392
|
+
token: Type.String()
|
|
4393
|
+
}),
|
|
4394
|
+
query: Type.Object({
|
|
4395
|
+
url: Type.String()
|
|
4396
|
+
})
|
|
4397
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
4398
|
+
const { params, query } = await c.data();
|
|
4399
|
+
const targetUrl = query.url;
|
|
4400
|
+
const result = verifyClickToken(params.token);
|
|
4401
|
+
if (result.valid && result.notificationId != null && result.linkIndex != null) {
|
|
4402
|
+
recordClickEvent(result.notificationId, result.linkIndex, targetUrl, {
|
|
4403
|
+
ipAddress: c.raw.req.header("x-forwarded-for") ?? c.raw.req.header("x-real-ip"),
|
|
4404
|
+
userAgent: c.raw.req.header("user-agent")
|
|
4405
|
+
});
|
|
4406
|
+
} else {
|
|
4407
|
+
log8.warn("Invalid click tracking token");
|
|
4408
|
+
}
|
|
4409
|
+
return new Response(null, {
|
|
4410
|
+
status: 302,
|
|
4411
|
+
headers: {
|
|
4412
|
+
"Location": targetUrl,
|
|
4413
|
+
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
4414
|
+
}
|
|
4415
|
+
});
|
|
4416
|
+
});
|
|
4417
|
+
var trackingRouter = defineRouter({
|
|
4418
|
+
trackOpen,
|
|
4419
|
+
trackClick
|
|
4420
|
+
});
|
|
4421
|
+
|
|
4048
4422
|
// src/server.ts
|
|
4049
4423
|
registerBuiltinTemplates();
|
|
4050
4424
|
export {
|
|
4051
4425
|
NOTIFICATION_CHANNELS,
|
|
4052
4426
|
NOTIFICATION_STATUSES,
|
|
4427
|
+
TRACKING_EVENT_TYPES,
|
|
4053
4428
|
cancelNotification,
|
|
4054
4429
|
cancelNotificationsByReference,
|
|
4055
4430
|
cancelScheduledNotification,
|
|
@@ -4062,19 +4437,26 @@ export {
|
|
|
4062
4437
|
findNotifications,
|
|
4063
4438
|
findScheduledNotifications,
|
|
4064
4439
|
getAppName,
|
|
4440
|
+
getClickDetails,
|
|
4065
4441
|
getEmailFrom,
|
|
4066
4442
|
getEmailReplyTo,
|
|
4443
|
+
getEngagementStats,
|
|
4067
4444
|
getNotificationConfig,
|
|
4068
4445
|
getNotificationStats,
|
|
4069
4446
|
getSmsDefaultCountryCode,
|
|
4070
4447
|
getTemplate,
|
|
4071
4448
|
getTemplateNames,
|
|
4449
|
+
getTrackingBaseUrl,
|
|
4450
|
+
getTrackingSecret,
|
|
4451
|
+
getTrackingStats,
|
|
4072
4452
|
hasTemplate,
|
|
4453
|
+
isTrackingEnabled,
|
|
4073
4454
|
markNotificationFailed,
|
|
4074
4455
|
markNotificationPending,
|
|
4075
4456
|
markNotificationSent,
|
|
4076
4457
|
notificationJobRouter,
|
|
4077
4458
|
notifications,
|
|
4459
|
+
processTrackingHtml,
|
|
4078
4460
|
registerBuiltinTemplates,
|
|
4079
4461
|
registerEmailProvider,
|
|
4080
4462
|
registerFilter,
|
|
@@ -4092,6 +4474,8 @@ export {
|
|
|
4092
4474
|
sendScheduledSmsJob,
|
|
4093
4475
|
sendSlack,
|
|
4094
4476
|
sendSlackBulk,
|
|
4477
|
+
trackingEvents,
|
|
4478
|
+
trackingRouter,
|
|
4095
4479
|
updateNotificationJobId
|
|
4096
4480
|
};
|
|
4097
4481
|
//# sourceMappingURL=server.js.map
|