sently 0.9.2 → 0.10.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 (219) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +18 -16
  4. package/dist/adapters/bun.js +1 -1
  5. package/dist/adapters/cf.js +2 -2
  6. package/dist/adapters/cf.js.map +1 -1
  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/channel-result.d.ts +6 -0
  12. package/dist/channel-result.js +1 -0
  13. package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
  14. package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
  15. package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
  16. package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
  17. package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
  18. package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
  19. package/dist/chunk-8kpgbrba.js +4 -0
  20. package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
  21. package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
  22. package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
  23. package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
  24. package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
  25. package/dist/chunk-cs1tvehn.js +4 -0
  26. package/dist/chunk-cs1tvehn.js.map +10 -0
  27. package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
  28. package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
  29. package/dist/chunk-f5x8vesr.js +4 -0
  30. package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
  31. package/dist/chunk-fv0bemgf.js +4 -0
  32. package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
  33. package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
  34. package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
  35. package/dist/chunk-htdg075t.js +4 -0
  36. package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
  37. package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
  38. package/dist/chunk-q2mahnb7.js.map +10 -0
  39. package/dist/chunk-sbgt4165.js +4 -0
  40. package/dist/chunk-sbgt4165.js.map +10 -0
  41. package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
  42. package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
  43. package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
  44. package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
  45. package/dist/chunk-w4c9kj70.js +13 -0
  46. package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
  47. package/dist/chunk-w9sabmb0.js +4 -0
  48. package/dist/chunk-w9sabmb0.js.map +10 -0
  49. package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
  50. package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
  51. package/dist/chunk-z1589fjk.js +4 -0
  52. package/dist/chunk-z1589fjk.js.map +10 -0
  53. package/dist/core/channel-result.d.ts +36 -0
  54. package/dist/core/channel-result.js +3 -0
  55. package/dist/core/channel-result.js.map +10 -0
  56. package/dist/core/decorated-transport.d.ts +30 -0
  57. package/dist/core/decorator-hooks.d.ts +17 -0
  58. package/dist/core/errors.js +2 -2
  59. package/dist/core/errors.js.map +1 -1
  60. package/dist/core/provider-label.d.ts +7 -3
  61. package/dist/core/push-endpoint.d.ts +5 -0
  62. package/dist/core/push-types.d.ts +48 -14
  63. package/dist/core/sms-types.d.ts +12 -0
  64. package/dist/core/smtp.js +2 -2
  65. package/dist/core/smtp.js.map +1 -1
  66. package/dist/core/whatsapp-types.d.ts +12 -0
  67. package/dist/detect.js +2 -2
  68. package/dist/detect.js.map +1 -1
  69. package/dist/dkim.js +2 -2
  70. package/dist/dkim.js.map +2 -2
  71. package/dist/idempotency.js +2 -2
  72. package/dist/idempotency.js.map +1 -1
  73. package/dist/index.d.ts +4 -0
  74. package/dist/index.js +5 -0
  75. package/dist/mailer.js +2 -2
  76. package/dist/mailer.js.map +1 -1
  77. package/dist/observability/console.js +1 -1
  78. package/dist/plugins/react.js +2 -2
  79. package/dist/plugins/react.js.map +1 -1
  80. package/dist/plugins/template.js +1 -1
  81. package/dist/pool/pool.js +2 -2
  82. package/dist/pool/pool.js.map +1 -1
  83. package/dist/push.d.ts +1 -0
  84. package/dist/push.js +2 -2
  85. package/dist/push.js.map +3 -3
  86. package/dist/sms.d.ts +1 -0
  87. package/dist/sms.js +2 -2
  88. package/dist/sms.js.map +3 -3
  89. package/dist/smtp-mailer.js +2 -2
  90. package/dist/smtp-mailer.js.map +1 -1
  91. package/dist/transports/brevo.js +2 -2
  92. package/dist/transports/brevo.js.map +2 -2
  93. package/dist/transports/cloudflare-email.js +2 -2
  94. package/dist/transports/cloudflare-email.js.map +2 -2
  95. package/dist/transports/fallback.d.ts +32 -6
  96. package/dist/transports/fallback.js +2 -2
  97. package/dist/transports/fallback.js.map +1 -1
  98. package/dist/transports/fcm.d.ts +55 -0
  99. package/dist/transports/fcm.js +4 -0
  100. package/dist/transports/fcm.js.map +10 -0
  101. package/dist/transports/loops.js +2 -2
  102. package/dist/transports/loops.js.map +1 -1
  103. package/dist/transports/mailersend.js +2 -2
  104. package/dist/transports/mailersend.js.map +2 -2
  105. package/dist/transports/mailgun.js +2 -2
  106. package/dist/transports/mailgun.js.map +2 -2
  107. package/dist/transports/mailtrap.js +2 -2
  108. package/dist/transports/mailtrap.js.map +2 -2
  109. package/dist/transports/msegat.js +2 -2
  110. package/dist/transports/msegat.js.map +1 -1
  111. package/dist/transports/plunk.js +2 -2
  112. package/dist/transports/plunk.js.map +1 -1
  113. package/dist/transports/postmark.js +2 -2
  114. package/dist/transports/postmark.js.map +2 -2
  115. package/dist/transports/preview.js +2 -2
  116. package/dist/transports/preview.js.map +1 -1
  117. package/dist/transports/resend.js +2 -2
  118. package/dist/transports/resend.js.map +2 -2
  119. package/dist/transports/retry.d.ts +28 -9
  120. package/dist/transports/retry.js +2 -2
  121. package/dist/transports/retry.js.map +3 -3
  122. package/dist/transports/sendgrid.js +2 -2
  123. package/dist/transports/sendgrid.js.map +2 -2
  124. package/dist/transports/ses.js +2 -2
  125. package/dist/transports/ses.js.map +2 -2
  126. package/dist/transports/smtp.js +2 -2
  127. package/dist/transports/smtp.js.map +1 -1
  128. package/dist/transports/sndr.js +2 -2
  129. package/dist/transports/sndr.js.map +1 -1
  130. package/dist/transports/sparkpost.js +2 -2
  131. package/dist/transports/sparkpost.js.map +2 -2
  132. package/dist/transports/taqnyat-mail.js +2 -2
  133. package/dist/transports/taqnyat-mail.js.map +1 -1
  134. package/dist/transports/taqnyat-sms.js +2 -2
  135. package/dist/transports/taqnyat-sms.js.map +1 -1
  136. package/dist/transports/taqnyat-whatsapp.js +2 -2
  137. package/dist/transports/taqnyat-whatsapp.js.map +1 -1
  138. package/dist/transports/twilio-sms.js +2 -2
  139. package/dist/transports/twilio-sms.js.map +2 -2
  140. package/dist/transports/unifonic.d.ts +81 -0
  141. package/dist/transports/unifonic.js +3 -0
  142. package/dist/transports/unifonic.js.map +10 -0
  143. package/dist/transports/webpush.js +2 -2
  144. package/dist/transports/webpush.js.map +3 -3
  145. package/dist/transports/weighted-fallback.d.ts +11 -9
  146. package/dist/transports/weighted-fallback.js +2 -2
  147. package/dist/transports/weighted-fallback.js.map +3 -3
  148. package/dist/transports/whatsapp-cloud.js +2 -2
  149. package/dist/transports/whatsapp-cloud.js.map +1 -1
  150. package/dist/webhooks/brevo.js +2 -2
  151. package/dist/webhooks/brevo.js.map +1 -1
  152. package/dist/webhooks/mailgun.js +2 -2
  153. package/dist/webhooks/mailgun.js.map +1 -1
  154. package/dist/webhooks/postmark.js +2 -2
  155. package/dist/webhooks/postmark.js.map +1 -1
  156. package/dist/webhooks/resend.js +2 -2
  157. package/dist/webhooks/resend.js.map +1 -1
  158. package/dist/webhooks/sendgrid.js +2 -2
  159. package/dist/webhooks/sendgrid.js.map +1 -1
  160. package/dist/webhooks/ses.js +2 -2
  161. package/dist/webhooks/ses.js.map +1 -1
  162. package/dist/webhooks/sndr.js +2 -2
  163. package/dist/webhooks/sndr.js.map +1 -1
  164. package/dist/webhooks/timing-safe-equal.js +1 -1
  165. package/dist/webhooks/twilio-sms.d.ts +20 -0
  166. package/dist/webhooks/twilio-sms.js +3 -0
  167. package/dist/webhooks/twilio-sms.js.map +10 -0
  168. package/dist/webhooks/types.d.ts +37 -3
  169. package/dist/webhooks/types.js +3 -0
  170. package/dist/webhooks/types.js.map +9 -0
  171. package/dist/webhooks/unifonic.d.ts +13 -0
  172. package/dist/webhooks/unifonic.js +3 -0
  173. package/dist/webhooks/unifonic.js.map +10 -0
  174. package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
  175. package/dist/webhooks/whatsapp-cloud.js +3 -0
  176. package/dist/webhooks/whatsapp-cloud.js.map +10 -0
  177. package/dist/webhooks.d.ts +10 -5
  178. package/dist/webhooks.js +11 -0
  179. package/dist/whatsapp.d.ts +1 -0
  180. package/dist/whatsapp.js +2 -2
  181. package/dist/whatsapp.js.map +3 -3
  182. package/package.json +25 -1
  183. package/site/content/docs/channels/hooks.mdx +6 -5
  184. package/site/content/docs/channels/index.mdx +2 -2
  185. package/site/content/docs/channels/push.mdx +27 -21
  186. package/site/content/docs/channels/sms.mdx +4 -1
  187. package/site/content/docs/decorators/fallback.mdx +41 -14
  188. package/site/content/docs/decorators/index.mdx +29 -12
  189. package/site/content/docs/decorators/retry.mdx +32 -9
  190. package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
  191. package/site/content/docs/get-started/entrypoints.mdx +6 -5
  192. package/site/content/docs/get-started/installation.mdx +3 -2
  193. package/site/content/docs/get-started/introduction.mdx +1 -1
  194. package/site/content/docs/guides/webhooks.mdx +35 -20
  195. package/site/content/docs/index.mdx +1 -1
  196. package/site/content/docs/quick-start/push.mdx +22 -5
  197. package/site/content/docs/reference/channel-result.mdx +82 -0
  198. package/site/content/docs/reference/exports.mdx +5 -3
  199. package/site/content/docs/reference/meta.json +1 -0
  200. package/site/content/docs/reference/push-options.mdx +44 -3
  201. package/site/content/docs/reference/sms-options.mdx +18 -2
  202. package/site/content/docs/reference/webhook-events.mdx +38 -14
  203. package/site/content/docs/reference/whatsapp-options.mdx +19 -1
  204. package/site/content/docs/transports/fcm.mdx +80 -0
  205. package/site/content/docs/transports/index.mdx +3 -1
  206. package/site/content/docs/transports/meta.json +3 -1
  207. package/site/content/docs/transports/unifonic.mdx +85 -0
  208. package/dist/chunk-0gqy32pe.js.map +0 -10
  209. package/dist/chunk-5915vbcj.js +0 -4
  210. package/dist/chunk-5915vbcj.js.map +0 -10
  211. package/dist/chunk-93vqxxj2.js +0 -4
  212. package/dist/chunk-b602dhck.js +0 -4
  213. package/dist/chunk-gp4fwrs8.js +0 -4
  214. package/dist/chunk-gp4fwrs8.js.map +0 -10
  215. package/dist/chunk-n0qeyzqm.js +0 -13
  216. package/dist/chunk-q8k3m6qe.js +0 -4
  217. package/dist/chunk-q8k3m6qe.js.map +0 -10
  218. package/dist/chunk-qtkd5bak.js +0 -4
  219. package/dist/chunk-yp311efr.js +0 -4
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: sently handbook
3
- description: Channel-first messaging for email, SMS, WhatsApp, and web push.
3
+ description: Channel-first messaging for email, SMS, WhatsApp, and push (Web Push / FCM).
4
4
  icon: BookOpen
