sently 1.0.0 → 1.1.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 (45) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +20 -5
  4. package/dist/chunk-z1589fjk.js.map +2 -2
  5. package/dist/core/push-types.d.ts +53 -4
  6. package/dist/transports/inbucket.d.ts +196 -0
  7. package/dist/transports/inbucket.js +3 -0
  8. package/dist/transports/inbucket.js.map +10 -0
  9. package/dist/transports/mailpit.d.ts +108 -8
  10. package/dist/transports/mailpit.js +2 -2
  11. package/dist/transports/mailpit.js.map +3 -3
  12. package/dist/transports/taqnyat-sms.d.ts +85 -0
  13. package/dist/transports/taqnyat-sms.js +2 -2
  14. package/dist/transports/taqnyat-sms.js.map +3 -3
  15. package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
  16. package/dist/transports/taqnyat-whatsapp.js +2 -2
  17. package/dist/transports/taqnyat-whatsapp.js.map +3 -3
  18. package/dist/transports/webpush.d.ts +24 -1
  19. package/dist/transports/webpush.js +2 -2
  20. package/dist/transports/webpush.js.map +3 -3
  21. package/dist/webhooks/sndr.js +2 -2
  22. package/dist/webhooks/sndr.js.map +3 -3
  23. package/package.json +7 -2
  24. package/site/content/docs/ai/llms-txt.mdx +2 -0
  25. package/site/content/docs/channels/email.mdx +2 -1
  26. package/site/content/docs/channels/push.mdx +3 -1
  27. package/site/content/docs/decorators/preview.mdx +2 -1
  28. package/site/content/docs/get-started/entrypoints.mdx +1 -1
  29. package/site/content/docs/get-started/support-matrix.mdx +2 -2
  30. package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
  31. package/site/content/docs/guides/webhooks.mdx +2 -0
  32. package/site/content/docs/guides/webpush-interop.mdx +9 -1
  33. package/site/content/docs/reference/exports.mdx +1 -1
  34. package/site/content/docs/reference/push-options.mdx +22 -5
  35. package/site/content/docs/transports/inbucket.mdx +200 -0
  36. package/site/content/docs/transports/index.mdx +2 -3
  37. package/site/content/docs/transports/mailpit.mdx +114 -14
  38. package/site/content/docs/transports/meta.json +5 -4
  39. package/site/content/docs/transports/smtp.mdx +3 -2
  40. package/site/content/docs/transports/sndr.mdx +68 -5
  41. package/site/content/docs/transports/taqnyat.mdx +361 -0
  42. package/site/content/docs/transports/webpush.mdx +208 -17
  43. package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
  44. package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
  45. package/site/content/docs/transports/taqnyat-whatsapp.mdx +0 -40
@@ -5,39 +5,230 @@ icon: Truck
5
5
  source: "src/transports/webpush.ts"
6
6
  ---
7
7
 
8
- Encrypt and send browser notifications with VAPID.
8
+ Encrypt and send browser notifications with VAPID — the welcome ping, the
9
+ “report ready” alert, or a silent data sync to a service worker.
9
10
 
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
+ <LiveVerified>
12
+ Browser notification send with VAPID succeeded against a real push service (okengine live verification).
13
+ </LiveVerified>
11
14
 
12
- ## Configuration
15
+ <Callout title="The one rule">
16
+ Create this transport under `createPushSender` and pass a browser
17
+ `subscription` — not an FCM device token.
18
+ </Callout>
13
19
 
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `vapidPublicKey` | `string` | required |
17
- | `vapidPrivateKey` | `string` | required |
18
- | `subject` | `string` | required |
19
- | `allowedEndpointHosts` | `string[]` | optional |
20
+ ## Quick start
20
21
 
21
22
  <Steps>
