sently 0.8.0 → 0.9.1

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.
Files changed (249) hide show
  1. package/AGENTS.md +33 -0
  2. package/CHANGELOG.md +82 -41
  3. package/README.md +89 -960
  4. package/dist/adapters/bun.js +1 -1
  5. package/dist/adapters/cf.js +2 -2
  6. package/dist/adapters/cf.js.map +2 -2
  7. package/dist/adapters/deno.js +1 -1
  8. package/dist/adapters/node.js +1 -1
  9. package/dist/auth/oauth2.js +2 -2
  10. package/dist/auth/oauth2.js.map +1 -1
  11. package/dist/chunk-0gqy32pe.js +4 -0
  12. package/dist/{chunk-n5dan5bz.js.map → chunk-0gqy32pe.js.map} +2 -2
  13. package/dist/{chunk-z62q7kqr.js → chunk-0qxws3kj.js} +2 -2
  14. package/dist/{chunk-z62q7kqr.js.map → chunk-0qxws3kj.js.map} +1 -1
  15. package/dist/chunk-29z7fkzy.js +4 -0
  16. package/dist/{chunk-wsymfbkn.js.map → chunk-29z7fkzy.js.map} +2 -2
  17. package/dist/chunk-5915vbcj.js +4 -0
  18. package/dist/chunk-5915vbcj.js.map +10 -0
  19. package/dist/{chunk-nbmq8ejv.js → chunk-6npp3x3c.js} +2 -2
  20. package/dist/{chunk-nbmq8ejv.js.map → chunk-6npp3x3c.js.map} +1 -1
  21. package/dist/chunk-7gy2q9sh.js +4 -0
  22. package/dist/chunk-7gy2q9sh.js.map +10 -0
  23. package/dist/{chunk-zst8an2p.js → chunk-8dty8c4p.js} +2 -2
  24. package/dist/{chunk-zst8an2p.js.map → chunk-8dty8c4p.js.map} +2 -2
  25. package/dist/{chunk-f3dz43cy.js → chunk-8njyga62.js} +2 -2
  26. package/dist/{chunk-f3dz43cy.js.map → chunk-8njyga62.js.map} +1 -1
  27. package/dist/chunk-93vqxxj2.js +4 -0
  28. package/dist/{chunk-qh90yr6d.js.map → chunk-93vqxxj2.js.map} +2 -2
  29. package/dist/chunk-b602dhck.js +4 -0
  30. package/dist/{chunk-wbvhaqcp.js.map → chunk-b602dhck.js.map} +2 -2
  31. package/dist/{chunk-9zkv5azm.js → chunk-gp4fwrs8.js} +2 -2
  32. package/dist/{chunk-9zkv5azm.js.map → chunk-gp4fwrs8.js.map} +1 -1
  33. package/dist/chunk-n0qeyzqm.js +13 -0
  34. package/dist/{chunk-8phpsns1.js.map → chunk-n0qeyzqm.js.map} +2 -2
  35. package/dist/chunk-qtkd5bak.js +4 -0
  36. package/dist/chunk-qtkd5bak.js.map +11 -0
  37. package/dist/chunk-skqhj0rm.js +4 -0
  38. package/dist/{chunk-vhxzhx8b.js.map → chunk-skqhj0rm.js.map} +2 -2
  39. package/dist/{chunk-6bkj4y4r.js → chunk-tamww15j.js} +2 -2
  40. package/dist/{chunk-6bkj4y4r.js.map → chunk-tamww15j.js.map} +1 -1
  41. package/dist/chunk-vjpds2pf.js +3 -0
  42. package/dist/{chunk-m07smapm.js.map → chunk-vjpds2pf.js.map} +2 -2
  43. package/dist/chunk-yp311efr.js +4 -0
  44. package/dist/chunk-yp311efr.js.map +10 -0
  45. package/dist/chunk-ywq6s10g.js +5 -0
  46. package/dist/{chunk-5scdgffb.js.map → chunk-ywq6s10g.js.map} +3 -3
  47. package/dist/core/base64.d.ts +10 -0
  48. package/dist/core/errors.js +2 -2
  49. package/dist/core/errors.js.map +1 -1
  50. package/dist/core/hooks.d.ts +14 -0
  51. package/dist/core/plugin.d.ts +7 -7
  52. package/dist/core/push-endpoint.d.ts +29 -0
  53. package/dist/core/push-types.d.ts +98 -0
  54. package/dist/core/sms-types.d.ts +76 -0
  55. package/dist/core/smtp.js +2 -2
  56. package/dist/core/smtp.js.map +1 -1
  57. package/dist/core/whatsapp-types.d.ts +116 -0
  58. package/dist/detect.js +2 -2
  59. package/dist/detect.js.map +1 -1
  60. package/dist/dkim.js +2 -2
  61. package/dist/dkim.js.map +2 -2
  62. package/dist/idempotency.js +2 -2
  63. package/dist/idempotency.js.map +1 -1
  64. package/dist/index.d.ts +5 -32
  65. package/dist/index.js +0 -35
  66. package/dist/mailer.js +2 -2
  67. package/dist/mailer.js.map +1 -1
  68. package/dist/observability/console.js +1 -1
  69. package/dist/plugins/react.js +2 -2
  70. package/dist/plugins/react.js.map +2 -2
  71. package/dist/plugins/template.js +1 -1
  72. package/dist/pool/pool.js +2 -2
  73. package/dist/pool/pool.js.map +1 -1
  74. package/dist/push.d.ts +26 -0
  75. package/dist/push.js +3 -0
  76. package/dist/push.js.map +10 -0
  77. package/dist/sms.d.ts +26 -0
  78. package/dist/sms.js +3 -0
  79. package/dist/sms.js.map +10 -0
  80. package/dist/smtp-mailer.js +2 -2
  81. package/dist/smtp-mailer.js.map +2 -2
  82. package/dist/transports/brevo.js +2 -2
  83. package/dist/transports/brevo.js.map +2 -2
  84. package/dist/transports/cloudflare-email.js +2 -2
  85. package/dist/transports/cloudflare-email.js.map +2 -2
  86. package/dist/transports/fallback.js +2 -2
  87. package/dist/transports/fallback.js.map +1 -1
  88. package/dist/transports/loops.js +2 -2
  89. package/dist/transports/loops.js.map +2 -2
  90. package/dist/transports/mailersend.js +2 -2
  91. package/dist/transports/mailersend.js.map +2 -2
  92. package/dist/transports/mailgun.js +2 -2
  93. package/dist/transports/mailgun.js.map +2 -2
  94. package/dist/transports/mailtrap.js +2 -2
  95. package/dist/transports/mailtrap.js.map +2 -2
  96. package/dist/transports/msegat.d.ts +132 -0
  97. package/dist/transports/msegat.js +3 -0
  98. package/dist/transports/msegat.js.map +10 -0
  99. package/dist/transports/plunk.js +2 -2
  100. package/dist/transports/plunk.js.map +2 -2
  101. package/dist/transports/postmark.js +2 -2
  102. package/dist/transports/postmark.js.map +2 -2
  103. package/dist/transports/preview.js +2 -2
  104. package/dist/transports/preview.js.map +2 -2
  105. package/dist/transports/resend.js +2 -2
  106. package/dist/transports/resend.js.map +2 -2
  107. package/dist/transports/retry.js +2 -2
  108. package/dist/transports/retry.js.map +2 -2
  109. package/dist/transports/sendgrid.js +2 -2
  110. package/dist/transports/sendgrid.js.map +2 -2
  111. package/dist/transports/ses.js +2 -2
  112. package/dist/transports/ses.js.map +2 -2
  113. package/dist/transports/smtp.js +2 -2
  114. package/dist/transports/smtp.js.map +1 -1
  115. package/dist/transports/sndr.d.ts +41 -0
  116. package/dist/transports/sndr.js +3 -0
  117. package/dist/transports/sndr.js.map +10 -0
  118. package/dist/transports/sparkpost.js +2 -2
  119. package/dist/transports/sparkpost.js.map +2 -2
  120. package/dist/transports/taqnyat-mail.d.ts +33 -0
  121. package/dist/transports/taqnyat-mail.js +3 -0
  122. package/dist/transports/taqnyat-mail.js.map +10 -0
  123. package/dist/transports/taqnyat-phone.d.ts +4 -0
  124. package/dist/transports/taqnyat-sms.d.ts +131 -0
  125. package/dist/transports/taqnyat-sms.js +3 -0
  126. package/dist/transports/taqnyat-sms.js.map +10 -0
  127. package/dist/transports/taqnyat-whatsapp.d.ts +51 -0
  128. package/dist/transports/taqnyat-whatsapp.js +3 -0
  129. package/dist/transports/taqnyat-whatsapp.js.map +10 -0
  130. package/dist/transports/twilio-sms.d.ts +66 -0
  131. package/dist/transports/twilio-sms.js +3 -0
  132. package/dist/transports/twilio-sms.js.map +10 -0
  133. package/dist/transports/webpush.d.ts +46 -0
  134. package/dist/transports/webpush.js +3 -0
  135. package/dist/transports/webpush.js.map +10 -0
  136. package/dist/transports/weighted-fallback.js +2 -2
  137. package/dist/transports/weighted-fallback.js.map +1 -1
  138. package/dist/transports/whatsapp-cloud.d.ts +56 -0
  139. package/dist/transports/whatsapp-cloud.js +3 -0
  140. package/dist/transports/whatsapp-cloud.js.map +10 -0
  141. package/dist/webhooks/brevo.js +1 -1
  142. package/dist/webhooks/mailgun.js +1 -1
  143. package/dist/webhooks/postmark.js +1 -1
  144. package/dist/webhooks/resend.js +1 -1
  145. package/dist/webhooks/sendgrid.js +1 -1
  146. package/dist/webhooks/ses.js +1 -1
  147. package/dist/webhooks/sndr.d.ts +16 -0
  148. package/dist/webhooks/sndr.js +3 -0
  149. package/dist/webhooks/sndr.js.map +10 -0
  150. package/dist/webhooks/timing-safe-equal.js +1 -1
  151. package/dist/webhooks.d.ts +9 -2
  152. package/dist/webhooks.js +4 -0
  153. package/dist/whatsapp.d.ts +26 -0
  154. package/dist/whatsapp.js +3 -0
  155. package/dist/whatsapp.js.map +10 -0
  156. package/package.json +85 -4
  157. package/site/README.md +26 -0
  158. package/site/content/docs/ai/index.mdx +13 -0
  159. package/site/content/docs/ai/llms-txt.mdx +12 -0
  160. package/site/content/docs/ai/mcp.mdx +16 -0
  161. package/site/content/docs/ai/meta.json +9 -0
  162. package/site/content/docs/channels/email.mdx +100 -0
  163. package/site/content/docs/channels/hooks.mdx +87 -0
  164. package/site/content/docs/channels/index.mdx +81 -0
  165. package/site/content/docs/channels/meta.json +12 -0
  166. package/site/content/docs/channels/push.mdx +92 -0
  167. package/site/content/docs/channels/sms.mdx +88 -0
  168. package/site/content/docs/channels/whatsapp.mdx +95 -0
  169. package/site/content/docs/decorators/fallback.mdx +34 -0
  170. package/site/content/docs/decorators/idempotency.mdx +34 -0
  171. package/site/content/docs/decorators/index.mdx +50 -0
  172. package/site/content/docs/decorators/meta.json +12 -0
  173. package/site/content/docs/decorators/preview.mdx +34 -0
  174. package/site/content/docs/decorators/retry.mdx +34 -0
  175. package/site/content/docs/decorators/weighted-fallback.mdx +34 -0
  176. package/site/content/docs/get-started/entrypoints.mdx +114 -0
  177. package/site/content/docs/get-started/index.mdx +14 -0
  178. package/site/content/docs/get-started/installation.mdx +78 -0
  179. package/site/content/docs/get-started/introduction.mdx +77 -0
  180. package/site/content/docs/get-started/meta.json +12 -0
  181. package/site/content/docs/get-started/migrate-nodemailer.mdx +23 -0
  182. package/site/content/docs/get-started/runtimes.mdx +17 -0
  183. package/site/content/docs/guides/adapters.mdx +26 -0
  184. package/site/content/docs/guides/attachments.mdx +19 -0
  185. package/site/content/docs/guides/bundle-size.mdx +78 -0
  186. package/site/content/docs/guides/dkim.mdx +26 -0
  187. package/site/content/docs/guides/index.mdx +15 -0
  188. package/site/content/docs/guides/meta.json +21 -0
  189. package/site/content/docs/guides/oauth2.mdx +23 -0
  190. package/site/content/docs/guides/observability.mdx +16 -0
  191. package/site/content/docs/guides/plugins-template.mdx +22 -0
  192. package/site/content/docs/guides/pool.mdx +24 -0
  193. package/site/content/docs/guides/react-email.mdx +20 -0
  194. package/site/content/docs/guides/security.mdx +17 -0
  195. package/site/content/docs/guides/send-bulk.mdx +18 -0
  196. package/site/content/docs/guides/vendor-extras-otp.mdx +18 -0
  197. package/site/content/docs/guides/webhooks.mdx +120 -0
  198. package/site/content/docs/guides/webpush-interop.mdx +18 -0
  199. package/site/content/docs/index.mdx +15 -0
  200. package/site/content/docs/meta.json +15 -0
  201. package/site/content/docs/quick-start/email.mdx +40 -0
  202. package/site/content/docs/quick-start/index.mdx +15 -0
  203. package/site/content/docs/quick-start/meta.json +5 -0
  204. package/site/content/docs/quick-start/push.mdx +36 -0
  205. package/site/content/docs/quick-start/sms.mdx +36 -0
  206. package/site/content/docs/quick-start/whatsapp.mdx +38 -0
  207. package/site/content/docs/reference/errors.mdx +22 -0
  208. package/site/content/docs/reference/exports.mdx +83 -0
  209. package/site/content/docs/reference/index.mdx +15 -0
  210. package/site/content/docs/reference/mail-options.mdx +22 -0
  211. package/site/content/docs/reference/meta.json +15 -0
  212. package/site/content/docs/reference/push-options.mdx +20 -0
  213. package/site/content/docs/reference/sms-options.mdx +17 -0
  214. package/site/content/docs/reference/transport-contracts.mdx +17 -0
  215. package/site/content/docs/reference/webhook-events.mdx +70 -0
  216. package/site/content/docs/reference/whatsapp-options.mdx +15 -0
  217. package/site/content/docs/transports/brevo.mdx +40 -0
  218. package/site/content/docs/transports/cloudflare-email.mdx +40 -0
  219. package/site/content/docs/transports/index.mdx +105 -0
  220. package/site/content/docs/transports/loops.mdx +41 -0
  221. package/site/content/docs/transports/mailersend.mdx +40 -0
  222. package/site/content/docs/transports/mailgun.mdx +42 -0
  223. package/site/content/docs/transports/mailtrap.mdx +42 -0
  224. package/site/content/docs/transports/meta.json +32 -0
  225. package/site/content/docs/transports/msegat.mdx +42 -0
  226. package/site/content/docs/transports/plunk.mdx +40 -0
  227. package/site/content/docs/transports/postmark.mdx +40 -0
  228. package/site/content/docs/transports/resend.mdx +43 -0
  229. package/site/content/docs/transports/sendgrid.mdx +40 -0
  230. package/site/content/docs/transports/ses.mdx +44 -0
  231. package/site/content/docs/transports/smtp.mdx +47 -0
  232. package/site/content/docs/transports/sndr.mdx +144 -0
  233. package/site/content/docs/transports/sparkpost.mdx +41 -0
  234. package/site/content/docs/transports/taqnyat-mail.mdx +41 -0
  235. package/site/content/docs/transports/taqnyat-sms.mdx +41 -0
  236. package/site/content/docs/transports/taqnyat-whatsapp.mdx +40 -0
  237. package/site/content/docs/transports/twilio-sms.mdx +128 -0
  238. package/site/content/docs/transports/webpush.mdx +43 -0
  239. package/site/content/docs/transports/whatsapp-cloud.mdx +42 -0
  240. package/dist/chunk-5scdgffb.js +0 -5
  241. package/dist/chunk-8phpsns1.js +0 -13
  242. package/dist/chunk-m07smapm.js +0 -3
  243. package/dist/chunk-n5dan5bz.js +0 -4
  244. package/dist/chunk-qh90yr6d.js +0 -4
  245. package/dist/chunk-vhxzhx8b.js +0 -4
  246. package/dist/chunk-wbvhaqcp.js +0 -4
  247. package/dist/chunk-wsymfbkn.js +0 -4
  248. package/dist/chunk-yr56b0ts.js +0 -4
  249. package/dist/chunk-yr56b0ts.js.map +0 -11