5
5
  source: "README.md"
6
6
  ---
@@ -1,14 +1,20 @@
1
1
  ---
2
2
  title: Push
3
- description: Send a browser notification through a channel sender.
3
+ description: Send a Web Push or FCM notification through a channel sender.
4
4
  icon: Bell
5
5
  source: "src/push.ts"
6
6
  ---
7
7
 
8
- <Callout title="The one rule">Store the browser subscription and send it back unchanged as `subscription`.</Callout>
8
+ Send a browser Web Push or FCM device notification through `createPushSender`.
9
+
10
+ <Callout title="The one rule">
11
+ Use `subscription` with `WebPushTransport`, or `token` with `FcmTransport` — not both shapes on the wrong transport.
12
+ </Callout>
13
+
14
+ ## Quick start
9
15
 
10
16
  <Steps>
11
- <Step title="Create the sender">
17
+ <Step title="Create a Web Push sender">
12
18
 
13
19
  ```ts
14
20
  import { createPushSender } from "sently/push";
@@ -27,10 +33,21 @@ const push = createPushSender({
27
33
  <Step title="Send">
28
34
 
29
35
  ```ts
30
- await push.send({ subscription, title: "Update", body: "Your report is ready." });
36
+ await push.send({
37
+ subscription,
38
+ title: "Update",
39
+ body: "Your report is ready.",
40
+ });
31
41
  ```
32
42
 
33
43
  </Step>
34
44
  </Steps>
35
45
 
36
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
46
+ For FCM device tokens, use `FcmTransport` and `token` instead of `subscription` — see [FCM](/docs/transports/fcm).
47
+
48
+ ## Next
49
+
50
+ <Cards>
51
+ <Card title="Push channel" href="/docs/channels/push" />
52
+ <Card title="FCM transport" href="/docs/transports/fcm" />
53
+ </Cards>
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Channel send result
3
+ description: Map email, SMS, WhatsApp, and push results to a shared accepted shape.
4
+ icon: Boxes
5
+ source: "src/core/channel-result.ts"
6
+ ---
7
+
8
+ Adapters that span channels can normalize every send into
9
+ `{ messageId, provider, accepted }` without inventing per-provider glue.
10
+ Use this after `mailer.send`, `sms.send`, `whatsapp.send`, or `push.send`.
11
+
12
+ <Callout title="The one rule">
13
+ Keep channel-specific fields on the original result; use `toChannelSendResult` only for the shared slice.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+ <Step title="Send on any channel">
20
+
21
+ ```ts
22
+ import { createSmsSender } from "sently/sms";
23
+ import { toChannelSendResult } from "sently/channel-result";
24
+ import { UnifonicTransport } from "sently/transports/unifonic";
25
+
26
+ const sms = createSmsSender({
27
+ transport: new UnifonicTransport({
28
+ appSid: process.env.UNIFONIC_APPSID!,
29
+ senderId: "MyBrand",
30
+ }),
31
+ });
32
+ const result = await sms.send({ to: "+15551234567", body: "Hi" });
33
+ ```
34
+
35
+ </Step>
36
+ <Step title="Normalize for your adapter">
37
+
38
+ ```ts
39
+ const shared = toChannelSendResult(result);
40
+ // { messageId, provider?, accepted }
41
+ ```
42
+
43
+ </Step>
44
+ </Steps>
45
+
46
+ ## ChannelSendResult
47
+
48
+ | Field | Type | Meaning |
49
+ | --- | --- | --- |
50
+ | `messageId` | `string` | Provider or client message id |
51
+ | `provider` | `string?` | Transport id when set |
52
+ | `accepted` | `boolean` | Provider accepted the send (not end-user delivery) |
53
+
54
+ ## Mapping
55
+
56
+ | Channel result | `accepted` |
57
+ | --- | --- |
58
+ | Email `SendResult` | `accepted.length > 0` and `rejected.length === 0` |
59
+ | SMS / WhatsApp / Push | `status` in `accepted` / `queued` / `sending` / `sent` / `delivered` / `read` / `success` (case-insensitive) |
60
+
61
+ Import from `sently/channel-result` or the main `sently` barrel.
62
+
63
+ ## Troubleshooting
64
+
65
+ <Accordions>
66
+ <Accordion title="Why is accepted true but the user never got the message?">
67
+ Acceptance means the provider took the message. Use [webhooks](/docs/guides/webhooks) for delivered / failed outcomes.
68
+ </Accordion>
69
+ </Accordions>
70
+
71
+ ## Learn more
72
+
73
+ - [Webhook events](./webhook-events)
74
+ - [SMS options](./sms-options)
75
+ - [Exports](./exports)
76
+
77
+ ## Next
78
+
79
+ <Cards>
80
+ <Card title="Webhook events" href="/docs/reference/webhook-events" />
81
+ <Card title="Exports" href="/docs/reference/exports" />
82
+ </Cards>
@@ -17,12 +17,13 @@ The main `sently` package is for shared types and factories — not every provid
17
17
 
18
18
  | Import | Primary API |
19
19
  | ------ | ----------- |
20
- | `sently` | `createMailer`, `createSMTPMailer`, types, OAuth2, `SentlyError`, `consoleObserver`, `FallbackTransport`, `WeightedFallbackTransport`, `CloudflareEmailTransport` |
20
+ | `sently` | `createMailer`, `createSMTPMailer`, types, OAuth2, `SentlyError`, `consoleObserver`, `toChannelSendResult`, `FallbackTransport`, `WeightedFallbackTransport`, `CloudflareEmailTransport` |
21
21
  | `sently/mailer` | `createMailer` only |
22
22
  | `sently/smtp` | `createSMTPMailer` |
23
23
  | `sently/sms` | `createSmsSender` |
24
24
  | `sently/whatsapp` | `createWhatsAppSender` |
25
25
  | `sently/push` | `createPushSender` |
26
+ | `sently/channel-result` | `toChannelSendResult`, `ChannelSendResult` |
26
27
  | `sently/errors` | `SentlyError` hierarchy |
27
28
  | `sently/idempotency` | `IdempotencyTransport` |
28
29
  | `sently/dkim` | DKIM signing |
@@ -36,7 +37,7 @@ The main `sently` package is for shared types and factories — not every provid
36
37
 
37
38
  | Import | Use |
38
39
  | ------ | --- |
39
- | `sently/transports/<name>` | One provider or email decorator (e.g. `sndr`, `resend`, `twilio-sms`) |
40
+ | `sently/transports/<name>` | One provider or decorator (e.g. `sndr`, `unifonic`, `fcm`, `retry`) |
40
41
 
41
42
  HTTP providers such as SNDR, Resend, and Plunk are **not** re-exported from `sently`.
42
43
 
@@ -47,7 +48,8 @@ HTTP providers such as SNDR, Resend, and Plunk are **not** re-exported from `sen
47
48
  | `sently/webhooks/<provider>` | One parser (+ verify helper when exported) |