22
- <Step title="Configure the transport">
23
+
24
+ <Step>
25
+ ### Generate VAPID keys
26
+
27
+ ```ts
28
+ import { generateVapidKeys } from "sently/transports/webpush";
29
+
30
+ const { publicKey, privateKey } = await generateVapidKeys();
31
+ // Store privateKey in env / secrets manager. Use publicKey in the browser subscribe call.
32
+ ```
33
+
34
+ </Step>
35
+
36
+ <Step>
37
+ ### Configure the sender
23
38
 
24
39
  ```ts
25
40
  import { createPushSender } from "sently/push";
26
41
  import { WebPushTransport } from "sently/transports/webpush";
27
42
 
28
- const sender = createPushSender({ transport: new WebPushTransport({ vapidPublicKey: "...", vapidPrivateKey: "...", subject: "mailto:you@example.com" }) });
43
+ const push = createPushSender({
44
+ transport: new WebPushTransport({
45
+ vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,
46
+ vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,
47
+ subject: "mailto:you@example.com",
48
+ }),
49
+ });
29
50
  ```
30
51
 
31
- </Step>
32
- <Step title="Send with the channel API">
52
+ </Step>
53
+
54
+ <Step>
55
+ ### Send
33
56
 
34
57
  ```ts
35
- await sender.send({ subscription, title: "Hello", body: "World" });
58
+ await push.send({
59
+ subscription,
60
+ title: "Report ready",
61
+ body: "Your weekly report is ready to view.",
62
+ urgency: "high",
63
+ topic: "report-ready",
64
+ });
36
65
  ```
37
66
 
38
- </Step>
67
+ </Step>
68
+
39
69
  </Steps>
40
70
 