@@ -1,3 +1,3 @@
1
- import{d as F,e as u,f as G,g as H}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var K={delivered:"delivered",hard_bounce:"bounced",hardBounce:"bounced",soft_bounce:"bounced",soft_bounced:"bounced",softBounce:"bounced",blocked:"bounced",invalid:"bounced",invalid_email:"bounced",deferred:"deferred",spam:"complained",opened:"opened",unique_opened:"opened",uniqueOpened:"opened",click:"clicked",clicked:"clicked",unique_click:"clicked",uniqueClicked:"clicked",sent:"unknown",request:"unknown",unsubscribe:"complained",unsubscribed:"complained"};function L(f){return u(f.ts_event)??u(f.ts)??u(f.date_event)??u(f.date)??u(f.ts_epoch!==void 0?Number(f.ts_epoch):void 0)}function Q(f){if(typeof f["message-id"]==="string")return f["message-id"];if(typeof f.messageId==="string")return f.messageId;return}function U(f){return G(f).flatMap((x)=>{if(typeof x!=="object"||x===null)return[];let q=x,J=typeof q.event==="string"?q.event:"",C=L(q),D=Q(q);return[H({provider:"brevo",type:F(J,K),...D!==void 0?{messageId:D}:{},...typeof q.email==="string"?{recipient:q.email}:{},...C!==void 0?{timestamp:C}:{},raw:x})]})}export{U as parse};
1
+ import{d as F,e as u,f as G,g as H}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var K={delivered:"delivered",hard_bounce:"bounced",hardBounce:"bounced",soft_bounce:"bounced",soft_bounced:"bounced",softBounce:"bounced",blocked:"bounced",invalid:"bounced",invalid_email:"bounced",deferred:"deferred",spam:"complained",opened:"opened",unique_opened:"opened",uniqueOpened:"opened",click:"clicked",clicked:"clicked",unique_click:"clicked",uniqueClicked:"clicked",sent:"unknown",request:"unknown",unsubscribe:"complained",unsubscribed:"complained"};function L(f){return u(f.ts_event)??u(f.ts)??u(f.date_event)??u(f.date)??u(f.ts_epoch!==void 0?Number(f.ts_epoch):void 0)}function Q(f){if(typeof f["message-id"]==="string")return f["message-id"];if(typeof f.messageId==="string")return f.messageId;return}function U(f){return G(f).flatMap((x)=>{if(typeof x!=="object"||x===null)return[];let q=x,J=typeof q.event==="string"?q.event:"",C=L(q),D=Q(q);return[H({provider:"brevo",type:F(J,K),...D!==void 0?{messageId:D}:{},...typeof q.email==="string"?{recipient:q.email}:{},...C!==void 0?{timestamp:C}:{},raw:x})]})}export{U as parse};
2
2
 