48
49
  | `sently/webhooks` | Convenience barrel of all named helpers |
49
50
 
50
- Providers: `brevo`, `mailgun`, `postmark`, `resend`, `sendgrid`, `ses`, `sndr`.
51
+ Email: `brevo`, `mailgun`, `postmark`, `resend`, `sendgrid`, `ses`, `sndr`.
52
+ SMS / WhatsApp: `twilio-sms` (+ verify), `unifonic`, `whatsapp-cloud` (+ verify).
51
53
 
52
54
  ## Adapters
53
55
 
@@ -9,6 +9,7 @@
9
9
  "whatsapp-options",
10
10
  "push-options",
11
11
  "transport-contracts",
12
+ "channel-result",
12
13
  "errors",
13
14
  "webhook-events"
14
15
  ]
@@ -1,13 +1,21 @@
1
1
  ---
2
2
  title: Push options
3
- description: Fields accepted by `PushSender.send`.
3
+ description: Fields accepted by `PushSender.send` for Web Push and FCM.
4
4
  icon: Bell
5
5
  source: "src/core/push-types.ts"
6
6
  ---
7
7
 
8
+ `PushOptions` is `WebPushOptions | FcmPushOptions`.
9
+ Pick the shape that matches your transport.
10
+
11
+ <Callout title="The one rule">
12
+ Pass `subscription` to `WebPushTransport` and `token` to `FcmTransport`.
13
+ </Callout>
14
+
15
+ ## Shared fields
16
+
8
17
  | Field | Type | Required |