41
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
71
+ ## Configuration
72
+
73
+ | Option | Type | Default or requirement |
74
+ | --- | --- | --- |
75
+ | `vapidPublicKey` | `string` | required — base64url uncompressed P-256 (65 bytes) |
76
+ | `vapidPrivateKey` | `string` | required — base64url raw private key (32 bytes) |
77
+ | `subject` | `string` | required — `mailto:you@example.com` or `https://example.com/contact` |
78
+ | `allowedEndpointHosts` | `string[]` | optional — exact hostnames for private push relays |
79
+
80
+ ## Send options (Web Push)
81
+
82
+ | Field | Type | Notes |
83
+ | --- | --- | --- |
84
+ | `subscription` | `PushSubscription` | required — `endpoint`, `keys.p256dh`, `keys.auth` |
85
+ | `title` / `body` | `string` | required together for a visible notification |
86
+ | `data` | `Record<string, unknown>` | optional; required for `silent` / data-only |
87
+ | `icon` / `badge` / `image` | `string` | Notification API URLs |
88
+ | `tag` | `string` | replace an existing notification with the same tag |
89
+ | `actions` | `{ action, title, icon? }[]` | action buttons |
90
+ | `requireInteraction` | `boolean` | keep open until the user interacts |
91
+ | `renotify` | `boolean` | re-alert when replacing by `tag` |
92
+ | `ttl` | `number` | seconds (default `2419200` / 28 days) |
93
+ | `urgency` | `"very-low" \| "low" \| "normal" \| "high"` | RFC 8030 `Urgency` header |
94
+ | `topic` | `string` | RFC 8030 `Topic` — 1–32 printable ASCII; collapses pending messages |
95
+ | `silent` | `boolean` | encrypt only `data` (no visible fields); requires `data` |
96
+ | `messageId` | `string` | optional client id |
97
+
98
+ ## Features
99
+
100
+ Pick a branch. Channel send goes through `push`; key generation is imported from
101
+ `sently/transports/webpush`.
102
+
103
+ <Tabs items={["Send", "Urgency", "Topic", "Rich", "Silent", "Keys"]}>
104
+ <Tab value="Send">
105
+
106
+ Visible notification via the channel sender.
107
+
108
+ ```ts
109
+ await push.send({
110
+ subscription,
111
+ title: "Report ready",
112
+ body: "Your weekly report is ready to view.",
113
+ });
114
+ ```
115
+
116
+ </Tab>
117
+ <Tab value="Urgency">
118
+
119
+ RFC 8030 `Urgency` header — delivery priority hint for the push service.
120
+
121
+ ```ts
122
+ await push.send({
123
+ subscription,
124
+ title: "Payment failed",
125
+ body: "Update your card to keep service running.",
126
+ urgency: "high",
127
+ });
128
+ ```
129
+
130
+ </Tab>
131
+ <Tab value="Topic">
132
+
133
+ RFC 8030 `Topic` — a newer message replaces a pending one with the same topic.
134
+
135
+ ```ts
136
+ await push.send({
137
+ subscription,
138
+ title: "Order update",
139
+ body: "Your package is out for delivery.",
140
+ topic: "order-42",
141
+ ttl: 3600,
142
+ });
143
+ ```
144
+
145
+ </Tab>
146
+ <Tab value="Rich">
147
+
148
+ Notification API fields encrypted into the JSON payload for the service worker.
149
+
150
+ ```ts
151
+ await push.send({
152
+ subscription,
153
+ title: "Report ready",
154
+ body: "Tap to open your weekly report.",
155
+ icon: "https://example.com/icon.png",
156
+ badge: "https://example.com/badge.png",
157
+ image: "https://example.com/hero.png",
158
+ tag: "report-ready",
159
+ requireInteraction: true,
160
+ renotify: true,
161
+ actions: [{ action: "open", title: "Open" }],
162
+ data: { reportId: "wk-12" },
163
+ });
164
+ ```
165
+
166
+ </Tab>
167
+ <Tab value="Silent">
168
+
169
+ Data-only / silent push — encrypt `data` without visible notification fields.
170
+ The service worker must handle `push` without calling `showNotification`.
171
+
172
+ ```ts
173
+ await push.send({
174
+ subscription,
175
+ silent: true,
176
+ data: { sync: "inbox", since: "2026-08-02T00:00:00Z" },
177
+ });
178
+
179
+ // Same shape without the flag: omit title/body and pass data.
180
+ await push.send({
181
+ subscription,
182
+ data: { ping: 1 },
183
+ });
184
+ ```
185
+
186
+ </Tab>
187
+ <Tab value="Keys">
188
+
189
+ Generate a VAPID key pair (`generateVapidKeys` — not on `createPushSender`).
190
+
191
+ ```ts
192
+ import { generateVapidKeys } from "sently/transports/webpush";
193
+
194
+ const { publicKey, privateKey } = await generateVapidKeys();
195
+ // Store privateKey in secrets. Use publicKey in pushManager.subscribe.
196
+ ```
197
+
198
+ </Tab>
199
+ </Tabs>
200
+
201
+ Invalid `urgency` / `topic`, `silent` without `data`, or a partial visible
202
+ payload (`title` without `body`) throw `WebPushError` (`provider: "webpush"`)
203
+ with status `400` before fetch.
204
+
205
+ ## Troubleshooting
206
+
207
+ <Accordions>
208
+ <Accordion title="Should I call the provider SDK?">
209
+ No. Use the sently sender; provider-specific extras stay on the transport module.
210
+ </Accordion>
211
+ <Accordion title="Why does construction throw on subject?">
212
+ VAPID `subject` must be a real `mailto:` address or `https:` URL. Values like `@oke.local` (no prefix) are rejected immediately as `WebPushError` so push services never return a confusing 403 later.
213
+ </Accordion>
214
+ <Accordion title="Why does silent send fail?">
215
+ `silent: true` requires `data`. Without it the transport throws `WebPushError` with status `400`.
216
+ </Accordion>
217
+ <Accordion title="Why was my topic rejected?">
218
+ `topic` must be 1–32 printable ASCII characters (no spaces). Invalid values throw before fetch.
219
+ </Accordion>
220
+ </Accordions>
221
+
222
+ ## Learn more
223
+
224
+ - [Push channel](/docs/channels/push) — sender, hooks, plugins
225
+ - [Push options](/docs/reference/push-options) — union fields for Web Push and FCM
226
+ - [Web Push interoperability](/docs/guides/webpush-interop) — browser subscription shape
227
+
228
+ ## Next
42
229
 
