@spfn/notification 0.1.0-beta.1 → 0.1.0-beta.10

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/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);
@@ -892,6 +923,140 @@ async function sendSMSBulk(items) {
892
923
  return { results, successCount, failureCount };
893
924
  }
894
925
 
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");
929
+ var webhookProvider = {
930
+ name: "webhook",
931
+ async send(params) {
932
+ try {
933
+ const res = await fetch(params.webhookUrl, {
934
+ method: "POST",
935
+ headers: { "Content-Type": "application/json" },
936
+ body: JSON.stringify({
937
+ text: params.text,
938
+ blocks: params.blocks
939
+ })
940
+ });
941
+ return {
942
+ success: res.ok,
943
+ error: res.ok ? void 0 : await res.text()
944
+ };
945
+ } catch (error) {
946
+ const err = error;
947
+ log5.error("Webhook request failed", err);
948
+ return {
949
+ success: false,
950
+ error: err.message
951
+ };
952
+ }
953
+ }
954
+ };
955
+
956
+ // src/channels/slack/index.ts
957
+ import { logger as logger6 } from "@spfn/core/logger";
958
+ var log6 = logger6.child("@spfn/notification:slack");
959
+ var providers3 = {
960
+ "webhook": webhookProvider
961
+ };
962
+ function registerSlackProvider(provider) {
963
+ providers3[provider.name] = provider;
964
+ }
965
+ function getProvider3() {
966
+ return providers3["webhook"];
967
+ }
968
+ function resolveWebhookUrl(params) {
969
+ return params.webhookUrl || env.SPFN_NOTIFICATION_SLACK_WEBHOOK_URL;
970
+ }
971
+ async function sendSlack(params) {
972
+ const webhookUrl = resolveWebhookUrl(params);
973
+ if (!webhookUrl) {
974
+ log6.warn("Slack webhook URL is required");
975
+ return {
976
+ success: false,
977
+ error: "Slack webhook URL is required. Set SPFN_NOTIFICATION_SLACK_WEBHOOK_URL or pass webhookUrl."
978
+ };
979
+ }
980
+ let text2 = params.text;
981
+ let blocks = params.blocks;
982
+ if (params.template) {
983
+ if (!hasTemplate(params.template)) {
984
+ log6.warn(`Template not found: ${params.template}`);
985
+ return {
986
+ success: false,
987
+ error: `Template not found: ${params.template}`
988
+ };
989
+ }
990
+ const rendered = renderTemplate(params.template, params.data || {}, "slack");
991
+ if (rendered.slack) {
992
+ text2 = rendered.slack.text;
993
+ blocks = rendered.slack.blocks;
994
+ }
995
+ }
996
+ if (!text2 && !blocks) {
997
+ log6.warn("Slack message requires text or blocks");
998
+ return {
999
+ success: false,
1000
+ error: "Slack message requires text or blocks"
1001
+ };
1002
+ }
1003
+ const internalParams = {
1004
+ webhookUrl,
1005
+ text: text2,
1006
+ blocks
1007
+ };
1008
+ const provider = getProvider3();
1009
+ let historyId;
1010
+ if (isHistoryEnabled()) {
1011
+ try {
1012
+ const record = await createNotificationRecord({
1013
+ channel: "slack",
1014
+ recipient: webhookUrl,
1015
+ templateName: params.template,
1016
+ templateData: params.data,
1017
+ content: text2,
1018
+ providerName: provider.name
1019
+ });
1020
+ historyId = record.id;
1021
+ } catch (error) {
1022
+ log6.warn("Failed to create notification history record", error);
1023
+ }
1024
+ }
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
+ }
1031
+ if (historyId && isHistoryEnabled()) {
1032
+ try {
1033
+ if (result.success) {
1034
+ await markNotificationSent(historyId, result.messageId);
1035
+ } else {
1036
+ await markNotificationFailed(historyId, result.error || "Unknown error");
1037
+ }
1038
+ } catch (error) {
1039
+ log6.warn("Failed to update notification history record", error);
1040
+ }
1041
+ }
1042
+ return result;
1043
+ }
1044
+ async function sendSlackBulk(items) {
1045
+ const results = [];
1046
+ let successCount = 0;
1047
+ let failureCount = 0;
1048
+ for (const item of items) {
1049
+ const result = await sendSlack(item);
1050
+ results.push(result);
1051
+ if (result.success) {
1052
+ successCount++;
1053
+ } else {
1054
+ failureCount++;
1055
+ }
1056
+ }
1057
+ return { results, successCount, failureCount };
1058
+ }
1059
+
895
1060
  // src/jobs/send-scheduled-email.ts