9
18
  | --- | --- | --- |
10
- | `subscription` | `PushSubscription` | yes |
11
19
  | `title` | `string` | yes |
12
20
  | `body` | `string` | yes |
13
21
  | `data` | `Record<string, unknown>` | no |
@@ -15,6 +23,39 @@ source: "src/core/push-types.ts"
15
23
  | `ttl` | `number` | no |
16
24
  | `messageId` | `string` | no |
17
25
 
26
+ ## Web Push
27
+
28
+ | Field | Type | Required |
29
+ | --- | --- | --- |
30
+ | `subscription` | `PushSubscription` | yes |
31
+
18
32
  A subscription contains `endpoint`, `keys.p256dh`, and `keys.auth`.
19
33
 
20
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
34
+ ## FCM
35
+
36
+ | Field | Type | Required |
37
+ | --- | --- | --- |
38
+ | `token` | `string` | yes |
39
+ | `image` | `string` | no |
40
+
41
+ FCM stringifies non-string `data` values before send.
42
+
43
+ ## Result
44
+
45
+ | Field | Type | Notes |
46
+ | --- | --- | --- |
47
+ | `messageId` | `string` | Provider or client id |
48
+ | `status` | `string` | e.g. `"accepted"` |
49
+ | `response` | `string` | Often the HTTP status or FCM name |
50
+ | `provider` | `string?` | `"webpush"` / `"fcm"`; set by fallback |
51
+ | `providerIndex` | `number?` | Fallback chain index |
52
+
53
+ Map any channel result with [channel send result](./channel-result).
54
+
55
+ ## Next
56
+
57
+ <Cards>
58
+ <Card title="Push channel" href="/docs/channels/push" />
59
+ <Card title="FCM transport" href="/docs/transports/fcm" />
60
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
61
+ </Cards>
@@ -12,6 +12,22 @@ source: "src/core/sms-types.ts"
12
12
  | `from` | `string` | no |
13
13
  | `messageId` | `string` | no |
14
14
 
15
- The result includes `messageId`, `to`, `status`, `response`, and optional `provider`.
15
+ ## Result
16
16
 
17
- <Cards><Card title="SMS channel" href="/docs/channels/sms" /></Cards>
17
+ | Field | Type | Notes |
18
+ | --- | --- | --- |
19
+ | `messageId` | `string` | Provider or client id |
20
+ | `to` | `string` | Recipient |
21
+ | `status` | `string` | e.g. `"accepted"`, `"queued"` |
22
+ | `response` | `string` | Provider text |
23
+ | `provider` | `string?` | Transport id; set by fallback |
24
+ | `providerIndex` | `number?` | Fallback chain index |
25
+
26
+ Map any channel result to `{ messageId, provider, accepted }` with [channel send result](./channel-result).
27
+
28
+ ## Next
29
+
30
+ <Cards>
31
+ <Card title="SMS channel" href="/docs/channels/sms" />
32
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
33
+ </Cards>
@@ -1,17 +1,29 @@
1
1
  ---
2
2
  title: Webhook events
3
- description: Normalized EmailEvent fields returned by every provider webhook parser.
3
+ description: Normalized EmailEvent and DeliveryEvent fields from provider webhook parsers.
4
4
  icon: Webhook
5
5
  source: "src/webhooks/types.ts"
6
6
  ---
7
7
 
8
- Every parser returns `EmailEvent[]` so delivery webhooks can share one application pipeline.
9
- Verify signatures in the [webhooks guide](/docs/guides/webhooks) before you process events.
8
+ Email parsers return `EmailEvent[]`. SMS and WhatsApp parsers return `DeliveryEvent[]`.
9
+ Use `toDeliveryEvent` to put email events on the same ingest path as SMS / WhatsApp.
10
10
 
11
11
  <Callout title="The one rule">
12
12
  Branch on `event.type` and keep provider-specific details in `event.raw` when you need them.
13
13
  </Callout>
14
14
 
15
+ ## DeliveryEvent
16
+
17
+ | Field | Type | Meaning |
18
+ | ----- | ---- | ------- |
19
+ | `channel` | `"email" \| "sms" \| "whatsapp" \| "push"` | Messaging channel |
20
+ | `provider` | `string` | Parser id (e.g. `"twilio-sms"`) |
21
+ | `type` | union below | Normalized lifecycle type |
22
+ | `messageId` | `string?` | Provider message id when present |
23
+ | `recipient` | `string?` | Address or phone when present |
24
+ | `timestamp` | `Date?` | Event time when present |
25
+ | `raw` | `unknown` | Original provider payload |
26
+
15
27
  ## EmailEvent
16
28
 
17
29
  | Field | Type | Meaning |
@@ -25,15 +37,27 @@ Verify signatures in the [webhooks guide](/docs/guides/webhooks) before you proc
25
37
 
26
38
  ## Normalized `type` values
27
39
 
28
- | Value | Meaning |
29
- | ----- | ------- |
30
- | `delivered` | Accepted / delivered to the mailbox provider. |
31
- | `bounced` | Hard failure or bounce. |
32
- | `complained` | Spam complaint. |
33
- | `opened` | Open tracking. |
34
- | `clicked` | Click tracking. |
35
- | `deferred` | Temporary deferral. |
36
- | `unknown` | Unmapped or informational provider event. |
40
+ | Value | Channels | Meaning |
41
+ | ----- | -------- | ------- |
42
+ | `queued` | SMS | Accepted / queued by the provider |
43
+ | `sent` | SMS / WhatsApp | Handed to the network |
44
+ | `delivered` | All | Delivered (or mailbox accepted for email) |
45
+ | `failed` | SMS / WhatsApp | Permanent failure |
46
+ | `read` | SMS / WhatsApp | Read receipt |
47
+ | `bounced` | Email | Hard failure or bounce |
48
+ | `complained` | Email | Spam complaint |
49
+ | `opened` | Email | Open tracking |
50
+ | `clicked` | Email | Click tracking |
51
+ | `deferred` | Email | Temporary deferral |
52
+ | `unknown` | All | Unmapped or informational event |
53
+
54
+ ## Mapping helpers
55
+
56
+ ```ts
57
+ import { toDeliveryEvent } from "sently/webhooks";
58
+
59
+ const delivery = toDeliveryEvent(emailEvent);
60
+ ```
37
61
 
