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

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.
@@ -119,6 +119,50 @@ interface InternalSendSMSParams {
119
119
  message: string;
120
120
  }
121
121
 
122
+ /**
123
+ * @spfn/notification - Slack Channel Types
124
+ */
125
+
126
+ /**
127
+ * Parameters for sending Slack message
128
+ */
129
+ interface SendSlackParams {
130
+ /**
131
+ * Webhook URL (overrides env/config default)
132
+ */
133
+ webhookUrl?: string;
134
+ /**
135
+ * Plain text message
136
+ */
137
+ text?: string;
138
+ /**
139
+ * Slack Block Kit blocks
140
+ */
141
+ blocks?: unknown[];
142
+ /**
143
+ * Template name
144
+ */
145
+ template?: string;
146
+ /**
147
+ * Template data for variable substitution
148
+ */
149
+ data?: Record<string, unknown>;
150
+ }
151
+ /**
152
+ * Slack provider interface
153
+ */
154
+ interface SlackProvider extends ChannelProvider<InternalSendSlackParams> {
155
+ name: 'webhook' | string;
156
+ }
157
+ /**
158
+ * Internal send Slack params (after template rendering)
159
+ */
160
+ interface InternalSendSlackParams {
161
+ webhookUrl: string;
162
+ text?: string;
163
+ blocks?: unknown[];
164
+ }
165
+
122
166
  /**
123
167
  * @spfn/notification - Template Types
124
168
  */
