sently 0.9.2 → 1.0.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.
Files changed (245) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +39 -34
  4. package/SECURITY.md +59 -0
  5. package/dist/adapters/bun.js +1 -1
  6. package/dist/adapters/cf.js +2 -2
  7. package/dist/adapters/cf.js.map +1 -1
  8. package/dist/adapters/deno.js +1 -1
  9. package/dist/adapters/node.js +1 -1
  10. package/dist/auth/oauth2.js +2 -2
  11. package/dist/auth/oauth2.js.map +1 -1
  12. package/dist/channel-result.d.ts +6 -0
  13. package/dist/channel-result.js +1 -0
  14. package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
  15. package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
  16. package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
  17. package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
  18. package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
  19. package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
  20. package/dist/chunk-8kpgbrba.js +4 -0
  21. package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
  22. package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
  23. package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
  24. package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
  25. package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
  26. package/dist/chunk-cs1tvehn.js +4 -0
  27. package/dist/chunk-cs1tvehn.js.map +10 -0
  28. package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
  29. package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
  30. package/dist/chunk-f5x8vesr.js +4 -0
  31. package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
  32. package/dist/chunk-fv0bemgf.js +4 -0
  33. package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
  34. package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
  35. package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
  36. package/dist/chunk-htdg075t.js +4 -0
  37. package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
  38. package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
  39. package/dist/chunk-q2mahnb7.js.map +10 -0
  40. package/dist/chunk-sbgt4165.js +4 -0
  41. package/dist/chunk-sbgt4165.js.map +10 -0
  42. package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
  43. package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
  44. package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
  45. package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
  46. package/dist/chunk-w4c9kj70.js +13 -0
  47. package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
  48. package/dist/chunk-w9sabmb0.js +4 -0
  49. package/dist/chunk-w9sabmb0.js.map +10 -0
  50. package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
  51. package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
  52. package/dist/chunk-z1589fjk.js +4 -0
  53. package/dist/chunk-z1589fjk.js.map +10 -0
  54. package/dist/core/channel-result.d.ts +36 -0
  55. package/dist/core/channel-result.js +3 -0
  56. package/dist/core/channel-result.js.map +10 -0
  57. package/dist/core/decorated-transport.d.ts +30 -0
  58. package/dist/core/decorator-hooks.d.ts +17 -0
  59. package/dist/core/errors.js +2 -2
  60. package/dist/core/errors.js.map +1 -1
  61. package/dist/core/provider-label.d.ts +7 -3
  62. package/dist/core/push-endpoint.d.ts +5 -0
  63. package/dist/core/push-types.d.ts +48 -14
  64. package/dist/core/sms-types.d.ts +12 -0
  65. package/dist/core/smtp.js +2 -2
  66. package/dist/core/smtp.js.map +1 -1
  67. package/dist/core/whatsapp-types.d.ts +12 -0
  68. package/dist/detect.js +2 -2
  69. package/dist/detect.js.map +1 -1
  70. package/dist/dkim.js +2 -2
  71. package/dist/dkim.js.map +2 -2
  72. package/dist/idempotency.js +2 -2
  73. package/dist/idempotency.js.map +1 -1
  74. package/dist/index.d.ts +4 -0
  75. package/dist/index.js +5 -0
  76. package/dist/mailer.js +2 -2
  77. package/dist/mailer.js.map +1 -1
  78. package/dist/observability/console.js +1 -1
  79. package/dist/plugins/react.js +2 -2
  80. package/dist/plugins/react.js.map +1 -1
  81. package/dist/plugins/template.js +1 -1
  82. package/dist/pool/pool.js +2 -2
  83. package/dist/pool/pool.js.map +1 -1
  84. package/dist/push.d.ts +1 -0
  85. package/dist/push.js +2 -2
  86. package/dist/push.js.map +3 -3
  87. package/dist/sms.d.ts +1 -0
  88. package/dist/sms.js +2 -2
  89. package/dist/sms.js.map +3 -3
  90. package/dist/smtp-mailer.js +2 -2
  91. package/dist/smtp-mailer.js.map +1 -1
  92. package/dist/transports/brevo.js +2 -2
  93. package/dist/transports/brevo.js.map +2 -2
  94. package/dist/transports/cloudflare-email.js +2 -2
  95. package/dist/transports/cloudflare-email.js.map +2 -2
  96. package/dist/transports/fallback.d.ts +32 -6
  97. package/dist/transports/fallback.js +2 -2
  98. package/dist/transports/fallback.js.map +1 -1
  99. package/dist/transports/fcm.d.ts +55 -0
  100. package/dist/transports/fcm.js +4 -0
  101. package/dist/transports/fcm.js.map +10 -0
  102. package/dist/transports/loops.js +2 -2
  103. package/dist/transports/loops.js.map +1 -1
  104. package/dist/transports/mailersend.js +2 -2
  105. package/dist/transports/mailersend.js.map +2 -2
  106. package/dist/transports/mailgun.js +2 -2
  107. package/dist/transports/mailgun.js.map +2 -2
  108. package/dist/transports/mailpit.d.ts +166 -0
  109. package/dist/transports/mailpit.js +3 -0
  110. package/dist/transports/mailpit.js.map +10 -0
  111. package/dist/transports/mailtrap.js +2 -2
  112. package/dist/transports/mailtrap.js.map +2 -2
  113. package/dist/transports/msegat.js +2 -2
  114. package/dist/transports/msegat.js.map +1 -1
  115. package/dist/transports/plunk.js +2 -2
  116. package/dist/transports/plunk.js.map +1 -1
  117. package/dist/transports/postmark.js +2 -2
  118. package/dist/transports/postmark.js.map +2 -2
  119. package/dist/transports/preview.js +2 -2
  120. package/dist/transports/preview.js.map +1 -1
  121. package/dist/transports/resend.js +2 -2
  122. package/dist/transports/resend.js.map +2 -2
  123. package/dist/transports/retry.d.ts +28 -9
  124. package/dist/transports/retry.js +2 -2
  125. package/dist/transports/retry.js.map +3 -3
  126. package/dist/transports/sendgrid.js +2 -2
  127. package/dist/transports/sendgrid.js.map +2 -2
  128. package/dist/transports/ses.js +2 -2
  129. package/dist/transports/ses.js.map +2 -2
  130. package/dist/transports/smtp.js +2 -2
  131. package/dist/transports/smtp.js.map +1 -1
  132. package/dist/transports/sndr.js +2 -2
  133. package/dist/transports/sndr.js.map +1 -1
  134. package/dist/transports/sparkpost.js +2 -2
  135. package/dist/transports/sparkpost.js.map +2 -2
  136. package/dist/transports/taqnyat-mail.js +2 -2
  137. package/dist/transports/taqnyat-mail.js.map +1 -1
  138. package/dist/transports/taqnyat-sms.js +2 -2
  139. package/dist/transports/taqnyat-sms.js.map +1 -1
  140. package/dist/transports/taqnyat-whatsapp.js +2 -2
  141. package/dist/transports/taqnyat-whatsapp.js.map +1 -1
  142. package/dist/transports/twilio-sms.js +2 -2
  143. package/dist/transports/twilio-sms.js.map +2 -2
  144. package/dist/transports/unifonic.d.ts +81 -0
  145. package/dist/transports/unifonic.js +3 -0
  146. package/dist/transports/unifonic.js.map +10 -0
  147. package/dist/transports/webpush.js +2 -2
  148. package/dist/transports/webpush.js.map +3 -3
  149. package/dist/transports/weighted-fallback.d.ts +11 -9
  150. package/dist/transports/weighted-fallback.js +2 -2
  151. package/dist/transports/weighted-fallback.js.map +3 -3
  152. package/dist/transports/whatsapp-cloud.js +2 -2
  153. package/dist/transports/whatsapp-cloud.js.map +1 -1
  154. package/dist/webhooks/brevo.js +2 -2
  155. package/dist/webhooks/brevo.js.map +1 -1
  156. package/dist/webhooks/mailgun.js +2 -2
  157. package/dist/webhooks/mailgun.js.map +1 -1
  158. package/dist/webhooks/postmark.js +2 -2
  159. package/dist/webhooks/postmark.js.map +1 -1
  160. package/dist/webhooks/resend.js +2 -2
  161. package/dist/webhooks/resend.js.map +1 -1
  162. package/dist/webhooks/sendgrid.js +2 -2
  163. package/dist/webhooks/sendgrid.js.map +1 -1
  164. package/dist/webhooks/ses.js +2 -2
  165. package/dist/webhooks/ses.js.map +1 -1
  166. package/dist/webhooks/sndr.js +2 -2
  167. package/dist/webhooks/sndr.js.map +1 -1
  168. package/dist/webhooks/timing-safe-equal.js +1 -1
  169. package/dist/webhooks/twilio-sms.d.ts +20 -0
  170. package/dist/webhooks/twilio-sms.js +3 -0
  171. package/dist/webhooks/twilio-sms.js.map +10 -0
  172. package/dist/webhooks/types.d.ts +37 -3
  173. package/dist/webhooks/types.js +3 -0
  174. package/dist/webhooks/types.js.map +9 -0
  175. package/dist/webhooks/unifonic.d.ts +13 -0
  176. package/dist/webhooks/unifonic.js +3 -0
  177. package/dist/webhooks/unifonic.js.map +10 -0
  178. package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
  179. package/dist/webhooks/whatsapp-cloud.js +3 -0
  180. package/dist/webhooks/whatsapp-cloud.js.map +10 -0
  181. package/dist/webhooks.d.ts +10 -5
  182. package/dist/webhooks.js +11 -0
  183. package/dist/whatsapp.d.ts +1 -0
  184. package/dist/whatsapp.js +2 -2
  185. package/dist/whatsapp.js.map +3 -3
  186. package/package.json +39 -2
  187. package/site/content/docs/ai/llms-txt.mdx +47 -2
  188. package/site/content/docs/channels/email.mdx +3 -0
  189. package/site/content/docs/channels/hooks.mdx +6 -5
  190. package/site/content/docs/channels/index.mdx +5 -3
  191. package/site/content/docs/channels/push.mdx +31 -21
  192. package/site/content/docs/channels/sms.mdx +4 -1
  193. package/site/content/docs/decorators/fallback.mdx +45 -13
  194. package/site/content/docs/decorators/index.mdx +31 -13
  195. package/site/content/docs/decorators/preview.mdx +52 -9
  196. package/site/content/docs/decorators/retry.mdx +32 -9
  197. package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
  198. package/site/content/docs/get-started/entrypoints.mdx +11 -8
  199. package/site/content/docs/get-started/index.mdx +8 -2
  200. package/site/content/docs/get-started/installation.mdx +3 -2
  201. package/site/content/docs/get-started/introduction.mdx +7 -3
  202. package/site/content/docs/get-started/meta.json +4 -1
  203. package/site/content/docs/get-started/migrate-nodemailer.mdx +132 -5
  204. package/site/content/docs/get-started/non-goals.mdx +57 -0
  205. package/site/content/docs/get-started/stability.mdx +69 -0
  206. package/site/content/docs/get-started/support-matrix.mdx +62 -0
  207. package/site/content/docs/guides/compare.mdx +87 -0
  208. package/site/content/docs/guides/failover.mdx +121 -0
  209. package/site/content/docs/guides/index.mdx +2 -0
  210. package/site/content/docs/guides/meta.json +2 -0
  211. package/site/content/docs/guides/security.mdx +38 -8
  212. package/site/content/docs/guides/webhooks.mdx +35 -20
  213. package/site/content/docs/index.mdx +78 -4
  214. package/site/content/docs/meta.json +0 -1
  215. package/site/content/docs/reference/channel-result.mdx +82 -0
  216. package/site/content/docs/reference/exports.mdx +9 -4
  217. package/site/content/docs/reference/meta.json +1 -0
  218. package/site/content/docs/reference/push-options.mdx +58 -3
  219. package/site/content/docs/reference/sms-options.mdx +18 -2
  220. package/site/content/docs/reference/webhook-events.mdx +38 -14
  221. package/site/content/docs/reference/whatsapp-options.mdx +19 -1
  222. package/site/content/docs/transports/fcm.mdx +84 -0
  223. package/site/content/docs/transports/index.mdx +11 -3
  224. package/site/content/docs/transports/mailpit.mdx +123 -0
  225. package/site/content/docs/transports/meta.json +4 -1
  226. package/site/content/docs/transports/smtp.mdx +61 -22
  227. package/site/content/docs/transports/unifonic.mdx +85 -0
  228. package/dist/chunk-0gqy32pe.js.map +0 -10
  229. package/dist/chunk-5915vbcj.js +0 -4
  230. package/dist/chunk-5915vbcj.js.map +0 -10
  231. package/dist/chunk-93vqxxj2.js +0 -4
  232. package/dist/chunk-b602dhck.js +0 -4
  233. package/dist/chunk-gp4fwrs8.js +0 -4
  234. package/dist/chunk-gp4fwrs8.js.map +0 -10
  235. package/dist/chunk-n0qeyzqm.js +0 -13
  236. package/dist/chunk-q8k3m6qe.js +0 -4
  237. package/dist/chunk-q8k3m6qe.js.map +0 -10
  238. package/dist/chunk-qtkd5bak.js +0 -4
  239. package/dist/chunk-yp311efr.js +0 -4
  240. package/site/content/docs/quick-start/email.mdx +0 -40
  241. package/site/content/docs/quick-start/index.mdx +0 -15
  242. package/site/content/docs/quick-start/meta.json +0 -5
  243. package/site/content/docs/quick-start/push.mdx +0 -36
  244. package/site/content/docs/quick-start/sms.mdx +0 -36
  245. package/site/content/docs/quick-start/whatsapp.mdx +0 -38