38
62
  ## SNDR mapping
39
63
 
@@ -51,7 +75,7 @@ SNDR `data.email_id` becomes `messageId`. Recipients come from `data.to` or `dat
51
75
 
52
76
  <Accordion title="Why is unsubscribed mapped to unknown?">
53
77
 
54
- The shared `EmailEvent.type` union has no unsubscribe value. Keep the vendor payload in `raw` if you need unsubscribe-specific handling.
78
+ The shared email `type` union has no unsubscribe value. Keep the vendor payload in `raw` if you need unsubscribe-specific handling.
55
79
 
56
80
  </Accordion>
57
81
 
@@ -60,7 +84,7 @@ The shared `EmailEvent.type` union has no unsubscribe value. Keep the vendor pay
60
84
  ## Learn more
61
85
 
62
86
  - [Webhooks guide](/docs/guides/webhooks) — verify + parse flow
63
- - [SNDR transport](/docs/transports/sndr) — sending side
87
+ - [Channel send result](/docs/reference/channel-result) — send-time accepted mapping
64
88
 
65
89
  ## Next
66
90
 
@@ -12,4 +12,22 @@ source: "src/core/whatsapp-types.ts"
12
12
 
13
13
  Both shapes accept optional `messageId`. Template components can include `header`, `body`, or `button` parameters.
14
14
 
15
- <Cards><Card title="WhatsApp channel" href="/docs/channels/whatsapp" /></Cards>
15
+ ## Result
16
+
17
+ | Field | Type | Notes |
18
+ | --- | --- | --- |
19
+ | `messageId` | `string` | Provider or client id |
20
+ | `to` | `string` | Recipient |
21
+ | `status` | `string` | e.g. `"accepted"` |
22
+ | `response` | `string` | Provider text |
23
+ | `provider` | `string?` | Transport id; set by fallback |
24
+ | `providerIndex` | `number?` | Fallback chain index |
25
+
26
+ Map any channel result with [channel send result](./channel-result).
27
+
28
+ ## Next
29
+
30
+ <Cards>
31
+ <Card title="WhatsApp channel" href="/docs/channels/whatsapp" />
32
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
33
+ </Cards>
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: FCM
3
+ description: Send mobile push through Firebase Cloud Messaging HTTP v1.
4
+ icon: Bell
5
+ source: "src/transports/fcm.ts"
6
+ ---
7
+
8
+ Send device-token push through Firebase Cloud Messaging HTTP v1 for native Android and iOS clients.
9
+ Use [Web Push](./webpush) when you have a browser Push API subscription instead.
10
+
11
+ <Callout title="The one rule">Pass `token` in push options — not a Web Push `subscription`.</Callout>
12
+
13
+ ## Quick start
14
+
15
+ <Steps>
16
+ <Step title="Configure the transport">
17
+
18
+ ```ts
19
+ import { createPushSender } from "sently/push";
20
+ import { FcmTransport } from "sently/transports/fcm";
21
+
22
+ const push = createPushSender({
23
+ transport: new FcmTransport({
24
+ projectId: process.env.FCM_PROJECT_ID!,
25
+ clientEmail: process.env.FCM_CLIENT_EMAIL!,
26
+ privateKey: process.env.FCM_PRIVATE_KEY!,
27
+ }),
28
+ });
29
+ ```
30
+
31
+ </Step>
32
+ <Step title="Send to a device token">
33
+
34
+ ```ts
35
+ const result = await push.send({
36
+ token: deviceRegistrationToken,
37
+ title: "Hello",
38
+ body: "World",
39
+ });
40
+ console.log(result.messageId, result.status);
41
+ ```
42
+
43
+ </Step>
44
+ </Steps>
45
+
46
+ ## Configuration
47
+
48
+ | Option | Type | Default or requirement |
49
+ | --- | --- | --- |
50
+ | `projectId` | `string` | required |
51
+ | `clientEmail` | `string` | service account email |
52
+ | `privateKey` | `string` | PEM private key (env / secrets only) |
53
+ | `getAccessToken` | `() => Promise<string>` | optional override for tests |
54
+
55
+ Literal `\n` sequences in `privateKey` env strings are normalized to newlines.
56
+ The transport mints a short-lived OAuth2 token with scope `firebase.messaging` — no Google SDK.
57
+
58
+ ## Troubleshooting
59
+
60
+ <Accordions>
61
+ <Accordion title="Can I pass a browser subscription?">
62
+ No. `FcmTransport` requires `token`. Use `WebPushTransport` for VAPID subscriptions.
63
+ </Accordion>
64
+ <Accordion title="Where do I get the private key?">
65
+ Download a Firebase / GCP service account JSON and pass `project_id`, `client_email`, and `private_key` from that file via environment variables.
66
+ </Accordion>
67
+ </Accordions>
68
+
69
+ ## Learn more
70
+
71
+ - [Push channel](/docs/channels/push)
72
+ - [Push options](/docs/reference/push-options)
73
+ - [Web Push](./webpush)
74
+
75
+ ## Next
76
+
77
+ <Cards>
78
+ <Card title="Push channel" href="/docs/channels/push" />
79
+ <Card title="Web Push" href="/docs/transports/webpush" />
80
+ </Cards>
@@ -63,9 +63,11 @@ Every import below is an exported package subpath.
63
63
  | [Twilio SMS](./twilio-sms) | SMS | `sently/transports/twilio-sms` |
64
64
  | [Taqnyat SMS](./taqnyat-sms) | SMS | `sently/transports/taqnyat-sms` |
65
65
  | [Msegat](./msegat) | SMS | `sently/transports/msegat` |
66
+ | [Unifonic](./unifonic) | SMS | `sently/transports/unifonic` |
66
67
  | [WhatsApp Cloud](./whatsapp-cloud) | WhatsApp | `sently/transports/whatsapp-cloud` |
67
68
  | [Taqnyat WhatsApp](./taqnyat-whatsapp) | WhatsApp | `sently/transports/taqnyat-whatsapp` |
68
69
  | [Web Push](./webpush) | Push | `sently/transports/webpush` |
70
+ | [FCM](./fcm) | Push | `sently/transports/fcm` |
69
71
 
70
72
  For retry, fallback, preview, and idempotency wrappers, see [Decorators](../decorators).
71
73
 
@@ -85,7 +87,7 @@ For retry, fallback, preview, and idempotency wrappers, see [Decorators](../deco
85
87
  No. A transport implements one channel contract. Choose a transport listed for the channel sender you use.
86
88
  </Accordion>
87
89
  <Accordion title="How do I add retries or fallback?">
88
- Wrap an email transport with a [decorator](../decorators), then pass the wrapper to `createMailer`.
90
+ Wrap any channel transport with a [decorator](../decorators), then pass the wrapper to the matching sender.
89
91
  </Accordion>
90
92
  </Accordions>
91
93
 
@@ -23,10 +23,12 @@
23
23
  "twilio-sms",
24
24
  "taqnyat-sms",
25
25
  "msegat",
26
+ "unifonic",
26
27
  "---WhatsApp---",
27
28
  "whatsapp-cloud",
28
29
  "taqnyat-whatsapp",
29
30
  "---Push---",
30
- "webpush"
31
+ "webpush",
32
+ "fcm"
31
33
  ]
32
34
  }
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Unifonic
3
+ description: Send SMS through the Unifonic el.cloud REST API.
4
+ icon: Truck
5
+ source: "src/transports/unifonic.ts"
6
+ ---
7
+
8
+ Send SMS through Unifonic’s el.cloud REST API for transactional texts such as OTPs and alerts.
9
+ Wire the transport into `createSmsSender` so app code stays on the SMS channel API.
10
+
11
+ <Callout title="The one rule">Create this transport under `createSmsSender` — do not call Unifonic from app code directly.</Callout>
12
+
13
+ ## Quick start
14
+
15
+ <Steps>
16
+ <Step title="Configure the transport">
17
+
18
+ ```ts
19
+ import { createSmsSender } from "sently/sms";
20
+ import { UnifonicTransport } from "sently/transports/unifonic";
21
+
22
+ const sender = createSmsSender({
23
+ transport: new UnifonicTransport({
24
+ appSid: process.env.UNIFONIC_APPSID!,
25
+ senderId: "MyBrand",
26
+ }),
27
+ });
28
+ ```
29
+
30
+ </Step>
31
+ <Step title="Send with the channel API">
32
+
33
+ ```ts
34
+ const result = await sender.send({ to: "+9665xxxxxxxx", body: "Hello" });
35
+ console.log(result.messageId, result.status);
36
+ ```
37
+
38
+ </Step>
39
+ </Steps>
40
+
41
+ ## Configuration
42
+
43
+ | Option | Type | Default or requirement |
44
+ | --- | --- | --- |
45
+ | `appSid` | `string` | required (`AppSid`) |
46
+ | `senderId` | `string` | required on send unless `options.from` is set |
47
+ | `statusCallback` | `string` | optional public HTTPS DLR URL |
48
+ | `async` | `boolean` | `false` |
49
+
50
+ `options.from` maps to `SenderID`. `options.messageId` maps to `CorrelationID`.
51
+ Phone numbers are normalized to international digits without `+` or leading `00`.
52
+
53
+ ## Delivery webhooks
54
+
55
+ Parse Unifonic status callbacks with `sently/webhooks/unifonic` into `DeliveryEvent[]`.
56
+
57
+ ```ts
58
+ import { parse } from "sently/webhooks/unifonic";
59
+
60
+ const events = parse(await request.json());
61
+ ```
62
+
63
+ ## Troubleshooting
64
+
65
+ <Accordions>
66
+ <Accordion title="Should I call the Unifonic SDK?">
67
+ No. Use the sently SMS sender; this transport posts to `el.cloud.unifonic.com` for you.
68
+ </Accordion>
69
+ <Accordion title="Why do I get a SenderID error?">
70
+ Set `senderId` on the transport or pass `from` on each `send`.
71
+ </Accordion>
72
+ </Accordions>
73
+
74
+ ## Learn more
75
+
76
+ - [SMS channel](/docs/channels/sms)
77
+ - [Webhooks](/docs/guides/webhooks)
78
+ - [Channel send result](/docs/reference/channel-result)
79
+
80
+ ## Next
81
+
82
+ <Cards>
83
+ <Card title="SMS channel" href="/docs/channels/sms" />
84
+ <Card title="Webhooks" href="/docs/guides/webhooks" />
85
+ </Cards>
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/transports/fallback.ts"],
4
- "sourcesContent": [
5
- "/**\n * @module\n * FallbackTransport — routes through an ordered list of transports,\n * advancing to the next when the current one fails. Use for provider\n * failover: if your primary provider has an outage, the next takes over.\n * Composes with RetryTransport — wrap each entry in RetryTransport to\n * retry within a provider before failing over to the next.\n *\n * @example\n * ```ts\n * import { FallbackTransport } from \"sently/transports/fallback\";\n * import { RetryTransport } from \"sently/transports/retry\";\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { SESTransport } from \"sently/transports/ses\";\n *\n * const transport = new FallbackTransport(\n * [\n * new RetryTransport(new ResendTransport({ apiKey })),\n * new RetryTransport(new SESTransport({ accessKeyId, secretAccessKey })),\n * ],\n * { cooldownMs: 300_000 },\n * );\n * ```\n */\nimport { SentlyError } from \"../core/errors.js\";\nimport { getProviderLabel } from \"../core/provider-label.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\n\n/** One failed provider attempt collected during failover. */\nexport interface FallbackAttempt {\n /** Provider label (e.g. `\"resend\"`, `\"ses\"`). */\n provider: string;\n /** Error thrown by that transport. */\n error: unknown;\n}\n\n/** Per-provider result from {@link FallbackTransport.verifyAll}. */\nexport interface FallbackProviderVerifyResult {\n /** Provider label. */\n provider: string;\n /** Whether this provider verified successfully. */\n ok: boolean;\n /** Human-readable status from the provider verify call. */\n message?: string;\n}\n\n/** Result of verifying every transport in a fallback chain. */\nexport interface FallbackVerifyAllResult {\n /** True when at least one provider in the chain verified successfully. */\n ok: boolean;\n /** Per-provider verify results in chain order. */\n providers: FallbackProviderVerifyResult[];\n}\n\n/** Options for {@link FallbackTransport} failover behavior. */\nexport interface FallbackOptions {\n /**\n * Decide whether a given error should trigger failover to the next\n * transport. Default: fail over on everything EXCEPT permanent client\n * errors (HTTP 400/401/403, SMTP permanent 5xx auth failures), since\n * those will fail identically on every provider.\n */\n shouldFallback?: (error: unknown) => boolean;\n /** Optional callback fired before advancing to the next transport. */\n onFallback?: (failedIndex: number, error: unknown) => void;\n /**\n * After a failed send, skip this provider until the cooldown expires (ms).\n * Prevents hammering a failing provider during an outage.\n */\n cooldownMs?: number;\n /** Injectable clock for cooldown and testing. Default: `Date.now`. */\n now?: () => number;\n /**\n * Shared cooldown state across fallback instances (e.g. {@link WeightedFallbackTransport}).\n * @internal\n */\n cooldownState?: Map<string, number>;\n}\n\n/**\n * Thrown when every transport in the chain fails.\n * {@link FallbackAttempt} entries record which provider failed and why.\n */\nexport class FallbackError extends Error {\n /** All failed attempts in order — which provider failed and why. */\n readonly attempts: FallbackAttempt[];\n\n /** Creates an error summarizing every failed transport attempt. */\n constructor(attempts: FallbackAttempt[]) {\n super(`All ${attempts.length} transports failed`);\n this.name = \"FallbackError\";\n this.attempts = attempts;\n }\n}\n\n/** @deprecated Use {@link getProviderLabel} from `sently/core` patterns. */\nexport const inferProviderLabel = getProviderLabel;\n\nfunction defaultShouldFallback(error: unknown): boolean {\n if (error instanceof SentlyError && error.sentlyCode === \"SMTP_AUTH_FAILED\") {\n return false;\n }\n\n const status = (error as { statusCode?: number })?.statusCode;\n if (typeof status === \"number\") {\n if (status === 400 || status === 401 || status === 403) {\n return false;\n }\n return true;\n }\n\n const code = (error as { code?: number })?.code;\n if (code === 535) {\n return false;\n }\n\n return true;\n}\n\ntype MailerOnFallback = (\n failedProvider: string,\n nextProvider: string,\n error: unknown,\n) => void | Promise<void>;\n\n/**\n * Decorator transport that fails over through an ordered list of transports.\n */\nexport class FallbackTransport implements Transport {\n readonly provider = \"fallback\";\n\n private readonly shouldFallback: (error: unknown) => boolean;\n private readonly onFallback?: FallbackOptions[\"onFallback\"];\n private readonly cooldownMs: number;\n private readonly now: () => number;\n private readonly unhealthyUntil: Map<string, number>;\n private mailerOnFallback: MailerOnFallback | undefined;\n\n /**\n * Creates a failover transport over an ordered list of backends.\n * @throws {Error} When the transports array is empty.\n */\n constructor(\n private readonly transports: Transport[],\n options?: FallbackOptions,\n ) {\n if (transports.length === 0) {\n throw new Error(\"FallbackTransport requires at least one transport\");\n }\n this.shouldFallback = options?.shouldFallback ?? defaultShouldFallback;\n this.onFallback = options?.onFallback;\n this.cooldownMs = options?.cooldownMs ?? 0;\n this.now = options?.now ?? (() => Date.now());\n this.unhealthyUntil = options?.cooldownState ?? new Map<string, number>();\n }\n\n /**\n * Register a per-send onFallback callback from mailer lifecycle hooks.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnFallback(callback: MailerOnFallback | undefined): void {\n this.mailerOnFallback = callback;\n }\n\n private isCoolingDown(provider: string): boolean {\n const until = this.unhealthyUntil.get(provider);\n if (until === undefined) {\n return false;\n }\n if (this.now() >= until) {\n this.unhealthyUntil.delete(provider);\n return false;\n }\n return true;\n }\n\n private markUnhealthy(provider: string): void {\n if (this.cooldownMs > 0) {\n this.unhealthyUntil.set(provider, this.now() + this.cooldownMs);\n }\n }\n\n /** Sends through transports in order until one succeeds. */\n async send(message: MailOptions): Promise<SendResult> {\n const attempts: FallbackAttempt[] = [];\n const skipped: FallbackAttempt[] = [];\n\n for (let i = 0; i < this.transports.length; i++) {\n const transport = this.transports[i] as Transport;\n const provider = getProviderLabel(transport);\n\n if (this.isCoolingDown(provider)) {\n skipped.push({\n provider,\n error: new Error(`Provider ${provider} is in cooldown`),\n });\n continue;\n }\n\n try {\n const result = await transport.send(message);\n return {\n ...result,\n provider,\n providerIndex: i,\n };\n } catch (err) {\n attempts.push({ provider, error: err });\n this.markUnhealthy(provider);\n\n const nextIndex = this.findNextTryIndex(i);\n if (nextIndex === -1) {\n break;\n }\n if (this.shouldFallback(err) === false) {\n throw err;\n }\n\n const nextProvider = getProviderLabel(this.transports[nextIndex] as Transport);\n this.onFallback?.(i, err);\n if (this.mailerOnFallback !== undefined) {\n await this.mailerOnFallback(provider, nextProvider, err);\n }\n }\n }\n\n const allAttempts = [...attempts, ...skipped];\n if (allAttempts.length === 0) {\n throw new FallbackError([\n { provider: \"fallback\", error: new Error(\"All providers are in cooldown\") },\n ]);\n }\n\n throw new FallbackError(allAttempts);\n }\n\n private findNextTryIndex(currentIndex: number): number {\n for (let j = currentIndex + 1; j < this.transports.length; j++) {\n const provider = getProviderLabel(this.transports[j] as Transport);\n if (!this.isCoolingDown(provider)) {\n return j;\n }\n }\n return -1;\n }\n\n /**\n * Returns the result of the first transport whose verify() returns `ok: true`.\n * Use {@link verifyAll} for full-chain visibility.\n */\n async verify(): Promise<VerifyResult> {\n const all = await this.verifyAll();\n const firstOk = all.providers.find((entry) => entry.ok);\n if (firstOk !== undefined) {\n return {\n ok: true,\n provider: firstOk.provider,\n ...(firstOk.message !== undefined ? { message: firstOk.message } : {}),\n };\n }\n\n return {\n ok: false,\n provider: \"fallback\",\n message: \"no transport verified\",\n };\n }\n\n /** Verifies every transport in the chain and returns per-provider results. */\n async verifyAll(): Promise<FallbackVerifyAllResult> {\n const providers: FallbackProviderVerifyResult[] = [];\n\n for (const transport of this.transports) {\n const provider = getProviderLabel(transport);\n if (transport.verify === undefined) {\n providers.push({ provider, ok: false, message: \"no verify() method\" });\n continue;\n }\n\n try {\n const result = await transport.verify();\n providers.push({\n provider,\n ok: result.ok,\n ...(result.message !== undefined ? { message: result.message } : {}),\n });\n } catch (err) {\n providers.push({\n provider,\n ok: false,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return {\n ok: providers.some((entry) => entry.ok),\n providers,\n };\n }\n\n /** Calls close() on every transport; one failure does not block the others. */\n async close(): Promise<void> {\n await Promise.allSettled(this.transports.map((transport) => transport.close?.()));\n }\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "833078D22947C91264756E2164756E21",
9
- "names": []
10
- }
@@ -1,4 +0,0 @@
1
- import{N as S}from"./chunk-gp4fwrs8.js";import{O as E}from"./chunk-0qxws3kj.js";import{S as D}from"./chunk-93vqxxj2.js";import{W as z,X as f}from"./chunk-qtkd5bak.js";import{ca as k}from"./chunk-tamww15j.js";function N(j){return typeof j.setMailerOnRetry==="function"}function P(j){return typeof j.setMailerOnFallback==="function"}var v="SMTP config passed to transport-only createMailer. Use: import { createSMTPMailer } from 'sently' or import { createSMTPMailer } from 'sently/smtp'.";async function c(j){if(j.transport===void 0||"host"in j&&j.transport===void 0)throw new k(v,"INVALID_CONFIG");return new M(j.transport,j.plugins??[],j.hooks)}function b(j){return j.attachments!==void 0&&j.attachments.length>0}function y(j){return S(j)}function _(j,K){return{...j.messageId!==void 0?{messageId:j.messageId}:{},to:D(j.to),subject:j.subject,provider:y(K)}}class M{transport;plugins;hooks;constructor(j,K=[],V){this.transport=j;this.plugins=K;this.hooks=V}async send(j){let K=await f(j,this.plugins),V=_(K,this.transport);if(await z(this.hooks?.onSend,V),this.hooks?.onRetry!==void 0&&N(this.transport))this.transport.setMailerOnRetry((w,C)=>{z(this.hooks?.onRetry,V,w,C)});if(this.hooks?.onFallback!==void 0&&P(this.transport))this.transport.setMailerOnFallback((w,C,T)=>{z(this.hooks?.onFallback,V,w,C,T)});let A=performance.now();try{let w=await this.transport.send(K),C={...V,messageId:w.messageId};return await z(this.hooks?.onSuccess,C,w,performance.now()-A),w}catch(w){throw await z(this.hooks?.onError,V,w,performance.now()-A),w}finally{if(N(this.transport))this.transport.setMailerOnRetry(void 0);if(P(this.transport))this.transport.setMailerOnFallback(void 0)}}async processMessage(j){return f(j,this.plugins)}async sendBulk(j,K){if(j.length===0)return{total:0,sent:0,failed:0,results:[]};let V=Array(j.length),A=K?.stopOnError??!1,w=!1,C=async(X,$,B,Q,Z)=>{if(V[$]={status:"sent",result:B},Q!==void 0&&this.hooks!==void 0){let W=_(Q,this.transport);await z(this.hooks.onSend,W),await z(this.hooks.onSuccess,{...W,messageId:B.messageId},B,Z)}K?.onSuccess?.(X,$,B)},T=async(X,$,B,Q,Z)=>{if(V[$]={status:"failed",error:B},Q!==void 0&&this.hooks!==void 0){let W=_(Q,this.transport);await z(this.hooks.onSend,W),await z(this.hooks.onError,W,B,Z)}if(K?.onError?.(X,$,B),A)w=!0};if(this.transport.sendBatch){let X=[],$=[];for(let[B,Q]of j.entries())if(b(Q))$.push({index:B,message:Q});else X.push({index:B,message:Q});if(X.length>0&&!w){let B=this.transport.batchMax??X.length,Q=K?.rateDelta??2,Z=Q>0?new E(Q,K?.rateLimit??1000,K?.now):null;for(let W=0;W<X.length&&!w;W+=B){let U=X.slice(W,W+B);if(Z)await Z.acquire();try{let Y=await Promise.all(U.map(({message:G})=>this.processMessage(G))),J=performance.now(),H=await this.transport.sendBatch(Y),I=performance.now()-J;for(let G=0;G<U.length;G++){let R=U[G],L=Y[G],F=H[G];if(F===void 0)await T(R.message,R.index,Error("Batch response missing result for message"),L,I);else if(F.batchError!==void 0)await T(R.message,R.index,F.batchError,L,I);else await C(R.message,R.index,F,L,I)}}catch(Y){let J=await Promise.all(U.map(({message:H})=>this.processMessage(H)));for(let H=0;H<U.length;H++){if(w)break;let I=U[H];await T(I.message,I.index,Y,J[H])}}}}if($.length>0&&!w){let B=K?.concurrency??1,Q=[...$],Z=0;await new Promise((W)=>{let U=()=>{if(Q.length===0&&Z===0||w)W()},Y=()=>{if(Q.length===0||w){U();return}let J=Q.shift();if(J===void 0){U();return}Z++,this.send(J.message).then((H)=>{V[J.index]={status:"sent",result:H},K?.onSuccess?.(J.message,J.index,H)}).catch((H)=>{if(V[J.index]={status:"failed",error:H},K?.onError?.(J.message,J.index,H),A)w=!0}).finally(()=>{Z--,Y(),U()})};for(let J=0;J<B;J++)Y()})}}else{let X=K?.concurrency??1,$=[...j.entries()],B=0;await new Promise((Q)=>{let Z=()=>{if($.length===0&&B===0||w)Q()},W=()=>{if($.length===0||w){Z();return}let U=$.shift();if(U===void 0){Z();return}let[Y,J]=U;B++,this.send(J).then((H)=>{V[Y]={status:"sent",result:H},K?.onSuccess?.(J,Y,H)}).catch((H)=>{if(V[Y]={status:"failed",error:H},K?.onError?.(J,Y,H),A)w=!0}).finally(()=>{B--,W(),Z()})};for(let U=0;U<X;U++)W()})}let q=0,O=0;for(let X of V){if(X===void 0)continue;if(X.status==="sent")q++;else O++}return{total:j.length,sent:q,failed:O,results:V}}verify(){if(this.transport.verify)return this.transport.verify();return Promise.resolve({ok:!0,provider:"mailer"})}close(){if(this.transport.close)return this.transport.close();return Promise.resolve()}}
2
- export{c as L,M};
3
-
4
- //# debugId=DF7737883683888864756E2164756E21