@@ -167,4 +211,4 @@ interface RenderedTemplate {
167
211
  */
168
212
  type TemplateData = Record<string, unknown>;
169
213
 
170
- export type { EmailProvider as E, NotificationChannel as N, RenderedTemplate as R, SendEmailParams as S, TemplateDefinition as T, SendResult as a, SendSMSParams as b, SMSProvider as c, TemplateData as d, EmailTemplateContent as e, SmsTemplateContent as f, SlackTemplateContent as g };
214
+ export type { EmailProvider as E, NotificationChannel as N, RenderedTemplate as R, SendEmailParams as S, TemplateDefinition as T, SendResult as a, SendSMSParams as b, SMSProvider as c, SendSlackParams as d, SlackProvider as e, TemplateData as f, EmailTemplateContent as g, SmsTemplateContent as h, SlackTemplateContent as i };
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { e as EmailTemplateContent, N as NotificationChannel, S as SendEmailParams, a as SendResult, b as SendSMSParams, g as SlackTemplateContent, f as SmsTemplateContent, d as TemplateData, T as TemplateDefinition } from './index-D_XTwHmO.js';
1
+ export { g as EmailTemplateContent, N as NotificationChannel, S as SendEmailParams, a as SendResult, b as SendSMSParams, d as SendSlackParams, i as SlackTemplateContent, h as SmsTemplateContent, f as TemplateData, T as TemplateDefinition } from './index-BAe1ZBYQ.js';
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { NotificationConfig, configureNotification, getAppName, getEmailFrom, getEmailReplyTo, getNotificationConfig, getSmsDefaultCountryCode } from './config/index.js';
2
- import { S as SendEmailParams, a as SendResult, E as EmailProvider, b as SendSMSParams, c as SMSProvider, T as TemplateDefinition, d as TemplateData, N as NotificationChannel$1, R as RenderedTemplate } from './index-D_XTwHmO.js';
3
- export { e as EmailTemplateContent, g as SlackTemplateContent, f as SmsTemplateContent } from './index-D_XTwHmO.js';
2
+ import { S as SendEmailParams, a as SendResult, E as EmailProvider, b as SendSMSParams, c as SMSProvider, d as SendSlackParams, e as SlackProvider, T as TemplateDefinition, f as TemplateData, N as NotificationChannel$1, R as RenderedTemplate } from './index-BAe1ZBYQ.js';
3
+ export { g as EmailTemplateContent, i as SlackTemplateContent, h as SmsTemplateContent } from './index-BAe1ZBYQ.js';
4
4
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
5
5
  import * as _spfn_core_job from '@spfn/core/job';
6
6
  import '@spfn/core/env';
@@ -47,6 +47,27 @@ declare function sendSMSBulk(items: SendSMSParams[]): Promise<{
47
47
  failureCount: number;
48
48
  }>;
49
49
 
50
+ /**
51
+ * @spfn/notification - Slack Channel
52
+ */
53
+
54
+ /**
55
+ * Register custom Slack provider
56
+ */
57
+ declare function registerSlackProvider(provider: SlackProvider): void;
58
+ /**
59
+ * Send Slack message
60
+ */
61
+ declare function sendSlack(params: SendSlackParams): Promise<SendResult>;
62
+ /**
63
+ * Send bulk Slack messages
64
+ */
65
+ declare function sendSlackBulk(items: SendSlackParams[]): Promise<{
66
+ results: SendResult[];
67
+ successCount: number;
68
+ failureCount: number;
69
+ }>;
70
+
50
71
  /**
51
72
  * @spfn/notification - Schedule Service
52
73
  *
@@ -680,4 +701,58 @@ declare const notificationJobRouter: _spfn_core_job.JobRouter<{
680
701
  }, void>;
681
702
  }>;
682
703
 
683
- export { type CancelResult, EmailProvider, type FindNotificationsOptions, NOTIFICATION_CHANNELS, NOTIFICATION_STATUSES, type NewNotification, type Notification, NotificationChannel$1 as NotificationChannel, type NotificationStats, type NotificationStatus, SMSProvider, type ScheduleOptions, type ScheduleResult, SendEmailParams, SendResult, SendSMSParams, TemplateData, TemplateDefinition, cancelNotification, cancelNotificationsByReference, cancelScheduledNotification, countNotifications, createNotificationRecord, createScheduledNotification, findNotificationByJobId, findNotifications, findScheduledNotifications, getNotificationStats, getTemplate, getTemplateNames, hasTemplate, markNotificationFailed, markNotificationPending, markNotificationSent, notificationJobRouter, notifications, registerBuiltinTemplates, registerEmailProvider, registerFilter, registerSMSProvider, registerTemplate, renderTemplate, scheduleEmail, scheduleSMS, sendEmail, sendEmailBulk, sendSMS, sendSMSBulk, sendScheduledEmailJob, sendScheduledSmsJob, updateNotificationJobId };
704
+ /**
705
+ * @spfn/notification - Error → Slack Integration
706
+ *
707
+ * Factory for creating an onError callback that sends error notifications to Slack.
708
+ *
709
+ * @example
710
+ * ```typescript
711
+ * import { createServer } from '@spfn/core/server';
712
+ * import { createErrorSlackNotifier } from '@spfn/notification/server';
713
+ *
714
+ * await createServer({
715
+ * middleware: {
716
+ * onError: createErrorSlackNotifier({ minStatusCode: 500 }),
717
+ * },
718
+ * });
719
+ * ```
720
+ */
721
+ interface ErrorContext {
722
+ statusCode: number;
723
+ path: string;
724
+ method: string;
725
+ requestId?: string;
726
+ timestamp: string;
727
+ userId?: string;
728
+ request: {
729
+ headers: Record<string, string>;
730
+ query: Record<string, string>;
731
+ };
732
+ }
733
+ interface ErrorSlackOptions {
734
+ /**
735
+ * Minimum status code to trigger notification
736
+ * @default 500
737
+ */
738
+ minStatusCode?: number;
739
+ /**
740
+ * Webhook URL override (defaults to env/config)
741
+ */
742
+ webhookUrl?: string;
743
+ /**
744
+ * Custom message formatter
745
+ */
746
+ formatMessage?: (err: Error, ctx: ErrorContext) => {
747
+ text?: string;
748
+ blocks?: unknown[];
749
+ };
750
+ }
751
+ /**
752
+ * Create an onError callback that sends Slack notifications
753
+ *
754
+ * Returns a function matching ErrorHandler's onError signature.
755
+ */
756
+ declare function createErrorSlackNotifier(options?: ErrorSlackOptions): (err: Error, ctx: ErrorContext) => Promise<void>;
757
+
758
+ export { type CancelResult, EmailProvider, type ErrorSlackOptions, type FindNotificationsOptions, NOTIFICATION_CHANNELS, NOTIFICATION_STATUSES, type NewNotification, type Notification, NotificationChannel$1 as NotificationChannel, type NotificationStats, type NotificationStatus, SMSProvider, type ScheduleOptions, type ScheduleResult, SendEmailParams, SendResult, SendSMSParams, SendSlackParams, SlackProvider, TemplateData, TemplateDefinition, cancelNotification, cancelNotificationsByReference, cancelScheduledNotification, countNotifications, createErrorSlackNotifier, createNotificationRecord, createScheduledNotification, findNotificationByJobId, findNotifications, findScheduledNotifications, getNotificationStats, getTemplate, getTemplateNames, hasTemplate, markNotificationFailed, markNotificationPending, markNotificationSent, notificationJobRouter, notifications, registerBuiltinTemplates, registerEmailProvider, registerFilter, registerSMSProvider, registerSlackProvider, registerTemplate, renderTemplate, scheduleEmail, scheduleSMS, sendEmail, sendEmailBulk, sendSMS, sendSMSBulk, sendScheduledEmailJob, sendScheduledSmsJob, sendSlack, sendSlackBulk, updateNotificationJobId };
package/dist/server.js CHANGED
@@ -892,6 +892,125 @@ async function sendSMSBulk(items) {
892
892
  return { results, successCount, failureCount };
893
893
  }
894
894
 
895
+ // src/channels/slack/providers/webhook.ts
896
+ var webhookProvider = {
897
+ name: "webhook",
898
+ async send(params) {
899
+ try {
900
+ const res = await fetch(params.webhookUrl, {
901
+ method: "POST",
902
+ headers: { "Content-Type": "application/json" },
903
+ body: JSON.stringify({
904
+ text: params.text,
905
+ blocks: params.blocks
906
+ })
907
+ });
908
+ return {
909
+ success: res.ok,
910
+ error: res.ok ? void 0 : await res.text()
911
+ };
912
+ } catch (error) {
913
+ const err = error;
914
+ return {
915
+ success: false,
916
+ error: err.message
917
+ };
918
+ }
919
+ }
920
+ };
921
+
922
+ // src/channels/slack/index.ts
923
+ var providers3 = {
924
+ "webhook": webhookProvider
925
+ };
926
+ function registerSlackProvider(provider) {
927
+ providers3[provider.name] = provider;
928
+ }
929
+ function getProvider3() {
930
+ return providers3["webhook"];
931
+ }
932
+ function resolveWebhookUrl(params) {
933
+ return params.webhookUrl || env.SPFN_NOTIFICATION_SLACK_WEBHOOK_URL;
934
+ }
935
+ async function sendSlack(params) {
936
+ const webhookUrl = resolveWebhookUrl(params);
937
+ if (!webhookUrl) {
938
+ return {
939
+ success: false,
940
+ error: "Slack webhook URL is required. Set SPFN_NOTIFICATION_SLACK_WEBHOOK_URL or pass webhookUrl."
941
+ };
942
+ }
943
+ let text2 = params.text;
944
+ let blocks = params.blocks;
945
+ if (params.template) {
946
+ if (!hasTemplate(params.template)) {
947
+ return {
948
+ success: false,
949
+ error: `Template not found: ${params.template}`
950
+ };
951
+ }
952
+ const rendered = renderTemplate(params.template, params.data || {}, "slack");
953
+ if (rendered.slack) {
954
+ text2 = rendered.slack.text;
955
+ blocks = rendered.slack.blocks;
956
+ }
957
+ }
958
+ if (!text2 && !blocks) {
959
+ return {
960
+ success: false,
961
+ error: "Slack message requires text or blocks"
962
+ };
963
+ }
964
+ const internalParams = {
965
+ webhookUrl,
966
+ text: text2,
967
+ blocks
968
+ };
969
+ const provider = getProvider3();
970
+ let historyId;
971
+ if (isHistoryEnabled()) {
972
+ try {
973
+ const record = await createNotificationRecord({
974
+ channel: "slack",
975
+ recipient: webhookUrl,
976
+ templateName: params.template,
977
+ templateData: params.data,
978
+ content: text2,
979
+ providerName: provider.name
980
+ });
981
+ historyId = record.id;
982
+ } catch {
983
+ }
984
+ }
985
+ const result = await provider.send(internalParams);
986
+ if (historyId && isHistoryEnabled()) {
987
+ try {
988
+ if (result.success) {
989
+ await markNotificationSent(historyId, result.messageId);
990
+ } else {
991
+ await markNotificationFailed(historyId, result.error || "Unknown error");
992
+ }
993
+ } catch {
994
+ }
995
+ }
996
+ return result;
997
+ }
998
+ async function sendSlackBulk(items) {
999
+ const results = [];
1000
+ let successCount = 0;
1001
+ let failureCount = 0;
1002
+ for (const item of items) {
1003
+ const result = await sendSlack(item);
1004
+ results.push(result);
1005
+ if (result.success) {
1006
+ successCount++;
1007
+ } else {
1008
+ failureCount++;
1009
+ }
1010
+ }
1011
+ return { results, successCount, failureCount };
1012
+ }
1013
+
895
1014
  // src/jobs/send-scheduled-email.ts
896
1015
  import { job } from "@spfn/core/job";
897
1016
 
@@ -3759,6 +3878,101 @@ var notificationJobRouter = defineJobRouter({
3759
3878
  sendScheduledSms: sendScheduledSmsJob
3760
3879
  });
3761
3880
 
3881
+ // src/integrations/error-slack.ts
3882
+ function formatHeaders(headers) {
3883
+ const entries = Object.entries(headers);
3884
+ if (entries.length === 0) {
3885
+ return "(none)";
3886
+ }
3887
+ return entries.map(([k, v]) => `${k}: ${v}`).join("\n");
3888
+ }
3889
+ function formatQuery(query) {
3890
+ const entries = Object.entries(query);
3891
+ if (entries.length === 0) {
3892
+ return "(none)";
3893
+ }
3894
+ return entries.map(([k, v]) => `${k}=${v}`).join("\n");
3895
+ }
3896
+ function shortStack(err, maxLines = 3) {
3897
+ if (!err.stack) {
3898
+ return "(no stack)";
3899
+ }
3900
+ const lines = err.stack.split("\n").slice(1);
3901
+ return lines.slice(0, maxLines).map((line) => line.trim()).join("\n");
3902
+ }
3903
+ function defaultFormat(err, ctx) {
3904
+ const emoji = ctx.statusCode >= 500 ? ":rotating_light:" : ":warning:";
3905
+ const title = `${emoji} *${err.name || "Error"}* \u2014 ${ctx.statusCode}`;
3906
+ const fields = [
3907
+ { type: "mrkdwn", text: `*Method*
3908
+ ${ctx.method}` },
3909
+ { type: "mrkdwn", text: `*Path*
3910
+ ${ctx.path}` },
3911
+ { type: "mrkdwn", text: `*User*
3912
+ ${ctx.userId ?? "(anonymous)"}` },
3913
+ { type: "mrkdwn", text: `*Request ID*
3914
+ ${ctx.requestId ?? "(none)"}` }
3915
+ ];
3916
+ const blocks = [
3917
+ // Title
3918
+ {
3919
+ type: "header",
3920
+ text: { type: "plain_text", text: `${err.name || "Error"} \u2014 ${ctx.statusCode}`, emoji: true }
3921
+ },
3922
+ // Error message
3923
+ {
3924
+ type: "section",
3925
+ text: { type: "mrkdwn", text: `> ${err.message}` }
3926
+ },
3927
+ // Fields: method, path, user, requestId
3928
+ {
3929
+ type: "section",
3930
+ fields
3931
+ },
3932
+ // Timestamp
3933
+ {
3934
+ type: "context",
3935
+ elements: [
3936
+ { type: "mrkdwn", text: `*Time:* ${ctx.timestamp}` }
3937
+ ]
3938
+ },
3939
+ { type: "divider" },
3940
+ // Headers
3941
+ {
3942
+ type: "section",
3943
+ text: { type: "mrkdwn", text: `*Request Headers*
3944
+ \`\`\`${formatHeaders(ctx.request.headers)}\`\`\`` }
3945
+ },
3946
+ // Query
3947
+ {
3948
+ type: "section",
3949
+ text: { type: "mrkdwn", text: `*Query Params*
3950
+ \`\`\`${formatQuery(ctx.request.query)}\`\`\`` }
3951
+ },
3952
+ { type: "divider" },
3953
+ // Stack trace
3954
+ {
3955
+ type: "section",
3956
+ text: { type: "mrkdwn", text: `*Stack Trace*
3957
+ \`\`\`${shortStack(err)}\`\`\`` }
3958
+ }
3959
+ ];
3960
+ return { text: title, blocks };
3961
+ }
3962
+ function createErrorSlackNotifier(options = {}) {
3963
+ const { minStatusCode = 500 } = options;
3964
+ return async (err, ctx) => {
3965
+ if (ctx.statusCode < minStatusCode) {
3966
+ return;
3967
+ }
3968
+ const message = options.formatMessage?.(err, ctx) ?? defaultFormat(err, ctx);
3969
+ await sendSlack({
3970
+ ...message,
3971
+ webhookUrl: options.webhookUrl
3972
+ });
3973
+ };
3974
+ }
3975
+
3762
3976
  // src/server.ts
3763
3977
  registerBuiltinTemplates();
3764
3978
  export {
@@ -3769,6 +3983,7 @@ export {
3769
3983
  cancelScheduledNotification,
3770
3984
  configureNotification,
3771
3985
  countNotifications,
3986
+ createErrorSlackNotifier,
3772
3987
  createNotificationRecord,
3773
3988
  createScheduledNotification,
3774
3989
  findNotificationByJobId,
@@ -3792,6 +4007,7 @@ export {
3792
4007
  registerEmailProvider,
3793
4008
  registerFilter,
3794
4009
  registerSMSProvider,
4010
+ registerSlackProvider,
3795
4011
  registerTemplate,
3796
4012
  renderTemplate,
3797
4013
  scheduleEmail,
@@ -3802,6 +4018,8 @@ export {
3802
4018
  sendSMSBulk,
3803
4019
  sendScheduledEmailJob,
3804
4020
  sendScheduledSmsJob,
4021
+ sendSlack,
4022
+ sendSlackBulk,
3805
4023
  updateNotificationJobId
3806
4024
  };
3807
4025
  //# sourceMappingURL=server.js.map