@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 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 (Slack, Push coming soon)
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,
@@ -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;