@@ -6,7 +6,7 @@ source: "README.md"
6
6
  ---
7
7
 
8
8
  Choose a channel sender for the kind of message your application sends.
9
- Each sender accepts a provider transport, so delivery providers can change without rewriting send calls.
9
+ Each sender accepts a provider transport — retry, fallback, and `SentlyError` codes stay the same as you add channels.
10
10
 
11
11
  <Callout title="The one rule">Application code calls a sently channel sender; provider-specific code stays in its transport.</Callout>
12
12
 
@@ -18,7 +18,7 @@ Each sender accepts a provider transport, so delivery providers can change witho
18
18
  | Email | `createMailer` or `createSMTPMailer` | Sender, recipient, subject, and body. |
19
19
  | SMS | `createSmsSender` | Recipient number and body. |
20
20
  | WhatsApp | `createWhatsAppSender` | Template or text message. |
21
- | Push | `createPushSender` | Browser subscription, title, and body. |
21
+ | Push | `createPushSender` | Web Push subscription or FCM token, plus title and body. |
22
22
 
23
23
  </Step>
24
24
  <Step title="Wire in a transport">
@@ -52,7 +52,7 @@ await sms.send({
52
52
  | `createSMTPMailer` | Yes | Email configured with SMTP host, port, and authentication. |
53
53
  | `createSmsSender` | No | SMS with an `SmsTransport`. |
54
54
  | `createWhatsAppSender` | No | WhatsApp with a `WhatsAppTransport`. |
55
- | `createPushSender` | No | Web Push with a `PushTransport`. |
55
+ | `createPushSender` | No | Web Push or FCM with a `PushTransport`. |
56
56
 
57
57
  ## Troubleshooting
58
58
 
@@ -68,6 +68,8 @@ await sms.send({
68
68
  ## Learn more
69
69
 
70
70
  - [Choose an entrypoint](../get-started/entrypoints)
71
+ - [Support matrix](../get-started/support-matrix)
72
+ - [Failover](../guides/failover)
71
73
  - [Browse transports](../transports)
72
74
 
73
75
  ## Next
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  title: Push
3
- description: Send encrypted Web Push notifications through a push transport.
3
+ description: Send Web Push or FCM notifications through a push transport.
4
4
  icon: Bell
5
5
  source: "src/push.ts"
6
6
  ---
7
7
 
8
- Use Web Push for browser notifications after a browser has granted permission and returned a subscription.
9
- The sender passes the subscription and notification payload to a push transport.
8
+ Use push for browser Web Push subscriptions or FCM device tokens.
9
+ The sender passes the target and notification payload to a push transport.
10
10
 
11
- <Callout title="The one rule">Treat subscription endpoints as secrets: they are long-lived delivery tokens.</Callout>
11
+ <Callout title="The one rule">Treat subscription endpoints and device tokens as secrets they are long-lived delivery credentials.</Callout>
12
+
13
+ ## Quick start
12
14
 
13
15
  <Steps>
14
16
  <Step title="Create a Push sender">
@@ -27,10 +29,11 @@ const push = createPushSender({
27
29
  ```
28
30
 
29
31
  </Step>
30
- <Step title="Send to a stored browser subscription">
32
+ <Step title="Send to a browser subscription or FCM token">
31
33
 
32
34
  ```ts
33
- const result = await push.send({
35
+ // Web Push
36
+ await push.send({
34
37
  subscription: {
35
38
  endpoint: "https://fcm.googleapis.com/fcm/send/example",
36
39
  keys: { p256dh: "browser-public-key", auth: "browser-auth-secret" },
@@ -39,7 +42,12 @@ const result = await push.send({
39
42
  body: "Your weekly report is ready to view.",
40
43
  });
41
44
 
42
- console.log(result.status);
45
+ // FCM (with FcmTransport)
46
+ await push.send({
47
+ token: deviceRegistrationToken,
48
+ title: "Report ready",
49
+ body: "Your weekly report is ready to view.",
50
+ });
43
51
  ```
44
52
 
45
53
  </Step>
@@ -53,15 +61,16 @@ console.log(result.status);
53
61
  | `plugins` | `PushPlugin[]` | Optional transforms run before delivery. |
54
62
  | `hooks` | `PushHooks` | Optional callbacks for delivery activity. |
55
63
 
56
- ## Push and VAPID options
64
+ ## Options shapes
57
65
 
58
- | Option | Type | Meaning |
66
+ `PushOptions` is a union:
67
+
68
+ | Shape | Required target | Transport |
59
69
  | --- | --- | --- |
60
- | `subscription` | `PushSubscription` | Required browser endpoint plus `p256dh` and `auth` keys. |
61
- | `title`, `body` | `string` | Required notification content. |
62
- | `data`, `icon` | `Record<string, unknown>`, `string` | Optional application payload and icon URL. |
63
- | `ttl` | `number` | Optional delivery lifetime in seconds. |
64
- | `subject` | `string` | Required Web Push contact URI for the VAPID transport. |
70
+ | Web Push | `subscription` | `WebPushTransport` |
71
+ | FCM | `token` | `FcmTransport` |
72
+
73
+ Shared fields: `title`, `body`, optional `data`, `icon`, `ttl`, `messageId`.
65
74
 
66
75
  ## Troubleshooting
67
76
 
@@ -69,24 +78,25 @@ console.log(result.status);
69
78
  <Accordion title="Why does the transport reject my endpoint?">
70
79
  Web Push validates subscription endpoint hosts before sending. Add only exact private relay hostnames with `allowedEndpointHosts` when needed.
71
80
  </Accordion>
72
- <Accordion title="Why is the endpoint redacted in hooks?">
73
- The endpoint path can contain a long-lived delivery token, so hook context keeps only a redacted form.
81
+ <Accordion title="Why does FCM reject my subscription object?">
82
+ `FcmTransport` requires `token`. Pass a Web Push `subscription` only to `WebPushTransport`.
74
83
  </Accordion>
75
- <Accordion title="What does verify do?">
76
- `push.verify()` delegates to the transport when available; the Web Push transport checks its VAPID credential shape.
84
+ <Accordion title="Why is the endpoint redacted in hooks?">
85
+ Endpoint paths and FCM tokens are long-lived credentials, so hook context keeps only a redacted fingerprint.
77
86
  </Accordion>
78
87
  </Accordions>
79
88
 
80
89
  ## Learn more
81
90
 
82
91
  - [Push options](../reference/push-options)
83
- - [Transport contracts](../reference/transport-contracts)
84
- - [Web Push interoperability](../guides/webpush-interop)
92
+ - [Web Push](../transports/webpush)
93
+ - [FCM](../transports/fcm)
94
+ - [Failover](../guides/failover)
85
95
  - [Hooks](./hooks)
86
96
 
87
97
  ## Next
88
98
 
89
99
  <Cards>
90
100
  <Card title="Web Push transport" href="/docs/transports/webpush" />
91
- <Card title="All transports" href="/docs/transports" />
101
+ <Card title="FCM transport" href="/docs/transports/fcm" />
92
102
  </Cards>
@@ -77,12 +77,15 @@ Twilio also accepts a default `from` or `messagingServiceSid` when you construct
77
77
  ## Learn more
78
78
 
79
79
  - [SMS options](../reference/sms-options)
80
- - [Transport contracts](../reference/transport-contracts)
80
+ - [Unifonic](../transports/unifonic)
81
+ - [Decorators](../decorators) — retry / fallback across SMS providers
82
+ - [Channel send result](../reference/channel-result)
81
83
  - [Hooks](./hooks)
82
84
 
83
85
  ## Next
84
86
 
85
87
  <Cards>
88
+ <Card title="Unifonic" href="/docs/transports/unifonic" />
86
89
  <Card title="SMS transports" href="/docs/transports" />
87
90
  <Card title="Provider OTP extras" href="/docs/guides/vendor-extras-otp" />
88
91
  </Cards>
@@ -1,34 +1,66 @@
1
1
  ---
2
2
  title: Fallback
3
- description: Fail over across ordered email transports.
3
+ description: Fail over through an ordered list of channel transports.
4
4
  icon: GitBranch
5
5
  source: "src/transports/fallback.ts"
6
6
  ---
7
7
 
8
- Fail over across ordered email transports.
8
+ Fail over through an ordered list of transports when the current one errors.
9
+ Works with email, SMS, WhatsApp, and push.
9
10
 
10
- <Callout title="The one rule">Permanent client errors do not fall through by default.</Callout>
11
+ <Callout title="The one rule">Wrap each entry in `RetryTransport` when you want in-provider retries before failover.</Callout>
11
12
 
12
- ## Configuration
13
+ ## Quick start
13
14
 
14
- Pass a non-empty transport array; options include `shouldFallback`, `onFallback`, and `cooldownMs`.
15
-
16
- <Steps><Step title="Wrap an email transport">
15
+ <Steps>
16
+ <Step title="Build a chain">
17
17
 
18
18
  ```ts
19
19
  import { FallbackTransport } from "sently/transports/fallback";
20
+ import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
21
+ import { UnifonicTransport } from "sently/transports/unifonic";
20
22
 
21
- const transport = new FallbackTransport([primary, secondary], { cooldownMs: 300_000 });
23
+ const transport = new FallbackTransport(
24
+ [
25
+ new TaqnyatSmsTransport({ bearerToken: process.env.TAQNYAT_TOKEN!, sender: "MyBrand" }),
26
+ new UnifonicTransport({ appSid: process.env.UNIFONIC_APPSID!, senderId: "MyBrand" }),
27
+ ],
28
+ { cooldownMs: 300_000 },
29
+ );
22
30
  ```
23
31
 
24
- </Step><Step title="Create the mailer">
32
+ </Step>
33
+ <Step title="Use the matching sender">
25
34
 
26
35
  ```ts
27
- const mailer = await createMailer({ transport });
36
+ import { createSmsSender } from "sently/sms";
37
+
38
+ const sms = createSmsSender({ transport });
28
39
  ```
29
40
 
30
- </Step></Steps>
41
+ </Step>
42
+ </Steps>
43
+
44
+ ## Result fields
45
+
46
+ Successful sends include `provider` and `providerIndex` for the transport that handled the message.
47
+
48
+ ## Troubleshooting
49
+
50
+ <Accordions>
51
+ <Accordion title="Why did it not fail over on 400?">
52
+ Permanent client errors (400 / 401 / 403) do not fail over by default — they usually fail on every provider.
53
+ </Accordion>
54
+ </Accordions>
55
+
56
+ ## Learn more
57
+
58
+ - [Failover guide](/docs/guides/failover) — retry-then-fallback recipes across channels
31
59
 
32
- <Accordions><Accordion title="Does this replace the inner provider?">No. It delegates sends to the wrapped transport.</Accordion></Accordions>
60
+ ## Next
33
61
 
34
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
62
+ <Cards>
63
+ <Card title="Failover guide" href="/docs/guides/failover" />
64
+ <Card title="Weighted fallback" href="/docs/decorators/weighted-fallback" />
65
+ <Card title="Retry" href="/docs/decorators/retry" />
66
+ </Cards>
@@ -1,25 +1,27 @@
1
1
  ---
2
2
  title: Decorators
3
- description: Wrap an email transport with retry, fallback, preview, or idempotency.
3
+ description: Wrap any channel transport with retry, fallback, preview, or idempotency.
4
4
  icon: Layers
5
5
  source: "src/transports/retry.ts"
6
6
  ---
7
7
 
8
- Decorators wrap another email transport. The mailer still calls `send` the same way — only the transport path changes.
8
+ Decorators wrap another transport. The channel sender still calls `send` the same way — only the transport path changes.
9
+ Retry, fallback, and weighted fallback work for email, SMS, WhatsApp, and push.
10
+ Preview and idempotency remain email-oriented.
9
11
 
10
- <Callout title="The one rule">Pass the outermost decorator to `createMailer`. Inner transports stay providers (or other decorators).</Callout>
12
+ <Callout title="The one rule">Pass the outermost decorator to the matching channel sender (`createMailer`, `createSmsSender`, ).</Callout>
11
13
 
12
14
  ## Available decorators
13
15
 
14
- | Decorator | Import |
15
- | --- | --- |
16
- | [Preview](./preview) | `sently/transports/preview` |
17
- | [Retry](./retry) | `sently/transports/retry` |
18
- | [Fallback](./fallback) | `sently/transports/fallback` |
19
- | [Weighted fallback](./weighted-fallback) | `sently/transports/weighted-fallback` |
20
- | [Idempotency](./idempotency) | `sently/idempotency` |
16
+ | Decorator | Import | Channels |
17
+ | --- | --- | --- |
18
+ | [Preview](./preview) | `sently/transports/preview` | Email |
19
+ | [Retry](./retry) | `sently/transports/retry` | Email, SMS, WhatsApp, Push |
20
+ | [Fallback](./fallback) | `sently/transports/fallback` | Email, SMS, WhatsApp, Push |
21
+ | [Weighted fallback](./weighted-fallback) | `sently/transports/weighted-fallback` | Email, SMS, WhatsApp, Push |
22
+ | [Idempotency](./idempotency) | `sently/idempotency` | Email |
21
23
 
22
- ## Typical stack
24
+ ## Typical stack (email)
23
25
 
24
26
  ```ts
25
27
  import { createMailer } from "sently/mailer";
@@ -35,16 +37,32 @@ const transport = new FallbackTransport([
35
37
  const mailer = await createMailer({ transport });
36
38
  ```
37
39
 
40
+ ## Typical stack (SMS)
41
+
42
+ ```ts
43
+ import { createSmsSender } from "sently/sms";
44
+ import { FallbackTransport } from "sently/transports/fallback";
45
+ import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
46
+ import { UnifonicTransport } from "sently/transports/unifonic";
47
+
48
+ const sms = createSmsSender({
49
+ transport: new FallbackTransport([
50
+ new TaqnyatSmsTransport({ bearerToken: process.env.TAQNYAT_TOKEN!, sender: "MyBrand" }),
51
+ new UnifonicTransport({ appSid: process.env.UNIFONIC_APPSID!, senderId: "MyBrand" }),
52
+ ]),
53
+ });
54
+ ```
55
+
38
56
  ## Learn more
39
57
 
58
+ - [Failover recipe](../guides/failover) — retry inside a provider, then fail over
40
59
  - [Transports](../transports)
41
- - [Email channel](../channels/email)
42
60
  - [Hooks](../channels/hooks)
43
61
 
44
62
  ## Next
45
63
 
46
64
  <Cards>
65
+ <Card title="Failover guide" href="/docs/guides/failover" />
47
66
  <Card title="Retry" href="/docs/decorators/retry" />
48
67
  <Card title="Fallback" href="/docs/decorators/fallback" />
49
- <Card title="Transports" href="/docs/transports" />
50
68
  </Cards>
@@ -6,29 +6,72 @@ source: "src/transports/preview.ts"
6
6
  ---
7
7
 
8
8
  Write email previews to disk instead of delivering them.
9
+ Use this when you want a local `.eml` or HTML file for the welcome email without a mail server.
9
10
 
10
11
  <Callout title="The one rule">Preview is for development, not delivery.</Callout>
11
12
 
12
- ## Configuration
13
-
14
- `outDir`, `open`, and `format` are optional; defaults are `./.emails`, false, and `eml`.
13
+ ## Quick start
15
14
 
16
- <Steps><Step title="Wrap an email transport">
15
+ <Steps>
16
+ <Step title="Create the preview transport">
17
17
 
18
18
  ```ts
19
+ import { createMailer } from "sently/mailer";
19
20
  import { PreviewTransport } from "sently/transports/preview";
20
21
 
21
22
  const transport = new PreviewTransport({ outDir: ".emails", open: true });
23
+ const mailer = await createMailer({ transport });
22
24
  ```
23
25
 
24
- </Step><Step title="Create the mailer">
26
+ </Step>
27
+ <Step title="Send and open the file">
25
28
 
26
29
  ```ts
27
- const mailer = await createMailer({ transport });
30
+ await mailer.send({
31
+ from: "dev@example.com",
32
+ to: "you@example.com",
33
+ subject: "Welcome",
34
+ text: "Thanks for joining.",
35
+ });
28
36
  ```
29
37
 
30
- </Step></Steps>
38
+ Files land under `outDir` (default `./.emails`). With `open: true`, the OS opens the written file.
39
+
40
+ </Step>
41
+ </Steps>
42
+
43
+ ## Configuration
44
+
45
+ | Option | Type | Default | Meaning |
46
+ | --- | --- | --- | --- |
47
+ | `outDir` | `string` | `"./.emails"` | Directory for preview files |
48
+ | `open` | `boolean` | `false` | Open the file after write |
49
+ | `format` | `"eml" \| "html"` | `"eml"` | Full MIME or HTML body only |
50
+
51
+ `provider` is `"preview"`. `verify()` always succeeds.
52
+
53
+ For a real SMTP catcher with a web UI, use [Mailpit](/docs/transports/mailpit) instead.
54
+
55
+ ## Troubleshooting
56
+
57
+ <Accordions>
58
+ <Accordion title="Does Preview wrap another transport?">
59
+ No. `PreviewTransport` is the delivery transport — it writes to disk and does not call a provider.
60
+ </Accordion>
61
+ <Accordion title="Where is the file?">
62
+ Check `outDir` (default `./.emails`). The console logs `[sently preview] Written: …` on each send.
63
+ </Accordion>
64
+ </Accordions>
65
+
66
+ ## Learn more
67
+
68
+ - [Mailpit](/docs/transports/mailpit) — local SMTP catcher with inbox API
69
+ - [Email channel](/docs/channels/email) — `createMailer` contract
31
70
 
32
- <Accordions><Accordion title="Does this replace the inner provider?">No. It delegates sends to the wrapped transport.</Accordion></Accordions>
71
+ ## Next
33
72
 
34
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
73
+ <Cards>
74
+ <Card title="Mailpit" href="/docs/transports/mailpit" />
75
+ <Card title="Email channel" href="/docs/channels/email" />
76
+ <Card title="Decorators" href="/docs/decorators" />
77
+ </Cards>
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  title: Retry
3
- description: Retry failed email sends with configurable backoff.
3
+ description: Retry failed sends with configurable backoff on any channel transport.
4
4
  icon: RotateCw
5
5
  source: "src/transports/retry.ts"
6
6
  ---
7
7
 
8
- Retry failed email sends with configurable backoff.
8
+ Retry failed sends with configurable backoff.
9
+ Works with email, SMS, WhatsApp, and push transports.
9
10
 
10
11
  <Callout title="The one rule">Authentication errors are not retried.</Callout>
11
12
 
@@ -13,22 +14,44 @@ Retry failed email sends with configurable backoff.
13
14
 
14
15
  `maxAttempts`, `backoff`, `baseDelay`, `retryOn`, and `onRetry` are optional.
15
16
 
16
- <Steps><Step title="Wrap an email transport">
17
+ ## Quick start
18
+
19
+ <Steps>
20
+ <Step title="Wrap a transport">
17
21
 
18
22
  ```ts
19
23
  import { RetryTransport } from "sently/transports/retry";
24
+ import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
20
25
 
21
- const transport = new RetryTransport(new ResendTransport({ apiKey: "..." }), { maxAttempts: 3 });
26
+ const transport = new RetryTransport(
27
+ new TaqnyatSmsTransport({ bearerToken: "...", sender: "MyBrand" }),
28
+ { maxAttempts: 3 },
29
+ );
22
30
  ```
23
31
 
24
- </Step><Step title="Create the mailer">
32
+ </Step>
33
+ <Step title="Pass it to the channel sender">
25
34
 
26
35
  ```ts
27
- const mailer = await createMailer({ transport });
36
+ import { createSmsSender } from "sently/sms";
37
+
38
+ const sms = createSmsSender({ transport });
28
39
  ```
29
40
 
30
- </Step></Steps>
41
+ </Step>
42
+ </Steps>
43
+
44
+ ## Troubleshooting
45
+
46
+ <Accordions>
47
+ <Accordion title="Does this replace the inner provider?">
48
+ No. It delegates sends to the wrapped transport.
49
+ </Accordion>
50
+ </Accordions>
31
51
 
32
- <Accordions><Accordion title="Does this replace the inner provider?">No. It delegates sends to the wrapped transport.</Accordion></Accordions>
52
+ ## Next
33
53
 
34
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
54
+ <Cards>
55
+ <Card title="Fallback" href="/docs/decorators/fallback" />
56
+ <Card title="Hooks" href="/docs/channels/hooks" />
57
+ </Cards>
@@ -6,6 +6,7 @@ source: "src/transports/weighted-fallback.ts"
6
6
  ---
7
7
 
8
8
  Choose a weighted primary transport and fail over on errors.
9
+ Works with email, SMS, WhatsApp, and push.
9
10
 
10
11
  <Callout title="The one rule">At least one entry with positive total weight is required.</Callout>
11
12
 
@@ -13,22 +14,42 @@ Choose a weighted primary transport and fail over on errors.
13
14
 
14
15
  Each entry has `transport` and positive `weight`; it accepts fallback options and optional `random`.
15
16
 
16
- <Steps><Step title="Wrap an email transport">
17
+ ## Quick start
18
+
19
+ <Steps>
20
+ <Step title="Build weighted entries">
17
21
 
18
22
  ```ts
19
23
  import { WeightedFallbackTransport } from "sently/transports/weighted-fallback";
20
24
 
21
- const transport = new WeightedFallbackTransport([{ transport: primary, weight: 80 }, { transport: secondary, weight: 20 }]);
25
+ const transport = new WeightedFallbackTransport([
26
+ { transport: primary, weight: 80 },
27
+ { transport: secondary, weight: 20 },
28
+ ]);
22
29
  ```
23
30
 
24
- </Step><Step title="Create the mailer">
31
+ </Step>
32
+ <Step title="Pass it to the channel sender">
25
33
 
26
34
  ```ts
27
35
  const mailer = await createMailer({ transport });
36
+ // or createSmsSender / createWhatsAppSender / createPushSender
28
37
  ```
29
38
 
30
- </Step></Steps>
39
+ </Step>
40
+ </Steps>
41
+
42
+ ## Troubleshooting
43
+
44
+ <Accordions>
45
+ <Accordion title="Does this replace the inner provider?">
46
+ No. It delegates sends to the wrapped transports.
47
+ </Accordion>
48
+ </Accordions>
31
49
 
32
- <Accordions><Accordion title="Does this replace the inner provider?">No. It delegates sends to the wrapped transport.</Accordion></Accordions>
50
+ ## Next
33
51
 
34
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
52
+ <Cards>
53
+ <Card title="Fallback" href="/docs/decorators/fallback" />
54
+ <Card title="Decorators" href="/docs/decorators" />
55
+ </Cards>
@@ -54,26 +54,27 @@ const mailer = await createSMTPMailer({
54
54
 
55
55
  | Import | Exports | Use |
56
56
  | ------ | ------- | --- |
57
- | `sently` | Shared factories, types, OAuth2, errors, Fallback / WeightedFallback / Cloudflare Email | General email entrypoint. |
57
+ | `sently` | Shared factories, types, OAuth2, errors, `toChannelSendResult`, Fallback / WeightedFallback / Cloudflare Email | General email + shared helpers. |
58
58
  | `sently/mailer` | `createMailer` | Email with an explicit transport (smallest HTTP path). |
59
59
  | `sently/smtp` | `createSMTPMailer` | SMTP relay configuration. |
60
60
  | `sently/sms` | `createSmsSender` | SMS sender. |
61
61
  | `sently/whatsapp` | `createWhatsAppSender` | WhatsApp sender. |
62
- | `sently/push` | `createPushSender` | Web Push sender. |
62
+ | `sently/push` | `createPushSender` | Push sender (Web Push or FCM). |
63
+ | `sently/channel-result` | `toChannelSendResult` | Shared `{ messageId, provider, accepted }` mapping. |
63
64
 
64
65
  ## Supporting entrypoints
65
66
 
66
67
  | Import | Use |
67
68
  | ------ | --- |
68
- | `sently/transports/*` | One provider or email decorator transport. |
69
- | `sently/webhooks/<provider>` | One webhook parser (smallest graph). |
69
+ | `sently/transports/*` | One provider or decorator transport (retry / fallback work across channels). |
70
+ | `sently/webhooks/<provider>` | One webhook parser (email, SMS, or WhatsApp). |
70
71
  | `sently/webhooks` | Convenience barrel of all webhook helpers. |
71
72
  | `sently/errors` | Stable sently error classes and codes. |
72
73
  | `sently/plugins/template` | Template plugin and simple engine. |
73
74
  | `sently/auth/oauth2` | SMTP OAuth2 client. |
74
75
  | `sently/pool` | SMTP connection pool. |
75
76
  | `sently/dkim` | DKIM signing entrypoint. |
76
- | `sently/idempotency` | Idempotency transport and memory store. |
77
+ | `sently/idempotency` | Idempotency transport and memory store (email). |
77
78
 
78
79
  ## Troubleshooting
79
80
 
@@ -87,7 +88,7 @@ No. `createMailer` requires a transport. Import `createSMTPMailer` from `sently/
87
88
 
88
89
  <Accordion title="Where do provider transports come from?">
89
90
 
90
- Import the concrete transport from its exported `sently/transports/*` subpath, such as `sently/transports/sndr` or `sently/transports/resend`.
91
+ Import the concrete transport from its exported `sently/transports/*` subpath, such as `sently/transports/sndr`, `sently/transports/resend`, or `sently/transports/mailpit`.
91
92
 
92
93
  </Accordion>
93
94
 
@@ -102,13 +103,15 @@ HTTP providers were removed from the main barrel so bare Node/Deno imports do no
102
103
  ## Learn more
103
104
 
104
105
  - [Export reference](/docs/reference/exports) — full entrypoint map
106
+ - [Support matrix](/docs/get-started/support-matrix) — Supported vs Available
107
+ - [Stability policy](/docs/get-started/stability) — frozen entrypoints at 1.x
105
108
  - [All transports](/docs/transports) — providers by channel
106
- - [Bundle size](/docs/guides/bundle-size) — keep stacks small
109
+ - [Bundle size](/docs/guides/bundle-size) — keep stacks small on Workers
107
110
 
108
111
  ## Next
109
112
 
110
113
  <Cards>
111
114
  <Card title="Exports reference" description="Public package entrypoints." href="/docs/reference/exports" />
115
+ <Card title="Support matrix" description="Production support promise." href="/docs/get-started/support-matrix" />
112
116
  <Card title="Email channel" description="createMailer and MailOptions." href="/docs/channels/email" />
113
- <Card title="SNDR" description="HTTP email via SNDR." href="/docs/transports/sndr" />
114
117
  </Cards>
@@ -5,10 +5,16 @@ icon: FolderOpen
5
5
  source: "README.md"
6
6
  ---
7
7
 
8
- Install sently and send your first message.
8
+ Install sently and send your first message. Learn the channel model once, then add SMS or push without a new failure model.
9
9
 
10
10
  <Cards>
11
11
  <Card title="Introduction" href="/docs/get-started/introduction" />
12
12
  <Card title="Installation" href="/docs/get-started/installation" />
13
- <Card title="Quick start" href="/docs/quick-start" />
13
+ <Card title="Entrypoints" href="/docs/get-started/entrypoints" />
14
+ <Card title="Runtimes" href="/docs/get-started/runtimes" />
15
+ <Card title="Channels" href="/docs/channels" />
16
+ <Card title="Migrate from Nodemailer" href="/docs/get-started/migrate-nodemailer" />
17
+ <Card title="Stability policy" href="/docs/get-started/stability" />
18
+ <Card title="Support matrix" href="/docs/get-started/support-matrix" />
19
+ <Card title="Non-goals" href="/docs/get-started/non-goals" />
14
20
  </Cards>
@@ -5,7 +5,7 @@ icon: Package
5
5
  source: "package.json"
6
6
  ---
7
7
 
8
- sently is an ESM package for applications that send email, SMS, WhatsApp, or browser push.
8
+ sently is an ESM package for applications that send email, SMS, WhatsApp, or push (Web Push / FCM).
9
9
  Install it in the application that owns your delivery credentials and provider transport configuration.
10
10
 
11
11
  <Callout title="The one rule">Import the narrowest sently entrypoint that contains the sender or transport you use.</Callout>
@@ -52,7 +52,8 @@ const mailer = await createMailer({
52
52
  | Email over SMTP | `sently/smtp` |
53
53
  | SMS | `sently/sms` |
54
54
  | WhatsApp | `sently/whatsapp` |
55
- | Web Push | `sently/push` |
55
+ | Push (Web Push or FCM) | `sently/push` |
56
+ | Shared send-result mapping | `sently/channel-result` |
56
57
 
57
58
  ## Troubleshooting
58
59
 
@@ -5,8 +5,8 @@ icon: Compass
5
5
  source: "README.md"
6
6
  ---
7
7
 
8
- sently sends email, SMS, WhatsApp, and browser push without making provider SDK calls your application API.
9
- Choose the channel sender first, then attach the provider transport that performs delivery.
8
+ sently is the channel-delivery layer for email, SMS, WhatsApp, and push (Web Push or FCM).
9
+ Choose a channel sender first, then attach a provider transport so adding SMS or push later does not invent a new error and retry model in your app.
10
10
 
11
11
  <Callout title="The one rule">Use `createMailer`, `createSmsSender`, `createWhatsAppSender`, or `createPushSender` in app code; swap transports when provider configuration changes.</Callout>
12
12
 
@@ -67,11 +67,15 @@ Email factories are asynchronous. The other channel factories return their sende
67
67
 
68
68
  - [Install sently](./installation)
69
69
  - [Choose an entrypoint](./entrypoints)
70
+ - [Stability policy](./stability)
71
+ - [Support matrix](./support-matrix)
72
+ - [Compare](/docs/guides/compare)
70
73
  - [Explore channels](../channels)
71
74
 
72
75
  ## Next
73
76
 
74
77
  <Cards>
75
- <Card title="Email quick start" href="/docs/quick-start/email" />
78
+ <Card title="Email channel" href="/docs/channels/email" />
79
+ <Card title="Support matrix" href="/docs/get-started/support-matrix" />
76
80
  <Card title="Channels" href="/docs/channels" />
77
81
  </Cards>