@skoolite/notify-hub 0.1.0 → 0.2.0

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/index.d.mts CHANGED
@@ -337,20 +337,15 @@ interface MetaWhatsAppConfig {
337
337
  webhookVerifyToken?: string;
338
338
  }
339
339
  /**
340
- * AWS SES email provider configuration
340
+ * Resend email provider configuration
341
341
  */
342
- interface SesConfig {
343
- /** AWS region */
344
- region: string;
345
- /** Default from address */
342
+ interface ResendConfig {
343
+ /** Resend API key */
344
+ apiKey: string;
345
+ /** Default from address (must be verified domain) */
346
346
  fromAddress: string;
347
347
  /** Default from name */
348
348
  fromName?: string;
349
- /** AWS credentials (optional, will use default credential chain) */
350
- credentials?: {
351
- accessKeyId: string;
352
- secretAccessKey: string;
353
- };
354
349
  }
355
350
  /**
356
351
  * Provider-specific configuration union
@@ -373,8 +368,8 @@ type WhatsAppProviderConfig = {
373
368
  instance: WhatsAppProvider;
374
369
  };
375
370
  type EmailProviderConfig = {
376
- provider: 'ses';
377
- config: SesConfig;
371
+ provider: 'resend';
372
+ config: ResendConfig;
378
373
  } | {
379
374
  provider: 'custom';
380
375
  instance: EmailProvider;
@@ -825,6 +820,46 @@ declare class MetaWhatsAppProvider implements WhatsAppProvider {
825
820
  destroy(): Promise<void>;
826
821
  }
827
822
 
823
+ /**
824
+ * Resend email provider implementation
825
+ * @see https://resend.com/docs/api-reference/emails/send-email
826
+ */
827
+ declare class ResendEmailProvider implements EmailProvider {
828
+ readonly name = "resend";
829
+ private readonly config;
830
+ private readonly logger?;
831
+ private readonly retryConfig;
832
+ private readonly baseUrl;
833
+ constructor(config: ResendConfig, options?: {
834
+ logger?: Logger;
835
+ retryConfig?: RetryConfig;
836
+ });
837
+ /**
838
+ * Initialize the provider (no-op for Resend, uses REST API)
839
+ */
840
+ initialize(): Promise<void>;
841
+ /**
842
+ * Build the "from" field with optional name
843
+ */
844
+ private buildFrom;
845
+ /**
846
+ * Send an email
847
+ */
848
+ send(to: string, email: EmailMessage, options?: EmailOptions): Promise<SendResult>;
849
+ /**
850
+ * Send bulk emails using Resend batch API
851
+ */
852
+ sendBulk(messages: EmailMessageWithRecipient[], options?: BulkOptions): Promise<BulkResult>;
853
+ /**
854
+ * Basic email validation
855
+ */
856
+ private isValidEmail;
857
+ /**
858
+ * Cleanup resources
859
+ */
860
+ destroy(): Promise<void>;
861
+ }
862
+
828
863
  /**
829
864
  * Twilio webhook payload for message status updates
830
865
  */
@@ -1077,4 +1112,4 @@ declare function getPakistanNetwork(phone: string): 'jazz' | 'telenor' | 'zong'
1077
1112
  */
1078
1113
  declare function routeByPrefix(phone: string, routing: Record<string, 'primary' | 'fallback'>): 'primary' | 'fallback';
1079
1114
 
1080
- export { BaseLocalSmsProvider, type BaseProvider, type BulkOptions, type BulkResult, type BulkSummary, type Channel, type Cost, DEFAULT_RETRY_CONFIG, type EmailAttachment, type EmailMessage, type EmailMessageWithRecipient, type EmailOptions, type EmailProvider, type EmailProviderConfig, type IncomingMessageEvent, JazzSmsProvider, type LocalSmsConfig, type Logger, type MessageStatus, type MetaWhatsAppConfig, MetaWhatsAppProvider, type Notification, type NotifyError, NotifyHub, type NotifyHubConfig, type RateLimitConfig, type RetryConfig, type RoutingConfig, type SendResult, type SesConfig, type SmsChannelConfig, type SmsMessage, type SmsOptions, type SmsProvider, type SmsProviderConfig, type StatusWebhookEvent, TelenorSmsProvider, type TwilioConfig, TwilioSmsProvider, TwilioWebhookHandler, type WebhookEvent, type WebhookHandler, type WhatsAppMessage, type WhatsAppOptions, type WhatsAppProvider, type WhatsAppProviderConfig, type WhatsAppTemplate, type WhatsAppTemplateButton, type WhatsAppTemplateHeader, WhatsAppWebhookHandler, ZongSmsProvider, calculateBackoff, cleanPhoneNumber, createNotifyError, createTwilioWebhookHandler, createWhatsAppWebhookHandler, getCallingCode, getCountryCode, getPakistanNetwork, isCountry, isPakistanMobile, isRetriableError, routeByPrefix, toE164, validatePhoneNumber, withRetry };
1115
+ export { BaseLocalSmsProvider, type BaseProvider, type BulkOptions, type BulkResult, type BulkSummary, type Channel, type Cost, DEFAULT_RETRY_CONFIG, type EmailAttachment, type EmailMessage, type EmailMessageWithRecipient, type EmailOptions, type EmailProvider, type EmailProviderConfig, type IncomingMessageEvent, JazzSmsProvider, type LocalSmsConfig, type Logger, type MessageStatus, type MetaWhatsAppConfig, MetaWhatsAppProvider, type Notification, type NotifyError, NotifyHub, type NotifyHubConfig, type RateLimitConfig, type ResendConfig, ResendEmailProvider, type RetryConfig, type RoutingConfig, type SendResult, type SmsChannelConfig, type SmsMessage, type SmsOptions, type SmsProvider, type SmsProviderConfig, type StatusWebhookEvent, TelenorSmsProvider, type TwilioConfig, TwilioSmsProvider, TwilioWebhookHandler, type WebhookEvent, type WebhookHandler, type WhatsAppMessage, type WhatsAppOptions, type WhatsAppProvider, type WhatsAppProviderConfig, type WhatsAppTemplate, type WhatsAppTemplateButton, type WhatsAppTemplateHeader, WhatsAppWebhookHandler, ZongSmsProvider, calculateBackoff, cleanPhoneNumber, createNotifyError, createTwilioWebhookHandler, createWhatsAppWebhookHandler, getCallingCode, getCountryCode, getPakistanNetwork, isCountry, isPakistanMobile, isRetriableError, routeByPrefix, toE164, validatePhoneNumber, withRetry };
package/dist/index.d.ts CHANGED
@@ -337,20 +337,15 @@ interface MetaWhatsAppConfig {
337
337
  webhookVerifyToken?: string;
338
338
  }
339
339
  /**
340
- * AWS SES email provider configuration
340
+ * Resend email provider configuration
341
341
  */
342
- interface SesConfig {
343
- /** AWS region */
344
- region: string;
345
- /** Default from address */
342
+ interface ResendConfig {
343
+ /** Resend API key */
344
+ apiKey: string;
345
+ /** Default from address (must be verified domain) */
346
346
  fromAddress: string;
347
347
  /** Default from name */
348
348
  fromName?: string;
349
- /** AWS credentials (optional, will use default credential chain) */
350
- credentials?: {
351
- accessKeyId: string;
352
- secretAccessKey: string;
353
- };
354
349
  }
355
350
  /**
356
351
  * Provider-specific configuration union
@@ -373,8 +368,8 @@ type WhatsAppProviderConfig = {
373
368
  instance: WhatsAppProvider;
374
369
  };
375
370
  type EmailProviderConfig = {
376
- provider: 'ses';
377
- config: SesConfig;
371
+ provider: 'resend';
372
+ config: ResendConfig;
378
373
  } | {
379
374
  provider: 'custom';
380
375
  instance: EmailProvider;
@@ -825,6 +820,46 @@ declare class MetaWhatsAppProvider implements WhatsAppProvider {
825
820
  destroy(): Promise<void>;
826
821
  }
827
822
 
823
+ /**
824
+ * Resend email provider implementation
825
+ * @see https://resend.com/docs/api-reference/emails/send-email
826
+ */
827
+ declare class ResendEmailProvider implements EmailProvider {
828
+ readonly name = "resend";
829
+ private readonly config;
830
+ private readonly logger?;
831
+ private readonly retryConfig;
832
+ private readonly baseUrl;
833
+ constructor(config: ResendConfig, options?: {
834
+ logger?: Logger;
835
+ retryConfig?: RetryConfig;
836
+ });
837
+ /**
838
+ * Initialize the provider (no-op for Resend, uses REST API)
839
+ */
840
+ initialize(): Promise<void>;
841
+ /**
842
+ * Build the "from" field with optional name
843
+ */
844
+ private buildFrom;
845
+ /**
846
+ * Send an email
847
+ */
848
+ send(to: string, email: EmailMessage, options?: EmailOptions): Promise<SendResult>;
849
+ /**
850
+ * Send bulk emails using Resend batch API
851
+ */
852
+ sendBulk(messages: EmailMessageWithRecipient[], options?: BulkOptions): Promise<BulkResult>;
853
+ /**
854
+ * Basic email validation
855
+ */
856
+ private isValidEmail;
857
+ /**
858
+ * Cleanup resources
859
+ */
860
+ destroy(): Promise<void>;
861
+ }
862
+
828
863
  /**
829
864
  * Twilio webhook payload for message status updates
830
865
  */
@@ -1077,4 +1112,4 @@ declare function getPakistanNetwork(phone: string): 'jazz' | 'telenor' | 'zong'
1077
1112
  */
1078
1113
  declare function routeByPrefix(phone: string, routing: Record<string, 'primary' | 'fallback'>): 'primary' | 'fallback';
1079
1114
 
1080
- export { BaseLocalSmsProvider, type BaseProvider, type BulkOptions, type BulkResult, type BulkSummary, type Channel, type Cost, DEFAULT_RETRY_CONFIG, type EmailAttachment, type EmailMessage, type EmailMessageWithRecipient, type EmailOptions, type EmailProvider, type EmailProviderConfig, type IncomingMessageEvent, JazzSmsProvider, type LocalSmsConfig, type Logger, type MessageStatus, type MetaWhatsAppConfig, MetaWhatsAppProvider, type Notification, type NotifyError, NotifyHub, type NotifyHubConfig, type RateLimitConfig, type RetryConfig, type RoutingConfig, type SendResult, type SesConfig, type SmsChannelConfig, type SmsMessage, type SmsOptions, type SmsProvider, type SmsProviderConfig, type StatusWebhookEvent, TelenorSmsProvider, type TwilioConfig, TwilioSmsProvider, TwilioWebhookHandler, type WebhookEvent, type WebhookHandler, type WhatsAppMessage, type WhatsAppOptions, type WhatsAppProvider, type WhatsAppProviderConfig, type WhatsAppTemplate, type WhatsAppTemplateButton, type WhatsAppTemplateHeader, WhatsAppWebhookHandler, ZongSmsProvider, calculateBackoff, cleanPhoneNumber, createNotifyError, createTwilioWebhookHandler, createWhatsAppWebhookHandler, getCallingCode, getCountryCode, getPakistanNetwork, isCountry, isPakistanMobile, isRetriableError, routeByPrefix, toE164, validatePhoneNumber, withRetry };
1115
+ export { BaseLocalSmsProvider, type BaseProvider, type BulkOptions, type BulkResult, type BulkSummary, type Channel, type Cost, DEFAULT_RETRY_CONFIG, type EmailAttachment, type EmailMessage, type EmailMessageWithRecipient, type EmailOptions, type EmailProvider, type EmailProviderConfig, type IncomingMessageEvent, JazzSmsProvider, type LocalSmsConfig, type Logger, type MessageStatus, type MetaWhatsAppConfig, MetaWhatsAppProvider, type Notification, type NotifyError, NotifyHub, type NotifyHubConfig, type RateLimitConfig, type ResendConfig, ResendEmailProvider, type RetryConfig, type RoutingConfig, type SendResult, type SmsChannelConfig, type SmsMessage, type SmsOptions, type SmsProvider, type SmsProviderConfig, type StatusWebhookEvent, TelenorSmsProvider, type TwilioConfig, TwilioSmsProvider, TwilioWebhookHandler, type WebhookEvent, type WebhookHandler, type WhatsAppMessage, type WhatsAppOptions, type WhatsAppProvider, type WhatsAppProviderConfig, type WhatsAppTemplate, type WhatsAppTemplateButton, type WhatsAppTemplateHeader, WhatsAppWebhookHandler, ZongSmsProvider, calculateBackoff, cleanPhoneNumber, createNotifyError, createTwilioWebhookHandler, createWhatsAppWebhookHandler, getCallingCode, getCountryCode, getPakistanNetwork, isCountry, isPakistanMobile, isRetriableError, routeByPrefix, toE164, validatePhoneNumber, withRetry };
package/dist/index.js CHANGED
@@ -975,6 +975,207 @@ var MetaWhatsAppProvider = class {
975
975
  }
976
976
  };
977
977
 
978
+ // src/providers/email/resend.provider.ts
979
+ var ResendEmailProvider = class {
980
+ name = "resend";
981
+ config;
982
+ logger;
983
+ retryConfig;
984
+ baseUrl = "https://api.resend.com";
985
+ constructor(config, options) {
986
+ this.config = config;
987
+ this.logger = options?.logger;
988
+ this.retryConfig = options?.retryConfig ?? DEFAULT_RETRY_CONFIG;
989
+ }
990
+ /**
991
+ * Initialize the provider (no-op for Resend, uses REST API)
992
+ */
993
+ async initialize() {
994
+ this.logger?.debug("ResendEmailProvider initialized");
995
+ }
996
+ /**
997
+ * Build the "from" field with optional name
998
+ */
999
+ buildFrom(options) {
1000
+ const name = options?.fromName ?? this.config.fromName;
1001
+ const address = options?.from ?? this.config.fromAddress;
1002
+ if (name) {
1003
+ return `${name} <${address}>`;
1004
+ }
1005
+ return address;
1006
+ }
1007
+ /**
1008
+ * Send an email
1009
+ */
1010
+ async send(to, email, options) {
1011
+ const timestamp = /* @__PURE__ */ new Date();
1012
+ if (!this.isValidEmail(to)) {
1013
+ return {
1014
+ success: false,
1015
+ channel: "email",
1016
+ status: "failed",
1017
+ provider: this.name,
1018
+ error: {
1019
+ code: "INVALID_EMAIL",
1020
+ message: `Invalid email address: ${to}`,
1021
+ retriable: false
1022
+ },
1023
+ timestamp,
1024
+ metadata: options?.metadata
1025
+ };
1026
+ }
1027
+ if (!email.body && !email.html) {
1028
+ return {
1029
+ success: false,
1030
+ channel: "email",
1031
+ status: "failed",
1032
+ provider: this.name,
1033
+ error: {
1034
+ code: "INVALID_EMAIL_CONTENT",
1035
+ message: "Email must have either body or html content",
1036
+ retriable: false
1037
+ },
1038
+ timestamp,
1039
+ metadata: options?.metadata
1040
+ };
1041
+ }
1042
+ this.logger?.debug(`Sending email to ${to}`);
1043
+ const result = await withRetry(
1044
+ async () => {
1045
+ const payload = {
1046
+ from: this.buildFrom(options),
1047
+ to: [to],
1048
+ subject: email.subject
1049
+ };
1050
+ if (email.html) {
1051
+ payload.html = email.html;
1052
+ }
1053
+ if (email.body) {
1054
+ payload.text = email.body;
1055
+ }
1056
+ if (options?.replyTo) {
1057
+ payload.reply_to = options.replyTo;
1058
+ }
1059
+ if (options?.cc?.length) {
1060
+ payload.cc = options.cc;
1061
+ }
1062
+ if (options?.bcc?.length) {
1063
+ payload.bcc = options.bcc;
1064
+ }
1065
+ if (email.attachments?.length) {
1066
+ payload.attachments = email.attachments.map((att) => ({
1067
+ filename: att.filename,
1068
+ content: typeof att.content === "string" ? att.content : att.content.toString("base64"),
1069
+ content_type: att.contentType
1070
+ }));
1071
+ }
1072
+ const response = await fetch(`${this.baseUrl}/emails`, {
1073
+ method: "POST",
1074
+ headers: {
1075
+ "Authorization": `Bearer ${this.config.apiKey}`,
1076
+ "Content-Type": "application/json"
1077
+ },
1078
+ body: JSON.stringify(payload)
1079
+ });
1080
+ if (!response.ok) {
1081
+ const errorData = await response.json();
1082
+ const error = new Error(errorData.message);
1083
+ error.statusCode = response.status;
1084
+ throw error;
1085
+ }
1086
+ return await response.json();
1087
+ },
1088
+ this.retryConfig
1089
+ );
1090
+ if (result.success && result.value) {
1091
+ this.logger?.info(`Email sent successfully: ${result.value.id}`);
1092
+ return {
1093
+ success: true,
1094
+ messageId: result.value.id,
1095
+ channel: "email",
1096
+ status: "sent",
1097
+ provider: this.name,
1098
+ timestamp,
1099
+ metadata: options?.metadata
1100
+ };
1101
+ }
1102
+ this.logger?.error(`Email send failed: ${result.error?.message}`);
1103
+ return {
1104
+ success: false,
1105
+ channel: "email",
1106
+ status: "failed",
1107
+ provider: this.name,
1108
+ error: createNotifyError(result.error, "RESEND_ERROR"),
1109
+ timestamp,
1110
+ metadata: options?.metadata
1111
+ };
1112
+ }
1113
+ /**
1114
+ * Send bulk emails using Resend batch API
1115
+ */
1116
+ async sendBulk(messages, options) {
1117
+ const startTime = Date.now();
1118
+ const results = [];
1119
+ let sent = 0;
1120
+ let failed = 0;
1121
+ const batchSize = Math.min(options?.concurrency ?? 100, 100);
1122
+ for (let i = 0; i < messages.length; i += batchSize) {
1123
+ const batch = messages.slice(i, i + batchSize);
1124
+ const batchResults = await Promise.all(
1125
+ batch.map(
1126
+ (msg) => this.send(msg.to, msg, { metadata: msg.metadata })
1127
+ )
1128
+ );
1129
+ for (const result of batchResults) {
1130
+ results.push(result);
1131
+ if (result.success) {
1132
+ sent++;
1133
+ } else {
1134
+ failed++;
1135
+ if (options?.stopOnError) {
1136
+ return {
1137
+ results,
1138
+ summary: {
1139
+ total: messages.length,
1140
+ sent,
1141
+ failed,
1142
+ durationMs: Date.now() - startTime
1143
+ }
1144
+ };
1145
+ }
1146
+ }
1147
+ }
1148
+ options?.onProgress?.(sent + failed, messages.length);
1149
+ if (i + batchSize < messages.length && options?.rateLimit) {
1150
+ const delayMs = Math.ceil(1e3 / options.rateLimit.messagesPerSecond) * batchSize;
1151
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1152
+ }
1153
+ }
1154
+ return {
1155
+ results,
1156
+ summary: {
1157
+ total: messages.length,
1158
+ sent,
1159
+ failed,
1160
+ durationMs: Date.now() - startTime
1161
+ }
1162
+ };
1163
+ }
1164
+ /**
1165
+ * Basic email validation
1166
+ */
1167
+ isValidEmail(email) {
1168
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1169
+ return emailRegex.test(email);
1170
+ }
1171
+ /**
1172
+ * Cleanup resources
1173
+ */
1174
+ async destroy() {
1175
+ this.logger?.debug("ResendEmailProvider destroyed");
1176
+ }
1177
+ };
1178
+
978
1179
  // src/notify-hub.ts
979
1180
  var noopLogger = {
980
1181
  debug: () => void 0,
@@ -1096,8 +1297,14 @@ var NotifyHub = class {
1096
1297
  if (config.provider === "custom" && config.instance) {
1097
1298
  return config.instance;
1098
1299
  }
1099
- if (config.provider === "ses") {
1100
- throw new Error("SES email provider not yet implemented");
1300
+ if (config.provider === "resend") {
1301
+ const resendConfig = config.config;
1302
+ const provider = new ResendEmailProvider(resendConfig, {
1303
+ logger: this.logger,
1304
+ retryConfig: this.retryConfig
1305
+ });
1306
+ await provider.initialize();
1307
+ return provider;
1101
1308
  }
1102
1309
  throw new Error(`Unknown email provider: ${config.provider}`);
1103
1310
  }
@@ -1668,6 +1875,7 @@ exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
1668
1875
  exports.JazzSmsProvider = JazzSmsProvider;
1669
1876
  exports.MetaWhatsAppProvider = MetaWhatsAppProvider;
1670
1877
  exports.NotifyHub = NotifyHub;
1878
+ exports.ResendEmailProvider = ResendEmailProvider;
1671
1879
  exports.TelenorSmsProvider = TelenorSmsProvider;
1672
1880
  exports.TwilioSmsProvider = TwilioSmsProvider;
1673
1881
  exports.TwilioWebhookHandler = TwilioWebhookHandler;