896
1061
  import { job } from "@spfn/core/job";
897
1062
 
@@ -3759,6 +3924,127 @@ var notificationJobRouter = defineJobRouter({
3759
3924
  sendScheduledSms: sendScheduledSmsJob
3760
3925
  });
3761
3926
 
3927
+ // src/integrations/error-slack.ts
3928
+ import { hostname } from "os";
3929
+ function formatHeaders(headers) {
3930
+ const entries = Object.entries(headers);
3931
+ if (entries.length === 0) {
3932
+ return "(none)";
3933
+ }
3934
+ return entries.map(([k, v]) => `${k}: ${v}`).join("\n");
3935
+ }
3936
+ function formatQuery(query) {
3937
+ const entries = Object.entries(query);
3938
+ if (entries.length === 0) {
3939
+ return "(none)";
3940
+ }
3941
+ return entries.map(([k, v]) => `${k}=${v}`).join("\n");
3942
+ }
3943
+ function shortStack(err, maxLines = 3) {
3944
+ if (!err.stack) {
3945
+ return "(no stack)";
3946
+ }
3947
+ const lines = err.stack.split("\n").slice(1);
3948
+ return lines.slice(0, maxLines).map((line) => line.trim()).join("\n");
3949
+ }
3950
+ var throttleMap = /* @__PURE__ */ new Map();
3951
+ function throttleKey(err, ctx) {
3952
+ return `${err.name}:${ctx.statusCode}:${ctx.path}`;
3953
+ }
3954
+ function getEnvLabel() {
3955
+ const env2 = process.env.NODE_ENV || "unknown";
3956
+ const host = hostname();
3957
+ const dbUrl = process.env.DATABASE_URL || "";
3958
+ const dbName = dbUrl.match(/\/([^/?]+)(\?|$)/)?.[1] || "(unknown)";
3959
+ return `${env2} | ${host} | db:${dbName}`;
3960
+ }
3961
+ function defaultFormat(err, ctx, suppressed = 0) {
3962
+ const envLabel = getEnvLabel();
3963
+ const emoji = ctx.statusCode >= 500 ? ":rotating_light:" : ":warning:";
3964
+ const title = `${emoji} *${err.name || "Error"}* \u2014 ${ctx.statusCode} [${envLabel}]`;
3965
+ const fields = [
3966
+ { type: "mrkdwn", text: `*Method*
3967
+ ${ctx.method}` },
3968
+ { type: "mrkdwn", text: `*Path*
3969
+ ${ctx.path}` },
3970
+ { type: "mrkdwn", text: `*User*
3971
+ ${ctx.userId ?? "(anonymous)"}` },
3972
+ { type: "mrkdwn", text: `*Request ID*
3973
+ ${ctx.requestId ?? "(none)"}` }
3974
+ ];
3975
+ const blocks = [
3976
+ // Title
3977
+ {
3978
+ type: "header",
3979
+ text: { type: "plain_text", text: `${err.name || "Error"} \u2014 ${ctx.statusCode}`, emoji: true }
3980
+ },
3981
+ // Error message
3982
+ {
3983
+ type: "section",
3984
+ text: { type: "mrkdwn", text: `> ${err.message}` }
3985
+ },
3986
+ // Fields: method, path, user, requestId
3987
+ {
3988
+ type: "section",
3989
+ fields
3990
+ },
3991
+ // Timestamp + suppressed count
3992
+ {
3993
+ type: "context",
3994
+ elements: [
3995
+ { type: "mrkdwn", text: `*Time:* ${ctx.timestamp}` },
3996
+ ...suppressed > 0 ? [{ type: "mrkdwn", text: `_+${suppressed} suppressed since last notification_` }] : []
3997
+ ]
3998
+ },
3999
+ { type: "divider" },
4000
+ // Headers
4001
+ {
4002
+ type: "section",
4003
+ text: { type: "mrkdwn", text: `*Request Headers*
4004
+ \`\`\`${formatHeaders(ctx.request.headers)}\`\`\`` }
4005
+ },
4006
+ // Query
4007
+ {
4008
+ type: "section",
4009
+ text: { type: "mrkdwn", text: `*Query Params*
4010
+ \`\`\`${formatQuery(ctx.request.query)}\`\`\`` }
4011
+ },
4012
+ { type: "divider" },
4013
+ // Stack trace
4014
+ {
4015
+ type: "section",
4016
+ text: { type: "mrkdwn", text: `*Stack Trace*
4017
+ \`\`\`${shortStack(err)}\`\`\`` }
4018
+ }
4019
+ ];
4020
+ return { text: title, blocks };
4021
+ }
4022
+ function createErrorSlackNotifier(options = {}) {
4023
+ console.warn(
4024
+ "[@spfn/notification] createErrorSlackNotifier() is deprecated. Use createMonitorErrorHandler() from @spfn/monitor/server instead."
4025
+ );
4026
+ const { minStatusCode = 500, throttleMs = 6e4 } = options;
4027
+ return async (err, ctx) => {
4028
+ if (ctx.statusCode < minStatusCode) {
4029
+ return;
4030
+ }
4031
+ const key = throttleKey(err, ctx);
4032
+ const now = Date.now();
4033
+ const entry = throttleMap.get(key);
4034
+ if (entry && now - entry.lastSent < throttleMs) {
4035
+ entry.suppressed++;
4036
+ return;
4037
+ }
4038
+ const suppressed = entry?.suppressed ?? 0;
4039
+ throttleMap.set(key, { lastSent: now, suppressed: 0 });
4040
+ const message = options.formatMessage?.(err, ctx) ?? defaultFormat(err, ctx, suppressed);
4041
+ await sendSlack({
4042
+ ...message,
4043
+ webhookUrl: options.webhookUrl
4044
+ });
4045
+ };
4046
+ }
4047
+
3762
4048
  // src/server.ts
3763
4049
  registerBuiltinTemplates();
3764
4050
  export {
@@ -3769,6 +4055,7 @@ export {
3769
4055
  cancelScheduledNotification,
3770
4056
  configureNotification,
3771
4057
  countNotifications,
4058
+ createErrorSlackNotifier,
3772
4059
  createNotificationRecord,
3773
4060
  createScheduledNotification,
3774
4061
  findNotificationByJobId,
@@ -3792,6 +4079,7 @@ export {
3792
4079
  registerEmailProvider,
3793
4080
  registerFilter,
3794
4081
  registerSMSProvider,
4082
+ registerSlackProvider,
3795
4083
  registerTemplate,
3796
4084
  renderTemplate,
3797
4085
  scheduleEmail,
@@ -3802,6 +4090,8 @@ export {
3802
4090
  sendSMSBulk,
3803
4091
  sendScheduledEmailJob,
3804
4092
  sendScheduledSmsJob,
4093
+ sendSlack,
4094
+ sendSlackBulk,
3805
4095
  updateNotificationJobId
3806
4096
  };
3807
4097
  //# sourceMappingURL=server.js.map