43
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
230
+ <Cards>
231
+ <Card title="Push channel" href="/docs/channels/push" />
232
+ <Card title="Push options" href="/docs/reference/push-options" />
233
+ <Card title="FCM" href="/docs/transports/fcm" />
234
+ </Cards>
@@ -1,41 +0,0 @@
1
- ---
2
- title: Taqnyat Mail
3
- description: Send email through the Taqnyat Email API.
4
- icon: Truck
5
- source: "src/transports/taqnyat-mail.ts"
6
- ---
7
-
8
- Send email through the Taqnyat Email API.
9
-
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
-
12
- ## Configuration
13
-
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `bearerToken` | `string` | required |
17
- | `campaignName` | `string` | required |
18
-
19
- <Steps>
20
- <Step title="Configure the transport">
21
-
22
- ```ts
23
- import { createMailer } from "sently/mailer";
24
- import { TaqnyatMailTransport } from "sently/transports/taqnyat-mail";
25
-
26
- const sender = createMailer({ transport: new TaqnyatMailTransport({ bearerToken: process.env.TAQNYAT_MAIL_TOKEN!, campaignName: "transactional" }) });
27
- ```
28
-
29
- </Step>
30
- <Step title="Send with the channel API">
31
-
32
- ```ts
33
- await sender.send({ from: "hello@example.com", to: "person@example.com", subject: "Hello", text: "Hi" });
34
- ```
35
-
36
- </Step>
37
- </Steps>
38
-
39
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
40
-
41
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
@@ -1,41 +0,0 @@
1
- ---
2
- title: Taqnyat SMS
3
- description: Send SMS through the Taqnyat API.
4
- icon: Truck
5
- source: "src/transports/taqnyat-sms.ts"
6
- ---
7
-
8
- Send SMS through the Taqnyat API.
9
-
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
-
12
- ## Configuration
13
-
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `bearerToken` | `string` | required |
17
- | `sender` | `string` | required |
18
-
19
- <Steps>
20
- <Step title="Configure the transport">
21
-
22
- ```ts
23
- import { createSmsSender } from "sently/sms";
24
- import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
25
-
26
- const sender = createSmsSender({ transport: new TaqnyatSmsTransport({ bearerToken: "...", sender: "MyBrand" }) });
27
- ```
28
-
29
- </Step>
30
- <Step title="Send with the channel API">
31
-
32
- ```ts
33
- await sender.send({ to: "+15551234567", body: "Hello" });
34
- ```
35
-
36
- </Step>
37
- </Steps>
38
-
39
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
40
-
41
- <Cards><Card title="Sms channel" href="/docs/channels/sms" /></Cards>
@@ -1,40 +0,0 @@
1
- ---
2
- title: Taqnyat WhatsApp
3
- description: Send WhatsApp templates and session text through Taqnyat.
4
- icon: Truck
5
- source: "src/transports/taqnyat-whatsapp.ts"
6
- ---
7
-
8
- Send WhatsApp templates and session text through Taqnyat.
9
-
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
-
12
- ## Configuration
13
-
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `bearerToken` | `string` | required |
17
-
18
- <Steps>
19
- <Step title="Configure the transport">
20
-
21
- ```ts
22
- import { createWhatsAppSender } from "sently/whatsapp";
23
- import { TaqnyatWhatsAppTransport } from "sently/transports/taqnyat-whatsapp";
24
-
25
- const sender = createWhatsAppSender({ transport: new TaqnyatWhatsAppTransport({ bearerToken: "..." }) });
26
- ```
27
-
28
- </Step>
29
- <Step title="Send with the channel API">
30
-
31
- ```ts
32
- await sender.send({ to: "15551234567", template: { name: "welcome", language: "en_US" } });
33
- ```
34
-
35
- </Step>
36
- </Steps>
37
-
38
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
39
-
40
- <Cards><Card title="Whatsapp channel" href="/docs/channels/whatsapp" /></Cards>