@spfn/notification 0.1.0-beta.4 → 0.1.0-beta.5
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 +33 -1
- package/dist/config/index.d.ts +14 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +52 -6
- package/dist/server.js.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Multi-channel notification system for SPFN applications.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Multi-channel support**: Email, SMS
|
|
7
|
+
- **Multi-channel support**: Email, SMS, Slack (Push coming soon)
|
|
8
8
|
- **Provider pattern**: Pluggable providers (AWS SES, AWS SNS, etc.)
|
|
9
9
|
- **Template system**: Variable substitution with filters
|
|
10
10
|
- **Scheduled delivery**: Schedule notifications for later via pg-boss
|
|
@@ -282,6 +282,32 @@ const twilioProvider: SMSProvider = {
|
|
|
282
282
|
registerSMSProvider(twilioProvider);
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
+
## Logging & Error Handling
|
|
286
|
+
|
|
287
|
+
All channels log via `@spfn/core/logger`. Logs are tagged by channel:
|
|
288
|
+
|
|
289
|
+
- `@spfn/notification:email` — Email send success/failure, validation errors
|
|
290
|
+
- `@spfn/notification:sms` — SMS send success/failure, validation errors
|
|
291
|
+
- `@spfn/notification:slack` — Slack send success/failure, validation errors
|
|
292
|
+
- `@spfn/notification:ses` — AWS SES client lifecycle, provider errors
|
|
293
|
+
- `@spfn/notification:sns` — AWS SNS client lifecycle, provider errors
|
|
294
|
+
|
|
295
|
+
`sendEmail`, `sendSMS`, `sendSlack` are designed to **not throw** — they return a `SendResult` object. Always check the result:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const result = await sendEmail({
|
|
299
|
+
to: 'user@example.com',
|
|
300
|
+
template: 'welcome',
|
|
301
|
+
data: { name: 'John' },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!result.success)
|
|
305
|
+
{
|
|
306
|
+
// result.error contains the failure reason
|
|
307
|
+
console.error('Email failed:', result.error);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
285
311
|
## Notification History
|
|
286
312
|
|
|
287
313
|
Enable history tracking to store all notifications in the database:
|
|
@@ -333,6 +359,7 @@ export type {
|
|
|
333
359
|
SendResult,
|
|
334
360
|
SendEmailParams,
|
|
335
361
|
SendSMSParams,
|
|
362
|
+
SendSlackParams,
|
|
336
363
|
TemplateDefinition,
|
|
337
364
|
TemplateData,
|
|
338
365
|
};
|
|
@@ -353,6 +380,11 @@ export {
|
|
|
353
380
|
sendSMSBulk,
|
|
354
381
|
registerSMSProvider,
|
|
355
382
|
|
|
383
|
+
// Slack
|
|
384
|
+
sendSlack,
|
|
385
|
+
sendSlackBulk,
|
|
386
|
+
registerSlackProvider,
|
|
387
|
+
|
|
356
388
|
// Scheduling
|
|
357
389
|
scheduleEmail,
|
|
358
390
|
scheduleSMS,
|
package/dist/config/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ declare const notificationEnvSchema: {
|
|
|
10
10
|
required: boolean;
|
|
11
11
|
examples: string[];
|
|
12
12
|
type: "string";
|
|
13
|
+
validator: (value: string) => string;
|
|
13
14
|
} & {
|
|
14
15
|
key: "SPFN_NOTIFICATION_EMAIL_PROVIDER";
|
|
15
16
|
};
|
|
@@ -18,6 +19,7 @@ declare const notificationEnvSchema: {
|
|
|
18
19
|
required: boolean;
|
|
19
20
|
examples: string[];
|
|
20
21
|
type: "string";
|
|
22
|
+
validator: (value: string) => string;
|
|
21
23
|
} & {
|
|
22
24
|
key: "SPFN_NOTIFICATION_EMAIL_FROM";
|
|
23
25
|
};
|
|
@@ -27,6 +29,7 @@ declare const notificationEnvSchema: {
|
|
|
27
29
|
required: boolean;
|
|
28
30
|
examples: string[];
|
|
29
31
|
type: "string";
|
|
32
|
+
validator: (value: string) => string;
|
|
30
33
|
} & {
|
|
31
34
|
key: "SPFN_NOTIFICATION_SMS_PROVIDER";
|
|
32
35
|
};
|
|
@@ -35,6 +38,7 @@ declare const notificationEnvSchema: {
|
|
|
35
38
|
required: boolean;
|
|
36
39
|
examples: string[];
|
|
37
40
|
type: "string";
|
|
41
|
+
validator: (value: string) => string;
|
|
38
42
|
} & {
|
|
39
43
|
key: "SPFN_NOTIFICATION_SLACK_WEBHOOK_URL";
|
|
40
44
|
};
|
|
@@ -44,6 +48,7 @@ declare const notificationEnvSchema: {
|
|
|
44
48
|
required: boolean;
|
|
45
49
|
examples: string[];
|
|
46
50
|
type: "string";
|
|
51
|
+
validator: (value: string) => string;
|
|
47
52
|
} & {
|
|
48
53
|
key: "AWS_REGION";
|
|
49
54
|
};
|
|
@@ -52,6 +57,7 @@ declare const notificationEnvSchema: {
|
|
|
52
57
|
required: boolean;
|
|
53
58
|
sensitive: boolean;
|
|
54
59
|
type: "string";
|
|
60
|
+
validator: (value: string) => string;
|
|
55
61
|
} & {
|
|
56
62
|
key: "AWS_ACCESS_KEY_ID";
|
|
57
63
|
};
|
|
@@ -60,6 +66,7 @@ declare const notificationEnvSchema: {
|
|
|
60
66
|
required: boolean;
|
|
61
67
|
sensitive: boolean;
|
|
62
68
|
type: "string";
|
|
69
|
+
validator: (value: string) => string;
|
|
63
70
|
} & {
|
|
64
71
|
key: "AWS_SECRET_ACCESS_KEY";
|
|
65
72
|
};
|
|
@@ -72,6 +79,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
72
79
|
required: boolean;
|
|
73
80
|
examples: string[];
|
|
74
81
|
type: "string";
|
|
82
|
+
validator: (value: string) => string;
|
|
75
83
|
} & {
|
|
76
84
|
key: "SPFN_NOTIFICATION_EMAIL_PROVIDER";
|
|
77
85
|
};
|
|
@@ -80,6 +88,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
80
88
|
required: boolean;
|
|
81
89
|
examples: string[];
|
|
82
90
|
type: "string";
|
|
91
|
+
validator: (value: string) => string;
|
|
83
92
|
} & {
|
|
84
93
|
key: "SPFN_NOTIFICATION_EMAIL_FROM";
|
|
85
94
|
};
|
|
@@ -89,6 +98,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
89
98
|
required: boolean;
|
|
90
99
|
examples: string[];
|
|
91
100
|
type: "string";
|
|
101
|
+
validator: (value: string) => string;
|
|
92
102
|
} & {
|
|
93
103
|
key: "SPFN_NOTIFICATION_SMS_PROVIDER";
|
|
94
104
|
};
|
|
@@ -97,6 +107,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
97
107
|
required: boolean;
|
|
98
108
|
examples: string[];
|
|
99
109
|
type: "string";
|
|
110
|
+
validator: (value: string) => string;
|
|
100
111
|
} & {
|
|
101
112
|
key: "SPFN_NOTIFICATION_SLACK_WEBHOOK_URL";
|
|
102
113
|
};
|
|
@@ -106,6 +117,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
106
117
|
required: boolean;
|
|
107
118
|
examples: string[];
|
|
108
119
|
type: "string";
|
|
120
|
+
validator: (value: string) => string;
|
|
109
121
|
} & {
|
|
110
122
|
key: "AWS_REGION";
|
|
111
123
|
};
|
|
@@ -114,6 +126,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
114
126
|
required: boolean;
|
|
115
127
|
sensitive: boolean;
|
|
116
128
|
type: "string";
|
|
129
|
+
validator: (value: string) => string;
|
|
117
130
|
} & {
|
|
118
131
|
key: "AWS_ACCESS_KEY_ID";
|
|
119
132
|
};
|
|
@@ -122,6 +135,7 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
122
135
|
required: boolean;
|
|
123
136
|
sensitive: boolean;
|
|
124
137
|
type: "string";
|
|
138
|
+
validator: (value: string) => string;
|
|
125
139
|
} & {
|
|
126
140
|
key: "AWS_SECRET_ACCESS_KEY";
|
|
127
141
|
};
|
package/dist/server.d.ts
CHANGED
|
@@ -627,13 +627,13 @@ declare function cancelNotificationsByReference(referenceType: string, reference
|
|
|
627
627
|
* Scheduled email sending job
|
|
628
628
|
*/
|
|
629
629
|
declare const sendScheduledEmailJob: _spfn_core_job.JobDef<{
|
|
630
|
+
from?: string | undefined;
|
|
630
631
|
subject?: string | undefined;
|
|
631
632
|
html?: string | undefined;
|
|
632
633
|
text?: string | undefined;
|
|
633
634
|
data?: {
|
|
634
635
|
[x: string]: unknown;
|
|
635
636
|
} | undefined;
|
|
636
|
-
from?: string | undefined;
|
|
637
637
|
template?: string | undefined;
|
|
638
638
|
replyTo?: string | undefined;
|
|
639
639
|
to: string | string[];
|
|
@@ -678,13 +678,13 @@ declare const sendScheduledSmsJob: _spfn_core_job.JobDef<{
|
|
|
678
678
|
*/
|
|
679
679
|
declare const notificationJobRouter: _spfn_core_job.JobRouter<{
|
|
680
680
|
sendScheduledEmail: _spfn_core_job.JobDef<{
|
|
681
|
+
from?: string | undefined;
|
|
681
682
|
subject?: string | undefined;
|
|
682
683
|
html?: string | undefined;
|
|
683
684
|
text?: string | undefined;
|
|
684
685
|
data?: {
|
|
685
686
|
[x: string]: unknown;
|
|
686
687
|
} | undefined;
|
|
687
|
-
from?: string | undefined;
|
|
688
688
|
template?: string | undefined;
|
|
689
689
|
replyTo?: string | undefined;
|
|
690
690
|
to: string | string[];
|
package/dist/server.js
CHANGED
|
@@ -95,6 +95,8 @@ function isHistoryEnabled() {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/channels/email/providers/aws-ses.ts
|
|
98
|
+
import { logger } from "@spfn/core/logger";
|
|
99
|
+
var log = logger.child("@spfn/notification:ses");
|
|
98
100
|
var sesClient = null;
|
|
99
101
|
async function getSESClient() {
|
|
100
102
|
if (sesClient) {
|
|
@@ -109,6 +111,7 @@ async function getSESClient() {
|
|
|
109
111
|
secretAccessKey: env.AWS_SECRET_ACCESS_KEY
|
|
110
112
|
} : void 0
|
|
111
113
|
});
|
|
114
|
+
log.debug("SES client created", { region: env.AWS_REGION });
|
|
112
115
|
return sesClient;
|
|
113
116
|
} catch {
|
|
114
117
|
throw new Error(
|
|
@@ -156,6 +159,7 @@ var awsSesProvider = {
|
|
|
156
159
|
};
|
|
157
160
|
} catch (error) {
|
|
158
161
|
const err = error;
|
|
162
|
+
log.error("SES send failed", err, { to: params.to, from: params.from });
|
|
159
163
|
return {
|
|
160
164
|
success: false,
|
|
161
165
|
error: err.message
|
|
@@ -630,6 +634,8 @@ async function findScheduledNotifications(options = {}) {
|
|
|
630
634
|
}
|
|
631
635
|
|
|
632
636
|
// src/channels/email/index.ts
|
|
637
|
+
import { logger as logger2 } from "@spfn/core/logger";
|
|
638
|
+
var log2 = logger2.child("@spfn/notification:email");
|
|
633
639
|
var providers = {
|
|
634
640
|
"aws-ses": awsSesProvider
|
|
635
641
|
};
|
|
@@ -651,6 +657,7 @@ async function sendEmail(params) {
|
|
|
651
657
|
let html = params.html;
|
|
652
658
|
if (params.template) {
|
|
653
659
|
if (!hasTemplate(params.template)) {
|
|
660
|
+
log2.warn(`Template not found: ${params.template}`);
|
|
654
661
|
return {
|
|
655
662
|
success: false,
|
|
656
663
|
error: `Template not found: ${params.template}`
|
|
@@ -664,12 +671,14 @@ async function sendEmail(params) {
|
|
|
664
671
|
}
|
|
665
672
|
}
|
|
666
673
|
if (!subject) {
|
|
674
|
+
log2.warn("Email subject is required", { to: recipients });
|
|
667
675
|
return {
|
|
668
676
|
success: false,
|
|
669
677
|
error: "Email subject is required"
|
|
670
678
|
};
|
|
671
679
|
}
|
|
672
680
|
if (!text2 && !html) {
|
|
681
|
+
log2.warn("Email content (text or html) is required", { to: recipients, subject });
|
|
673
682
|
return {
|
|
674
683
|
success: false,
|
|
675
684
|
error: "Email content (text or html) is required"
|
|
@@ -697,10 +706,16 @@ async function sendEmail(params) {
|
|
|
697
706
|
providerName: provider.name
|
|
698
707
|
});
|
|
699
708
|
historyId = record.id;
|
|
700
|
-
} catch {
|
|
709
|
+
} catch (error) {
|
|
710
|
+
log2.warn("Failed to create notification history record", error);
|
|
701
711
|
}
|
|
702
712
|
}
|
|
703
713
|
const result = await provider.send(internalParams);
|
|
714
|
+
if (result.success) {
|
|
715
|
+
log2.info("Email sent", { to: recipients, subject, messageId: result.messageId });
|
|
716
|
+
} else {
|
|
717
|
+
log2.error("Email send failed", { to: recipients, subject, error: result.error });
|
|
718
|
+
}
|
|
704
719
|
if (historyId && isHistoryEnabled()) {
|
|
705
720
|
try {
|
|
706
721
|
if (result.success) {
|
|
@@ -708,7 +723,8 @@ async function sendEmail(params) {
|
|
|
708
723
|
} else {
|
|
709
724
|
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
710
725
|
}
|
|
711
|
-
} catch {
|
|
726
|
+
} catch (error) {
|
|
727
|
+
log2.warn("Failed to update notification history record", error);
|
|
712
728
|
}
|
|
713
729
|
}
|
|
714
730
|
return result;
|
|
@@ -730,6 +746,8 @@ async function sendEmailBulk(items) {
|
|
|
730
746
|
}
|
|
731
747
|
|
|
732
748
|
// src/channels/sms/providers/aws-sns.ts
|
|
749
|
+
import { logger as logger3 } from "@spfn/core/logger";
|
|
750
|
+
var log3 = logger3.child("@spfn/notification:sns");
|
|
733
751
|
var snsClient = null;
|
|
734
752
|
async function getSNSClient() {
|
|
735
753
|
if (snsClient) {
|
|
@@ -744,6 +762,7 @@ async function getSNSClient() {
|
|
|
744
762
|
secretAccessKey: env.AWS_SECRET_ACCESS_KEY
|
|
745
763
|
} : void 0
|
|
746
764
|
});
|
|
765
|
+
log3.debug("SNS client created", { region: env.AWS_REGION });
|
|
747
766
|
return snsClient;
|
|
748
767
|
} catch {
|
|
749
768
|
throw new Error(
|
|
@@ -774,6 +793,7 @@ var awsSnsProvider = {
|
|
|
774
793
|
};
|
|
775
794
|
} catch (error) {
|
|
776
795
|
const err = error;
|
|
796
|
+
log3.error("SNS send failed", err, { to: params.to });
|
|
777
797
|
return {
|
|
778
798
|
success: false,
|
|
779
799
|
error: err.message
|
|
@@ -796,6 +816,8 @@ function normalizePhoneNumber(phone, defaultCountryCode) {
|
|
|
796
816
|
}
|
|
797
817
|
|
|
798
818
|
// src/channels/sms/index.ts
|
|
819
|
+
import { logger as logger4 } from "@spfn/core/logger";
|
|
820
|
+
var log4 = logger4.child("@spfn/notification:sms");
|
|
799
821
|
var providers2 = {
|
|
800
822
|
"aws-sns": awsSnsProvider
|
|
801
823
|
};
|
|
@@ -815,6 +837,7 @@ async function sendSMS(params) {
|
|
|
815
837
|
let message = params.message;
|
|
816
838
|
if (params.template) {
|
|
817
839
|
if (!hasTemplate(params.template)) {
|
|
840
|
+
log4.warn(`Template not found: ${params.template}`);
|
|
818
841
|
return {
|
|
819
842
|
success: false,
|
|
820
843
|
error: `Template not found: ${params.template}`
|
|
@@ -826,6 +849,7 @@ async function sendSMS(params) {
|
|
|
826
849
|
}
|
|
827
850
|
}
|
|
828
851
|
if (!message) {
|
|
852
|
+
log4.warn("SMS message is required", { to: recipients });
|
|
829
853
|
return {
|
|
830
854
|
success: false,
|
|
831
855
|
error: "SMS message is required"
|
|
@@ -851,10 +875,16 @@ async function sendSMS(params) {
|
|
|
851
875
|
providerName: provider.name
|
|
852
876
|
});
|
|
853
877
|
historyId = record.id;
|
|
854
|
-
} catch {
|
|
878
|
+
} catch (error) {
|
|
879
|
+
log4.warn("Failed to create notification history record", error);
|
|
855
880
|
}
|
|
856
881
|
}
|
|
857
882
|
const result = await provider.send(internalParams);
|
|
883
|
+
if (result.success) {
|
|
884
|
+
log4.info("SMS sent", { to: normalizedPhone, messageId: result.messageId });
|
|
885
|
+
} else {
|
|
886
|
+
log4.error("SMS send failed", { to: normalizedPhone, error: result.error });
|
|
887
|
+
}
|
|
858
888
|
if (historyId && isHistoryEnabled()) {
|
|
859
889
|
try {
|
|
860
890
|
if (result.success) {
|
|
@@ -862,7 +892,8 @@ async function sendSMS(params) {
|
|
|
862
892
|
} else {
|
|
863
893
|
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
864
894
|
}
|
|
865
|
-
} catch {
|
|
895
|
+
} catch (error) {
|
|
896
|
+
log4.warn("Failed to update notification history record", error);
|
|
866
897
|
}
|
|
867
898
|
}
|
|
868
899
|
results.push(result);
|
|
@@ -893,6 +924,8 @@ async function sendSMSBulk(items) {
|
|
|
893
924
|
}
|
|
894
925
|
|
|
895
926
|
// src/channels/slack/providers/webhook.ts
|
|
927
|
+
import { logger as logger5 } from "@spfn/core/logger";
|
|
928
|
+
var log5 = logger5.child("@spfn/notification:slack-webhook");
|
|
896
929
|
var webhookProvider = {
|
|
897
930
|
name: "webhook",
|
|
898
931
|
async send(params) {
|
|
@@ -911,6 +944,7 @@ var webhookProvider = {
|
|
|
911
944
|
};
|
|
912
945
|
} catch (error) {
|
|
913
946
|
const err = error;
|
|
947
|
+
log5.error("Webhook request failed", err);
|
|
914
948
|
return {
|
|
915
949
|
success: false,
|
|
916
950
|
error: err.message
|
|
@@ -920,6 +954,8 @@ var webhookProvider = {
|
|
|
920
954
|
};
|
|
921
955
|
|
|
922
956
|
// src/channels/slack/index.ts
|
|
957
|
+
import { logger as logger6 } from "@spfn/core/logger";
|
|
958
|
+
var log6 = logger6.child("@spfn/notification:slack");
|
|
923
959
|
var providers3 = {
|
|
924
960
|
"webhook": webhookProvider
|
|
925
961
|
};
|
|
@@ -935,6 +971,7 @@ function resolveWebhookUrl(params) {
|
|
|
935
971
|
async function sendSlack(params) {
|
|
936
972
|
const webhookUrl = resolveWebhookUrl(params);
|
|
937
973
|
if (!webhookUrl) {
|
|
974
|
+
log6.warn("Slack webhook URL is required");
|
|
938
975
|
return {
|
|
939
976
|
success: false,
|
|
940
977
|
error: "Slack webhook URL is required. Set SPFN_NOTIFICATION_SLACK_WEBHOOK_URL or pass webhookUrl."
|
|
@@ -944,6 +981,7 @@ async function sendSlack(params) {
|
|
|
944
981
|
let blocks = params.blocks;
|
|
945
982
|
if (params.template) {
|
|
946
983
|
if (!hasTemplate(params.template)) {
|
|
984
|
+
log6.warn(`Template not found: ${params.template}`);
|
|
947
985
|
return {
|
|
948
986
|
success: false,
|
|
949
987
|
error: `Template not found: ${params.template}`
|
|
@@ -956,6 +994,7 @@ async function sendSlack(params) {
|
|
|
956
994
|
}
|
|
957
995
|
}
|
|
958
996
|
if (!text2 && !blocks) {
|
|
997
|
+
log6.warn("Slack message requires text or blocks");
|
|
959
998
|
return {
|
|
960
999
|
success: false,
|
|
961
1000
|
error: "Slack message requires text or blocks"
|
|
@@ -979,10 +1018,16 @@ async function sendSlack(params) {
|
|
|
979
1018
|
providerName: provider.name
|
|
980
1019
|
});
|
|
981
1020
|
historyId = record.id;
|
|
982
|
-
} catch {
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
log6.warn("Failed to create notification history record", error);
|
|
983
1023
|
}
|
|
984
1024
|
}
|
|
985
1025
|
const result = await provider.send(internalParams);
|
|
1026
|
+
if (result.success) {
|
|
1027
|
+
log6.info("Slack message sent");
|
|
1028
|
+
} else {
|
|
1029
|
+
log6.error("Slack send failed", { error: result.error });
|
|
1030
|
+
}
|
|
986
1031
|
if (historyId && isHistoryEnabled()) {
|
|
987
1032
|
try {
|
|
988
1033
|
if (result.success) {
|
|
@@ -990,7 +1035,8 @@ async function sendSlack(params) {
|
|
|
990
1035
|
} else {
|
|
991
1036
|
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
992
1037
|
}
|
|
993
|
-
} catch {
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
log6.warn("Failed to update notification history record", error);
|
|
994
1040
|
}
|
|
995
1041
|
}
|
|
996
1042
|
return result;
|