3
3
  //# debugId=916BC5D0AADEB5C764756E2164756E21
@@ -1,3 +1,3 @@
1
- import{a as G,c as H}from"../chunk-pqp843mh.js";import{d as Y,e as Z,g as $}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var R={accepted:"unknown",delivered:"delivered",rejected:"bounced",complained:"complained",opened:"opened",clicked:"clicked",stored:"unknown",unsubscribed:"unknown"};function U(z){let J=typeof z.event==="string"?z.event:"";if(J==="failed")return z.severity==="temporary"?"deferred":"bounced";return Y(J,R)}function b(z){if(!z)return;let J=z.headers;if(typeof J==="object"&&J!==null){let C=J["message-id"];if(typeof C==="string")return C}return typeof z["message-id"]==="string"?z["message-id"]:void 0}function N(z){if(typeof z!=="object"||z===null)return[];let J=z,C=typeof J["event-data"]==="object"&&J["event-data"]!==null?J["event-data"]:J,O=typeof C.message==="object"&&C.message!==null?C.message:void 0,Q=b(O),W=Z(C.timestamp);return[$({provider:"mailgun",type:U(C),...Q!==void 0?{messageId:Q}:{},...typeof C.recipient==="string"?{recipient:C.recipient}:{},...W!==void 0?{timestamp:W}:{},raw:z})]}async function q(z,J,C,O){let Q=await crypto.subtle.importKey("raw",new TextEncoder().encode(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),W=new TextEncoder().encode(z+J),L=await crypto.subtle.sign("HMAC",Q,W),P=new Uint8Array(L),X=H(C);if(X===null)return!1;return G(P,X)}async function S(z,J){if(typeof z!=="object"||z===null)return!1;let C=z.signature;if(typeof C!=="object"||C===null)return!1;let O=C;if(typeof O.timestamp!=="string"||typeof O.token!=="string"||typeof O.signature!=="string")return!1;return q(O.timestamp,O.token,O.signature,J)}export{q as verifySignature,S as verifyPayload,N as parse};
1
+ import{a as G,c as H}from"../chunk-pqp843mh.js";import{d as Y,e as Z,g as $}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var R={accepted:"unknown",delivered:"delivered",rejected:"bounced",complained:"complained",opened:"opened",clicked:"clicked",stored:"unknown",unsubscribed:"unknown"};function U(z){let J=typeof z.event==="string"?z.event:"";if(J==="failed")return z.severity==="temporary"?"deferred":"bounced";return Y(J,R)}function b(z){if(!z)return;let J=z.headers;if(typeof J==="object"&&J!==null){let C=J["message-id"];if(typeof C==="string")return C}return typeof z["message-id"]==="string"?z["message-id"]:void 0}function N(z){if(typeof z!=="object"||z===null)return[];let J=z,C=typeof J["event-data"]==="object"&&J["event-data"]!==null?J["event-data"]:J,O=typeof C.message==="object"&&C.message!==null?C.message:void 0,Q=b(O),W=Z(C.timestamp);return[$({provider:"mailgun",type:U(C),...Q!==void 0?{messageId:Q}:{},...typeof C.recipient==="string"?{recipient:C.recipient}:{},...W!==void 0?{timestamp:W}:{},raw:z})]}async function q(z,J,C,O){let Q=await crypto.subtle.importKey("raw",new TextEncoder().encode(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),W=new TextEncoder().encode(z+J),L=await crypto.subtle.sign("HMAC",Q,W),P=new Uint8Array(L),X=H(C);if(X===null)return!1;return G(P,X)}async function S(z,J){if(typeof z!=="object"||z===null)return!1;let C=z.signature;if(typeof C!=="object"||C===null)return!1;let O=C;if(typeof O.timestamp!=="string"||typeof O.token!=="string"||typeof O.signature!=="string")return!1;return q(O.timestamp,O.token,O.signature,J)}export{q as verifySignature,S as verifyPayload,N as parse};
2
2
 
3
3
  //# debugId=7FB21CD9E29B4FE264756E2164756E21
@@ -1,3 +1,3 @@
1
- import{d as u,e as q,g as x}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var C={Delivery:"delivered",Bounce:"bounced",SpamComplaint:"complained",Open:"opened",Click:"clicked",SubscriptionChange:"unknown",Inbound:"unknown"};function F(g){if(typeof g!=="object"||g===null)return[];let f=g,B=typeof f.RecordType==="string"?f.RecordType:"";return[x({provider:"postmark",type:u(B,C),...typeof f.MessageID==="string"?{messageId:f.MessageID}:{},...typeof f.Recipient==="string"?{recipient:f.Recipient}:typeof f.Email==="string"?{recipient:f.Email}:{},...q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)!==void 0?{timestamp:q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)}:{},raw:g})]}export{F as parse};
1
+ import{d as u,e as q,g as x}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var C={Delivery:"delivered",Bounce:"bounced",SpamComplaint:"complained",Open:"opened",Click:"clicked",SubscriptionChange:"unknown",Inbound:"unknown"};function F(g){if(typeof g!=="object"||g===null)return[];let f=g,B=typeof f.RecordType==="string"?f.RecordType:"";return[x({provider:"postmark",type:u(B,C),...typeof f.MessageID==="string"?{messageId:f.MessageID}:{},...typeof f.Recipient==="string"?{recipient:f.Recipient}:typeof f.Email==="string"?{recipient:f.Email}:{},...q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)!==void 0?{timestamp:q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)}:{},raw:g})]}export{F as parse};
2
2
 
3
3
  //# debugId=7CD05BB54D484C3864756E2164756E21
@@ -1,3 +1,3 @@
1
- import{a as $,b as A}from"../chunk-pqp843mh.js";import{d as X,e as Y,g as Z}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var j={"email.sent":"unknown","email.scheduled":"unknown","email.delivered":"delivered","email.delivery_delayed":"deferred","email.bounced":"bounced","email.failed":"bounced","email.complained":"complained","email.opened":"opened","email.clicked":"clicked","email.received":"unknown","email.suppressed":"unknown"};function k(G){let F=G.startsWith("whsec_")?G.slice(6):G,K=atob(F),J=new Uint8Array(K.length);for(let z=0;z<K.length;z++)J[z]=K.charCodeAt(z);return J.buffer}function H(G){if(typeof G!=="object"||G===null)return[];let F=G,K=typeof F.type==="string"?F.type:"",J=typeof F.data==="object"&&F.data!==null?F.data:{},z=J.to,L=Array.isArray(z)?typeof z[0]==="string"?z[0]:void 0:typeof z==="string"?z:void 0,O=Y(F.created_at??J.created_at);return[Z({provider:"resend",type:X(K,j),...typeof J.email_id==="string"?{messageId:J.email_id}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:G})]}async function I(G,F,K){let J=F["svix-id"],z=F["svix-timestamp"],L=F["svix-signature"];if(!J||!z||!L)return!1;let O=`${J}.${z}.${G}`,D=await crypto.subtle.importKey("raw",k(K),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),M=await crypto.subtle.sign("HMAC",D,new TextEncoder().encode(O)),P=new Uint8Array(M);for(let W of L.split(" ")){let Q=W.split(",",2)[1];if(Q===void 0)continue;let U=A(Q);if(U===null)continue;if($(P,U))return!0}return!1}export{I as verifySignature,H as parse};
1
+ import{a as $,b as A}from"../chunk-pqp843mh.js";import{d as X,e as Y,g as Z}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var j={"email.sent":"unknown","email.scheduled":"unknown","email.delivered":"delivered","email.delivery_delayed":"deferred","email.bounced":"bounced","email.failed":"bounced","email.complained":"complained","email.opened":"opened","email.clicked":"clicked","email.received":"unknown","email.suppressed":"unknown"};function k(G){let F=G.startsWith("whsec_")?G.slice(6):G,K=atob(F),J=new Uint8Array(K.length);for(let z=0;z<K.length;z++)J[z]=K.charCodeAt(z);return J.buffer}function H(G){if(typeof G!=="object"||G===null)return[];let F=G,K=typeof F.type==="string"?F.type:"",J=typeof F.data==="object"&&F.data!==null?F.data:{},z=J.to,L=Array.isArray(z)?typeof z[0]==="string"?z[0]:void 0:typeof z==="string"?z:void 0,O=Y(F.created_at??J.created_at);return[Z({provider:"resend",type:X(K,j),...typeof J.email_id==="string"?{messageId:J.email_id}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:G})]}async function I(G,F,K){let J=F["svix-id"],z=F["svix-timestamp"],L=F["svix-signature"];if(!J||!z||!L)return!1;let O=`${J}.${z}.${G}`,D=await crypto.subtle.importKey("raw",k(K),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),M=await crypto.subtle.sign("HMAC",D,new TextEncoder().encode(O)),P=new Uint8Array(M);for(let W of L.split(" ")){let Q=W.split(",",2)[1];if(Q===void 0)continue;let U=A(Q);if(U===null)continue;if($(P,U))return!0}return!1}export{I as verifySignature,H as parse};
2
2
 
3
3
  //# debugId=2EA62D07463E258664756E2164756E21
@@ -1,3 +1,3 @@
1
- import{d as w,e as x,f as B,g as C}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var F={processed:"unknown",delivered:"delivered",deferred:"deferred",bounce:"bounced",dropped:"bounced",spamreport:"complained",unsubscribe:"complained",group_unsubscribe:"complained",group_resubscribe:"unknown",open:"opened",click:"clicked"};function H(q){let g=typeof q.event==="string"?q.event:"";if(g==="bounce"&&q.type==="blocked")return"deferred";return w(g,F)}function K(q){return B(q).flatMap((g)=>{if(typeof g!=="object"||g===null)return[];let f=g,u=x(f.timestamp);return[C({provider:"sendgrid",type:H(f),...typeof f.sg_message_id==="string"?{messageId:f.sg_message_id}:typeof f["smtp-id"]==="string"?{messageId:f["smtp-id"]}:{},...typeof f.email==="string"?{recipient:f.email}:{},...u!==void 0?{timestamp:u}:{},raw:g})]})}export{K as parse};
1
+ import{d as w,e as x,f as B,g as C}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var F={processed:"unknown",delivered:"delivered",deferred:"deferred",bounce:"bounced",dropped:"bounced",spamreport:"complained",unsubscribe:"complained",group_unsubscribe:"complained",group_resubscribe:"unknown",open:"opened",click:"clicked"};function H(q){let g=typeof q.event==="string"?q.event:"";if(g==="bounce"&&q.type==="blocked")return"deferred";return w(g,F)}function K(q){return B(q).flatMap((g)=>{if(typeof g!=="object"||g===null)return[];let f=g,u=x(f.timestamp);return[C({provider:"sendgrid",type:H(f),...typeof f.sg_message_id==="string"?{messageId:f.sg_message_id}:typeof f["smtp-id"]==="string"?{messageId:f["smtp-id"]}:{},...typeof f.email==="string"?{recipient:f.email}:{},...u!==void 0?{timestamp:u}:{},raw:g})]})}export{K as parse};
2
2
 
3
3
  //# debugId=D4C7DDEE719D449764756E2164756E21
@@ -1,3 +1,3 @@
1
- import{d as Q,e as U,g as X}from"../chunk-q8k3m6qe.js";import"../chunk-nbmq8ejv.js";var $={Delivery:"delivered",Bounce:"bounced",Complaint:"complained",Open:"opened",Click:"clicked",DeliveryDelay:"deferred",Send:"unknown",Reject:"bounced","Rendering Failure":"bounced",Subscription:"unknown"};function h(q){if(typeof q.eventType==="string")return q.eventType;if(typeof q.notificationType==="string")return q.notificationType;return""}function j(q){let J=typeof q.delivery==="object"&&q.delivery!==null?q.delivery:void 0;if(J&&Array.isArray(J.recipients)&&typeof J.recipients[0]==="string")return J.recipients[0];let K=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0;if(K&&Array.isArray(K.bouncedRecipients)){let I=K.bouncedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let B=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0;if(B&&Array.isArray(B.complainedRecipients)){let I=B.complainedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let G=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0;if(Array.isArray(G?.destination)&&typeof G.destination[0]==="string")return G.destination[0];return}function w(q){let J=h(q),K=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0,B=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0,G=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0,I=typeof q.open==="object"&&q.open!==null?q.open:void 0,Y=typeof q.click==="object"&&q.click!==null?q.click:void 0,Z=typeof q.deliveryDelay==="object"&&q.deliveryDelay!==null?q.deliveryDelay:void 0,L=j(q),O=U(q.timestamp??B?.timestamp??G?.timestamp??I?.timestamp??Y?.timestamp??Z?.timestamp??K?.timestamp);return[X({provider:"ses",type:Q(J,$),...typeof K?.messageId==="string"?{messageId:K.messageId}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:q})]}function z(q){if(typeof q!=="object"||q===null)return[];let J=q,K=typeof J.Type==="string"?J.Type:"";if(K==="SubscriptionConfirmation")return[];if(K!=="Notification")return[];let B=J.Message,G;if(typeof B==="string")try{G=JSON.parse(B)}catch{return[{provider:"ses",type:"unknown",raw:q}]}else if(typeof B==="object"&&B!==null)G=B;else return[];if(typeof G!=="object"||G===null)return[{provider:"ses",type:"unknown",raw:q}];return w(G)}export{z as parse};
1
+ import{d as Q,e as U,g as X}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var $={Delivery:"delivered",Bounce:"bounced",Complaint:"complained",Open:"opened",Click:"clicked",DeliveryDelay:"deferred",Send:"unknown",Reject:"bounced","Rendering Failure":"bounced",Subscription:"unknown"};function h(q){if(typeof q.eventType==="string")return q.eventType;if(typeof q.notificationType==="string")return q.notificationType;return""}function j(q){let J=typeof q.delivery==="object"&&q.delivery!==null?q.delivery:void 0;if(J&&Array.isArray(J.recipients)&&typeof J.recipients[0]==="string")return J.recipients[0];let K=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0;if(K&&Array.isArray(K.bouncedRecipients)){let I=K.bouncedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let B=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0;if(B&&Array.isArray(B.complainedRecipients)){let I=B.complainedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let G=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0;if(Array.isArray(G?.destination)&&typeof G.destination[0]==="string")return G.destination[0];return}function w(q){let J=h(q),K=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0,B=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0,G=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0,I=typeof q.open==="object"&&q.open!==null?q.open:void 0,Y=typeof q.click==="object"&&q.click!==null?q.click:void 0,Z=typeof q.deliveryDelay==="object"&&q.deliveryDelay!==null?q.deliveryDelay:void 0,L=j(q),O=U(q.timestamp??B?.timestamp??G?.timestamp??I?.timestamp??Y?.timestamp??Z?.timestamp??K?.timestamp);return[X({provider:"ses",type:Q(J,$),...typeof K?.messageId==="string"?{messageId:K.messageId}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:q})]}function z(q){if(typeof q!=="object"||q===null)return[];let J=q,K=typeof J.Type==="string"?J.Type:"";if(K==="SubscriptionConfirmation")return[];if(K!=="Notification")return[];let B=J.Message,G;if(typeof B==="string")try{G=JSON.parse(B)}catch{return[{provider:"ses",type:"unknown",raw:q}]}else if(typeof B==="object"&&B!==null)G=B;else return[];if(typeof G!=="object"||G===null)return[{provider:"ses",type:"unknown",raw:q}];return w(G)}export{z as parse};
2
2
 
3
3
  //# debugId=C5FC4A2C6082819B64756E2164756E21
@@ -0,0 +1,16 @@
1
+ import type { EmailEvent } from "./types.js";
2
+ /** Parse an SNDR webhook payload into normalized {@link EmailEvent} records. */
3
+ export declare function parse(payload: unknown): EmailEvent[];
4
+ /**
5
+ * Verify an SNDR webhook signature using HMAC-SHA256 (Web Crypto).
6
+ *
7
+ * Pass the **exact** raw request body string — re-serializing JSON breaks the HMAC.
8
+ *
9
+ * @param rawBody — Raw HTTP body bytes as UTF-8 string (or the string used when signing).
10
+ * @param signatureHeader — Value of the `X-Sndr-Signature` header (`t=…,v1=…`).
11
+ * @param secret — Webhook endpoint signing secret from SNDR.
12
+ * @param opts.toleranceSeconds — Max age of `t` in seconds (default `300`). Pass `0` to skip.
13
+ */
14
+ export declare function verifySignature(rawBody: string, signatureHeader: string, secret: string, opts?: {
15
+ toleranceSeconds?: number;
16
+ }): Promise<boolean>;
@@ -0,0 +1,3 @@
1
+ import{a as W,c as _}from"../chunk-pqp843mh.js";import{d as U,e as X,g as j}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var x={"email.delivered":"delivered","email.bounced":"bounced","email.unsubscribed":"unknown"};function P(K){if(typeof K!=="object"||K===null)return[];let I=K,z=typeof I.type==="string"?I.type:"",L=typeof I.data==="object"&&I.data!==null?I.data:{},G=L.to??L.addresses,Q=Array.isArray(G)?typeof G[0]==="string"?G[0]:void 0:typeof G==="string"?G:void 0,Z=X(I.created_at);return[j({provider:"sndr",type:U(z,x),...typeof L.email_id==="string"?{messageId:L.email_id}:{},...Q!==void 0?{recipient:Q}:{},...Z!==void 0?{timestamp:Z}:{},raw:K})]}async function R(K,I,z,L){if(!I||!z)return!1;let G,Q;for(let Y of I.split(",")){let $=Y.indexOf("=");if($<0)continue;let J=Y.slice(0,$).trim(),O=Y.slice($+1).trim();if(J==="t")G=O;else if(J==="v1")Q=O}if(!G||!Q)return!1;let Z=L?.toleranceSeconds??300;if(Z>0){let Y=Number(G);if(!Number.isFinite(Y))return!1;let $=Math.floor(Date.now()/1000);if(Math.abs($-Y)>Z)return!1}let F=_(Q);if(F===null)return!1;let C=await crypto.subtle.importKey("raw",new TextEncoder().encode(z),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),D=new TextEncoder().encode(`${G}.${K}`),V=await crypto.subtle.sign("HMAC",C,D);return W(new Uint8Array(V),F)}export{R as verifySignature,P as parse};
2
+
3
+ //# debugId=7647757468D3D2B664756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/webhooks/sndr.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * SNDR webhook parser and signature verification.\n *\n * Signature scheme (from `@rkiza/sndr` / SNDR dispatcher):\n * `X-Sndr-Signature: t=<unix_seconds>,v1=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`\n */\nimport { decodeHexBytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst SNDR_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n \"email.delivered\": \"delivered\",\n \"email.bounced\": \"bounced\",\n \"email.unsubscribed\": \"unknown\",\n};\n\n/** Parse an SNDR webhook payload into normalized {@link EmailEvent} records. */\nexport function parse(payload: unknown): EmailEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const record = payload as Record<string, unknown>;\n const type = typeof record.type === \"string\" ? record.type : \"\";\n const data =\n typeof record.data === \"object\" && record.data !== null\n ? (record.data as Record<string, unknown>)\n : {};\n\n const to = data.to ?? data.addresses;\n const recipient = Array.isArray(to)\n ? typeof to[0] === \"string\"\n ? to[0]\n : undefined\n : typeof to === \"string\"\n ? to\n : undefined;\n\n const timestamp = parseTimestamp(record.created_at);\n\n return [\n emailEvent({\n provider: \"sndr\",\n type: mapEventType(type, SNDR_EVENT_MAP),\n ...(typeof data.email_id === \"string\" ? { messageId: data.email_id } : {}),\n ...(recipient !== undefined ? { recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n raw: payload,\n }),\n ];\n}\n\n/**\n * Verify an SNDR webhook signature using HMAC-SHA256 (Web Crypto).\n *\n * Pass the **exact** raw request body string — re-serializing JSON breaks the HMAC.\n *\n * @param rawBody — Raw HTTP body bytes as UTF-8 string (or the string used when signing).\n * @param signatureHeader — Value of the `X-Sndr-Signature` header (`t=…,v1=…`).\n * @param secret — Webhook endpoint signing secret from SNDR.\n * @param opts.toleranceSeconds — Max age of `t` in seconds (default `300`). Pass `0` to skip.\n */\nexport async function verifySignature(\n rawBody: string,\n signatureHeader: string,\n secret: string,\n opts?: { toleranceSeconds?: number },\n): Promise<boolean> {\n if (!signatureHeader || !secret) {\n return false;\n }\n\n let timestamp: string | undefined;\n let v1: string | undefined;\n for (const part of signatureHeader.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === \"t\") timestamp = value;\n else if (key === \"v1\") v1 = value;\n }\n\n if (!timestamp || !v1) {\n return false;\n }\n\n const tolerance = opts?.toleranceSeconds ?? 300;\n if (tolerance > 0) {\n const tsNum = Number(timestamp);\n if (!Number.isFinite(tsNum)) {\n return false;\n }\n const nowSec = Math.floor(Date.now() / 1000);\n if (Math.abs(nowSec - tsNum) > tolerance) {\n return false;\n }\n }\n\n const providedBytes = decodeHexBytes(v1);\n if (providedBytes === null) {\n return false;\n }\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const signed = new TextEncoder().encode(`${timestamp}.${rawBody}`);\n const expected = await crypto.subtle.sign(\"HMAC\", key, signed);\n return timingSafeEqual(new Uint8Array(expected), providedBytes);\n}\n"
6
+ ],
7
+ "mappings": "8IAWA,DAAM,HAAqD,CACzD,kBAAmB,YACnB,gBAAiB,UACjB,qBAAsB,SACxB,EAGO,SAAS,CAAK,CAAC,EAAgC,CACpD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAGV,IAAM,EAAS,EACT,EAAO,OAAO,EAAO,OAAS,SAAW,EAAO,KAAO,GACvD,EACJ,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,KAC9C,EAAO,KACR,CAAC,EAED,EAAK,EAAK,IAAM,EAAK,UACrB,EAAY,MAAM,QAAQ,CAAE,EAC9B,OAAO,EAAG,KAAO,SACf,EAAG,GACH,OACF,OAAO,IAAO,SACZ,EACA,OAEA,EAAY,EAAe,EAAO,UAAU,EAElD,MAAO,CACL,EAAW,CACT,SAAU,OACV,KAAM,EAAa,EAAM,CAAc,KACnC,OAAO,EAAK,WAAa,SAAW,CAAE,UAAW,EAAK,QAAS,EAAI,CAAC,KACpE,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,EAC/C,IAAK,CACP,CAAC,CACH,EAaF,eAAsB,CAAe,CACnC,EACA,EACA,EACA,EACkB,CAClB,GAAI,CAAC,GAAmB,CAAC,EACvB,MAAO,GAGT,IAAI,EACA,EACJ,QAAW,KAAQ,EAAgB,MAAM,GAAG,EAAG,CAC7C,IAAM,EAAK,EAAK,QAAQ,GAAG,EAC3B,GAAI,EAAK,EAAG,SACZ,IAAM,EAAM,EAAK,MAAM,EAAG,CAAE,EAAE,KAAK,EAC7B,EAAQ,EAAK,MAAM,EAAK,CAAC,EAAE,KAAK,EACtC,GAAI,IAAQ,IAAK,EAAY,EACxB,QAAI,IAAQ,KAAM,EAAK,EAG9B,GAAI,CAAC,GAAa,CAAC,EACjB,MAAO,GAGT,IAAM,EAAY,GAAM,kBAAoB,IAC5C,GAAI,EAAY,EAAG,CACjB,IAAM,EAAQ,OAAO,CAAS,EAC9B,GAAI,CAAC,OAAO,SAAS,CAAK,EACxB,MAAO,GAET,IAAM,EAAS,KAAK,MAAM,KAAK,IAAI,EAAI,IAAI,EAC3C,GAAI,KAAK,IAAI,EAAS,CAAK,EAAI,EAC7B,MAAO,GAIX,IAAM,EAAgB,EAAe,CAAE,EACvC,GAAI,IAAkB,KACpB,MAAO,GAGT,IAAM,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,IAAI,YAAY,EAAE,OAAO,CAAM,EAC/B,CAAE,KAAM,OAAQ,KAAM,SAAU,EAChC,GACA,CAAC,MAAM,CACT,EAEM,EAAS,IAAI,YAAY,EAAE,OAAO,GAAG,KAAa,GAAS,EAC3D,EAAW,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,CAAM,EAC7D,OAAO,EAAgB,IAAI,WAAW,CAAQ,EAAG,CAAa",
8
+ "debugId": "7647757468D3D2B664756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,3 +1,3 @@
1
- import{a,b,c}from"../chunk-pqp843mh.js";import"../chunk-nbmq8ejv.js";export{a as timingSafeEqual,c as decodeHexBytes,b as decodeBase64Bytes};
1
+ import{a,b,c}from"../chunk-pqp843mh.js";import"../chunk-6npp3x3c.js";export{a as timingSafeEqual,c as decodeHexBytes,b as decodeBase64Bytes};
2
2
 
3
3
  //# debugId=CA7D50FE7388E14864756E2164756E21
@@ -5,11 +5,17 @@
5
5
  * Pure parsing only; no server framework required. Signature verification
6
6
  * helpers are optional and never required to call `parse`.
7
7
  *
8
+ * Prefer per-provider subpaths for the smallest runtime graph
9
+ * (`sently/webhooks/resend`, `sently/webhooks/sndr`, …). This barrel re-exports
10
+ * every parser for convenience — bundlers tree-shake unused named imports, but
11
+ * Node/Deno without a bundler evaluate every re-export.
12
+ *
8
13
  * @example
9
14
  * ```ts
10
- * import { parseResendWebhook } from "sently/webhooks";
15
+ * import { parse } from "sently/webhooks/resend";
16
+ * // or: import { parseResendWebhook } from "sently/webhooks";
11
17
  *
12
- * const events = parseResendWebhook(await request.json());
18
+ * const events = parse(await request.json());
13
19
  * for (const event of events) {
14
20
  * if (event.type === "bounced") {
15
21
  * // handle bounce
@@ -23,4 +29,5 @@ export { parse as parsePostmarkWebhook } from "./webhooks/postmark.js";
23
29
  export { parse as parseResendWebhook, verifySignature as verifyResendSignature, } from "./webhooks/resend.js";
24
30
  export { parse as parseSendGridWebhook } from "./webhooks/sendgrid.js";
25
31
  export { parse as parseSesWebhook } from "./webhooks/ses.js";
32
+ export { parse as parseSndrWebhook, verifySignature as verifySndrSignature, } from "./webhooks/sndr.js";
26
33
  export type { EmailEvent } from "./webhooks/types.js";
package/dist/webhooks.js CHANGED
@@ -11,3 +11,7 @@ export {
11
11
  } from "./webhooks/resend.js";
12
12
  export { parse as parseSendGridWebhook } from "./webhooks/sendgrid.js";
13
13
  export { parse as parseSesWebhook } from "./webhooks/ses.js";
14
+ export {
15
+ parse as parseSndrWebhook,
16
+ verifySignature as verifySndrSignature,
17
+ } from "./webhooks/sndr.js";
@@ -0,0 +1,26 @@
1
+ import type { VerifyResult } from "./core/types.js";
2
+ import type { WhatsAppHooks, WhatsAppOptions, WhatsAppPlugin, WhatsAppSendResult, WhatsAppTransport } from "./core/whatsapp-types.js";
3
+ /** Configuration for {@link createWhatsAppSender}. */
4
+ export interface WhatsAppSenderConfig {
5
+ /** WhatsApp delivery transport. */
6
+ transport: WhatsAppTransport;
7
+ /** Optional plugins run before each send. */
8
+ plugins?: WhatsAppPlugin[];
9
+ /** Optional lifecycle hooks for metrics and tracing. */
10
+ hooks?: WhatsAppHooks;
11
+ }
12
+ /** WhatsApp sender returned by {@link createWhatsAppSender}. */
13
+ export interface WhatsAppSender {
14
+ /** Send a WhatsApp message through the configured transport. */
15
+ send(options: WhatsAppOptions): Promise<WhatsAppSendResult>;
16
+ /** Test connectivity and credentials without sending. */
17
+ verify(): Promise<VerifyResult>;
18
+ /** Release resources held by the transport. */
19
+ close(): Promise<void>;
20
+ }
21
+ /**
22
+ * Create a WhatsApp sender that wraps a {@link WhatsAppTransport}.
23
+ *
24
+ * Pipeline: plugins → onSend → transport.send → onSuccess / onError.
25
+ */
26
+ export declare function createWhatsAppSender(config: WhatsAppSenderConfig): WhatsAppSender;
@@ -0,0 +1,3 @@
1
+ import{W as z,X as I}from"./chunk-qtkd5bak.js";import"./chunk-6npp3x3c.js";function M(y,m){return{...y.messageId!==void 0?{messageId:y.messageId}:{},to:y.to,provider:m.provider??"whatsapp"}}function Q(y){let{transport:m,plugins:J,hooks:B}=y;return{async send(K){let F=await I(K,J),D=M(F,m);await z(B?.onSend,D);let G=performance.now();try{let q=await m.send(F),L={...D,messageId:q.messageId};return await z(B?.onSuccess,L,q,performance.now()-G),q}catch(q){throw await z(B?.onError,D,q,performance.now()-G),q}},async verify(){if(m.verify)return m.verify();return{ok:!0,provider:"whatsapp"}},async close(){if(m.close)await m.close()}}}export{Q as createWhatsAppSender};
2
+
3
+ //# debugId=A4F982D22B2A1BB164756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/whatsapp.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * WhatsApp sender orchestrator — plugins, hooks, and transport.send pipeline.\n *\n * Sently-first: apps call {@link createWhatsAppSender}; providers implement\n * {@link WhatsAppTransport}. Multi-product vendors get a separate WhatsApp\n * transport (e.g. future `taqnyat-whatsapp`), not a mega vendor client.\n *\n * @example\n * ```ts\n * import { createWhatsAppSender } from \"sently/whatsapp\";\n * import { WhatsAppCloudTransport } from \"sently/transports/whatsapp-cloud\";\n *\n * const wa = createWhatsAppSender({\n * transport: new WhatsAppCloudTransport({\n * accessToken: \"...\",\n * phoneNumberId: \"...\",\n * }),\n * });\n *\n * await wa.send({ to: \"15551234567\", text: \"Hello\" });\n * ```\n */\nimport { invokeHook } from \"./core/hooks.js\";\nimport { runPlugins } from \"./core/plugin.js\";\nimport type { VerifyResult } from \"./core/types.js\";\nimport type {\n WhatsAppHookContext,\n WhatsAppHooks,\n WhatsAppOptions,\n WhatsAppPlugin,\n WhatsAppSendResult,\n WhatsAppTransport,\n} from \"./core/whatsapp-types.js\";\n\n/** Configuration for {@link createWhatsAppSender}. */\nexport interface WhatsAppSenderConfig {\n /** WhatsApp delivery transport. */\n transport: WhatsAppTransport;\n /** Optional plugins run before each send. */\n plugins?: WhatsAppPlugin[];\n /** Optional lifecycle hooks for metrics and tracing. */\n hooks?: WhatsAppHooks;\n}\n\n/** WhatsApp sender returned by {@link createWhatsAppSender}. */\nexport interface WhatsAppSender {\n /** Send a WhatsApp message through the configured transport. */\n send(options: WhatsAppOptions): Promise<WhatsAppSendResult>;\n /** Test connectivity and credentials without sending. */\n verify(): Promise<VerifyResult>;\n /** Release resources held by the transport. */\n close(): Promise<void>;\n}\n\nfunction buildWhatsAppHookContext(\n options: WhatsAppOptions,\n transport: WhatsAppTransport,\n): WhatsAppHookContext {\n return {\n ...(options.messageId !== undefined ? { messageId: options.messageId } : {}),\n to: options.to,\n provider: transport.provider ?? \"whatsapp\",\n };\n}\n\n/**\n * Create a WhatsApp sender that wraps a {@link WhatsAppTransport}.\n *\n * Pipeline: plugins → onSend → transport.send → onSuccess / onError.\n */\nexport function createWhatsAppSender(config: WhatsAppSenderConfig): WhatsAppSender {\n const { transport, plugins, hooks } = config;\n\n return {\n async send(options: WhatsAppOptions): Promise<WhatsAppSendResult> {\n const processed = await runPlugins(options, plugins);\n const ctx = buildWhatsAppHookContext(processed, transport);\n\n await invokeHook(hooks?.onSend, ctx);\n\n const start = performance.now();\n\n try {\n const result = await transport.send(processed);\n const successCtx: WhatsAppHookContext = {\n ...ctx,\n messageId: result.messageId,\n };\n await invokeHook(hooks?.onSuccess, successCtx, result, performance.now() - start);\n return result;\n } catch (error) {\n await invokeHook(hooks?.onError, ctx, error, performance.now() - start);\n throw error;\n }\n },\n\n async verify(): Promise<VerifyResult> {\n if (transport.verify) {\n return transport.verify();\n }\n return { ok: true, provider: \"whatsapp\" };\n },\n\n async close(): Promise<void> {\n if (transport.close) {\n await transport.close();\n }\n },\n };\n}\n"
6
+ ],
7
+ "mappings": "iFAuDA,GAAS,CAAwB,CAC/B,EACA,EACqB,CACrB,MAAO,IACD,EAAQ,YAAc,OAAY,CAAE,UAAW,EAAQ,SAAU,EAAI,CAAC,EAC1E,GAAI,EAAQ,GACZ,SAAU,EAAU,UAAY,UAClC,EAQK,SAAS,CAAoB,CAAC,EAA8C,CACjF,IAAQ,YAAW,UAAS,SAAU,EAEtC,MAAO,MACC,KAAI,CAAC,EAAuD,CAChE,IAAM,EAAY,MAAM,EAAW,EAAS,CAAO,EAC7C,EAAM,EAAyB,EAAW,CAAS,EAEzD,MAAM,EAAW,GAAO,OAAQ,CAAG,EAEnC,IAAM,EAAQ,YAAY,IAAI,EAE9B,GAAI,CACF,IAAM,EAAS,MAAM,EAAU,KAAK,CAAS,EACvC,EAAkC,IACnC,EACH,UAAW,EAAO,SACpB,EAEA,OADA,MAAM,EAAW,GAAO,UAAW,EAAY,EAAQ,YAAY,IAAI,EAAI,CAAK,EACzE,EACP,MAAO,EAAO,CAEd,MADA,MAAM,EAAW,GAAO,QAAS,EAAK,EAAO,YAAY,IAAI,EAAI,CAAK,EAChE,SAIJ,OAAM,EAA0B,CACpC,GAAI,EAAU,OACZ,OAAO,EAAU,OAAO,EAE1B,MAAO,CAAE,GAAI,GAAM,SAAU,UAAW,QAGpC,MAAK,EAAkB,CAC3B,GAAI,EAAU,MACZ,MAAM,EAAU,MAAM,EAG5B",
8
+ "debugId": "A4F982D22B2A1BB164756E2164756E21",
9
+ "names": []
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sently",
3
- "version": "0.8.0",
4
- "description": "Runtime-agnostic email library for Node.js, Bun, Deno, and Cloudflare Workers. ESM-native Nodemailer alternative with zero dependencies.",
3
+ "version": "0.9.1",
4
+ "description": "Runtime-agnostic messaging library for Node.js, Bun, Deno, and Cloudflare Workers. Channel-first email, SMS, WhatsApp, and push with pluggable provider transports.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.js",
@@ -10,12 +10,17 @@
10
10
  "dist",
11
11
  "README.md",
12
12
  "LICENSE",
13
- "CHANGELOG.md"
13
+ "CHANGELOG.md",
14
+ "AGENTS.md",
15
+ "site/content/docs"
14
16
  ],
15
17
  "publishConfig": {
16
18
  "access": "public",
17
19
  "registry": "https://registry.npmjs.org"
18
20
  },
21
+ "workspaces": [
22
+ "site"
23
+ ],
19
24
  "scripts": {
20
25
  "build": "bun run build.ts",
21
26
  "test": "bun test",
@@ -24,6 +29,8 @@
24
29
  "format": "biome format src --write",
25
30
  "typecheck": "tsc --noEmit",
26
31
  "mcp": "bun tools/mcp/index.ts",
32
+ "site:dev": "bun run --cwd site dev",
33
+ "site:build": "bun run --cwd site build",
27
34
  "verify": "bun run lint && bun run typecheck && bun run build && bun test",
28
35
  "prepack": "bun run verify",
29
36
  "pack:npm": "npm pack --dry-run --cache ./.npm-cache",
@@ -36,12 +43,14 @@
36
43
  "verify:publish": "bun run pack:npm",
37
44
  "measure:size": "bun tools/measure-bundle-size.ts",
38
45
  "measure:size:md": "bun tools/measure-bundle-size.ts --markdown",
39
- "check:size": "bun tools/measure-bundle-size.ts --check"
46
+ "check:size": "bun tools/measure-bundle-size.ts --check",
47
+ "g": "gflows"
40
48
  },
41
49
  "devDependencies": {
42
50
  "@biomejs/biome": "2.4.16",
43
51
  "@modelcontextprotocol/sdk": "1.29.0",
44
52
  "@react-email/render": "1.1.2",
53
+ "gflows": "1.2.1",
45
54
  "@types/node": "25.9.1",
46
55
  "@types/react": "19.2.15",
47
56
  "react": "19.2.6",
@@ -174,6 +183,38 @@
174
183
  "import": "./dist/transports/loops.js",
175
184
  "types": "./dist/transports/loops.d.ts"
176
185
  },
186
+ "./transports/sndr": {
187
+ "import": "./dist/transports/sndr.js",
188
+ "types": "./dist/transports/sndr.d.ts"
189
+ },
190
+ "./transports/twilio-sms": {
191
+ "import": "./dist/transports/twilio-sms.js",
192
+ "types": "./dist/transports/twilio-sms.d.ts"
193
+ },
194
+ "./transports/taqnyat-sms": {
195
+ "import": "./dist/transports/taqnyat-sms.js",
196
+ "types": "./dist/transports/taqnyat-sms.d.ts"
197
+ },
198
+ "./transports/taqnyat-whatsapp": {
199
+ "import": "./dist/transports/taqnyat-whatsapp.js",
200
+ "types": "./dist/transports/taqnyat-whatsapp.d.ts"
201
+ },
202
+ "./transports/taqnyat-mail": {
203
+ "import": "./dist/transports/taqnyat-mail.js",
204
+ "types": "./dist/transports/taqnyat-mail.d.ts"
205
+ },
206
+ "./transports/msegat": {
207
+ "import": "./dist/transports/msegat.js",
208
+ "types": "./dist/transports/msegat.d.ts"
209
+ },
210
+ "./transports/whatsapp-cloud": {
211
+ "import": "./dist/transports/whatsapp-cloud.js",
212
+ "types": "./dist/transports/whatsapp-cloud.d.ts"
213
+ },
214
+ "./transports/webpush": {
215
+ "import": "./dist/transports/webpush.js",
216
+ "types": "./dist/transports/webpush.d.ts"
217
+ },
177
218
  "./observability": {
178
219
  "import": "./dist/observability/console.js",
179
220
  "types": "./dist/observability/console.d.ts"
@@ -194,6 +235,18 @@
194
235
  "import": "./dist/mailer.js",
195
236
  "types": "./dist/mailer.d.ts"
196
237
  },
238
+ "./sms": {
239
+ "import": "./dist/sms.js",
240
+ "types": "./dist/sms.d.ts"
241
+ },
242
+ "./whatsapp": {
243
+ "import": "./dist/whatsapp.js",
244
+ "types": "./dist/whatsapp.d.ts"
245
+ },
246
+ "./push": {
247
+ "import": "./dist/push.js",
248
+ "types": "./dist/push.d.ts"
249
+ },
197
250
  "./smtp": {
198
251
  "import": "./dist/smtp-mailer.js",
199
252
  "types": "./dist/smtp-mailer.d.ts"
@@ -214,6 +267,34 @@
214
267
  "import": "./dist/webhooks.js",
215
268
  "types": "./dist/webhooks.d.ts"
216
269
  },
270
+ "./webhooks/brevo": {
271
+ "import": "./dist/webhooks/brevo.js",
272
+ "types": "./dist/webhooks/brevo.d.ts"
273
+ },
274
+ "./webhooks/mailgun": {
275
+ "import": "./dist/webhooks/mailgun.js",
276
+ "types": "./dist/webhooks/mailgun.d.ts"
277
+ },
278
+ "./webhooks/postmark": {
279
+ "import": "./dist/webhooks/postmark.js",
280
+ "types": "./dist/webhooks/postmark.d.ts"
281
+ },
282
+ "./webhooks/resend": {
283
+ "import": "./dist/webhooks/resend.js",
284
+ "types": "./dist/webhooks/resend.d.ts"
285
+ },
286
+ "./webhooks/sendgrid": {
287
+ "import": "./dist/webhooks/sendgrid.js",
288
+ "types": "./dist/webhooks/sendgrid.d.ts"
289
+ },
290
+ "./webhooks/ses": {
291
+ "import": "./dist/webhooks/ses.js",
292
+ "types": "./dist/webhooks/ses.d.ts"
293
+ },
294
+ "./webhooks/sndr": {
295
+ "import": "./dist/webhooks/sndr.js",
296
+ "types": "./dist/webhooks/sndr.d.ts"
297
+ },
217
298
  "./errors": {
218
299
  "import": "./dist/errors.js",
219
300
  "types": "./dist/errors.d.ts"
package/site/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # sently docs site
2
+
3
+ Next.js + [Fumadocs](https://fumadocs.dev) documentation site.
4
+
5
+ - Handbook content lives under `content/docs/`.
6
+ - **Changelog** (`/changelog`) is derived from the root `CHANGELOG.md`.
7
+
8
+ ## Commands
9
+
10
+ ```bash
11
+ bun run dev # Next development server
12
+ bun run build # static export → out/
13
+ bun run start # serve out/
14
+ ```
15
+
16
+ From the monorepo root:
17
+
18
+ ```bash
19
+ bun run site:dev
20
+ bun run site:build
21
+ ```
22
+
23
+ ## Deployment
24
+
25
+ `next.config.mjs` sets `output: 'export'`. The build produces a static `out/`
26
+ directory — host it anywhere.
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: AI integration
3
+ description: Machine-readable documentation and the local MCP server.
4
+ icon: FolderOpen
5
+ source: "README.md"
6
+ ---
7
+
8
+ Machine-readable documentation and the local MCP server.
9
+
10
+ <Cards>
11
+ <Card title="llms.txt" href="/docs/ai/llms-txt" />
12
+ <Card title="MCP" href="/docs/ai/mcp" />
13
+ </Cards>
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: llms.txt
3
+ description: Use the docs site machine-readable indexes for agent context.
4
+ icon: FileText
5
+ source: "site/app/llms.txt/route.ts"
6
+ ---
7
+
8
+ The docs site serves `/llms.txt` and `/llms-full.txt` for concise and complete machine-readable documentation.
9
+
10
+ Use `/llms.txt` for navigation and `/llms-full.txt` when an agent needs page content.
11
+
12
+ <Cards><Card title="MCP" href="/docs/ai/mcp" /></Cards>
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: MCP
3
+ description: Run the local Model Context Protocol server for sently tooling.
4
+ icon: Server
5
+ source: "tools/mcp/index.ts"
6
+ ---
7
+
8
+ The repository exposes a local MCP server through the `bun run mcp` script.
9
+
10
+ ```bash
11
+ bun run mcp
12
+ ```
13
+
14
+ Consult the server tool descriptions after connecting; only invoke tools that the connected server advertises.
15
+
16
+ <Cards><Card title="llms.txt" href="/docs/ai/llms-txt" /></Cards>
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "AI",
3
+ "icon": "Bot",
4
+ "pages": [
5
+ "index",
6
+ "llms-txt",
7
+ "mcp"
8
+ ]
9
+ }
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Email
3
+ description: Compose and deliver email through a mailer and a transport.
4
+ icon: Mail
5
+ source: "src/mailer.ts"
6
+ ---
7
+
8
+ Use the email channel for receipts, invitations, and other messages sent by your application.
9
+ A mailer owns the send pipeline; a transport delivers each message through SMTP or an email API.
10
+
11
+ <Callout title="The one rule">Create the mailer once with a transport, then `await` `createMailer` before sending.</Callout>
12
+
13
+ <Steps>
14
+ <Step title="Create a transport-backed mailer">
15
+
16
+ ```ts
17
+ import { createMailer } from "sently/mailer";
18
+ import { ResendTransport } from "sently/transports/resend";
19
+
20
+ const mailer = await createMailer({
21
+ transport: new ResendTransport({
22
+ apiKey: process.env.RESEND_API_KEY!,
23
+ }),
24
+ });
25
+ ```
26
+
27
+ </Step>
28
+ <Step title="Send an email">
29
+
30
+ ```ts
31
+ const result = await mailer.send({
32
+ from: "Acme <hello@example.com>",
33
+ to: "person@example.com",
34
+ subject: "Welcome",
35
+ text: "Thanks for joining Acme.",
36
+ });
37
+
38
+ console.log(result.messageId);
39
+ ```
40
+
41
+ </Step>
42
+ </Steps>
43
+
44
+ ## Mailer configuration
45
+
46
+ | Option | Type | Meaning |
47
+ | --- | --- | --- |
48
+ | `transport` | `Transport` | Required delivery implementation, such as `ResendTransport`. |
49
+ | `plugins` | `MailPlugin[]` | Optional transforms run before delivery. |
50
+ | `hooks` | `MailerHooks` | Optional callbacks for send activity. |
51
+
52
+ ## Message options
53
+
54
+ | Option | Type | Meaning |
55
+ | --- | --- | --- |
56
+ | `from` | `AddressInput` | Required sender address. |
57
+ | `to` | `AddressInput` | Required recipient or recipients. |
58
+ | `subject` | `string` | Required message subject. |
59
+ | `text` / `html` | `string` | Plain-text or HTML body. |
60
+ | `attachments` | `Attachment[]` | Optional in-memory or supported-runtime file attachments. |
61
+ | `cc`, `bcc`, `replyTo` | `AddressInput` | Optional recipient fields. |
62
+
63
+ For SMTP relay configuration, use the asynchronous `createSMTPMailer` factory instead.
64
+
65
+ ```ts
66
+ import { createSMTPMailer } from "sently/smtp";
67
+
68
+ const mailer = await createSMTPMailer({
69
+ host: "smtp.example.com",
70
+ port: 587,
71
+ auth: { user: "you@example.com", pass: process.env.SMTP_PASSWORD! },
72
+ });
73
+ ```
74
+
75
+ ## Troubleshooting
76
+
77
+ <Accordions>
78
+ <Accordion title="Why does createMailer reject host and port?">
79
+ `createMailer` only accepts an explicit transport. Use `createSMTPMailer` when your configuration starts with an SMTP host and port.
80
+ </Accordion>
81
+ <Accordion title="Can I change email providers?">
82
+ Yes. Keep calls to `mailer.send` and create the mailer with a different email transport.
83
+ </Accordion>
84
+ <Accordion title="What does verify do?">
85
+ `mailer.verify()` delegates to the transport when it provides `verify`; otherwise it reports a successful mailer result.
86
+ </Accordion>
87
+ </Accordions>
88
+
89
+ ## Learn more
90
+
91
+ - [Email options](../reference/mail-options)
92
+ - [Transport contracts](../reference/transport-contracts)
93
+ - [Hooks](./hooks)
94
+
95
+ ## Next
96
+
97
+ <Cards>
98
+ <Card title="Transports" href="/docs/transports" />
99
+ <Card title="Send in bulk" href="/docs/guides/send-bulk" />
100
+ </Cards>
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Lifecycle hooks
3
+ description: Observe sender activity without putting message bodies in hook context.
4
+ icon: Activity
5
+ source: "src/core/hooks.ts"
6
+ ---
7
+
8
+ Hooks observe sends for metrics and tracing without coupling your application to a provider.
9
+ Their contexts include delivery metadata, not a message body.
10
+
11
+ <Callout title="The one rule">Do not log message bodies in hooks; their contexts intentionally expose only delivery metadata.</Callout>
12
+
13
+ <Steps>
14
+ <Step title="Configure hooks when creating the sender">
15
+
16
+ ```ts
17
+ import { createMailer } from "sently/mailer";
18
+ import { ResendTransport } from "sently/transports/resend";
19
+
20
+ const mailer = await createMailer({
21
+ transport: new ResendTransport({
22
+ apiKey: process.env.RESEND_API_KEY!,
23
+ }),
24
+ hooks: {
25
+ onSuccess: ({ provider, subject, to }, result, durationMs) => {
26
+ console.log(provider, subject, result.messageId, durationMs);
27
+ console.log(to);
28
+ },
29
+ },
30
+ });
31
+ ```
32
+
33
+ </Step>
34
+ <Step title="Send normally">
35
+
36
+ ```ts
37
+ await mailer.send({
38
+ from: "hello@example.com",
39
+ to: "person@example.com",
40
+ subject: "Welcome",
41
+ text: "Thanks for joining.",
42
+ });
43
+ ```
44
+
45
+ </Step>
46
+ </Steps>
47
+
48
+ ## Hook callbacks
49
+
50
+ | Channel | Callbacks | Context |
51
+ | --- | --- | --- |
52
+ | Email | `onSend`, `onSuccess`, `onError`, `onRetry`, `onFallback` | `messageId`, recipients, subject, provider. |
53
+ | SMS | `onSend`, `onSuccess`, `onError` | `messageId`, recipient number, provider. |
54
+ | WhatsApp | `onSend`, `onSuccess`, `onError` | `messageId`, recipient number, provider. |
55
+ | Push | `onSend`, `onSuccess`, `onError` | `messageId`, redacted endpoint, provider. |
56
+
57
+ `onSuccess` receives the delivery result and optional duration in milliseconds. `onError` receives the error and optional duration.
58
+
59
+ ## Retry and fallback
60
+
61
+ Email `onRetry` runs only with `RetryTransport`. Email `onFallback` runs only with a fallback transport; both are configured as decorators around an email transport.
62
+
63
+ ## Troubleshooting
64
+
65
+ <Accordions>
66
+ <Accordion title="Why does onRetry not run?">
67
+ `onRetry` is wired only when the mailer's transport is `RetryTransport`.
68
+ </Accordion>
69
+ <Accordion title="Why does my hook error not stop the send?">
70
+ Hook failures are intentionally swallowed so observability code cannot break message delivery.
71
+ </Accordion>
72
+ <Accordion title="Why is a push endpoint redacted?">
73
+ The endpoint path can contain a long-lived delivery token and is not exposed in full to hooks.
74
+ </Accordion>
75
+ </Accordions>
76
+
77
+ ## Learn more
78
+
79
+ - [Email channel](./email)
80
+ - [Observability](../guides/observability)
81
+
82
+ ## Next
83
+
84
+ <Cards>
85
+ <Card title="Observability" href="/docs/guides/observability" />
86
+ <Card title="Retry" href="/docs/decorators/retry" />
87
+ </Cards>