sently 0.10.0 → 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 (45) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +42 -2
  3. package/README.md +21 -18
  4. package/SECURITY.md +59 -0
  5. package/dist/chunk-z1589fjk.js.map +1 -1
  6. package/dist/core/push-types.d.ts +1 -1
  7. package/dist/transports/mailpit.d.ts +166 -0
  8. package/dist/transports/mailpit.js +3 -0
  9. package/dist/transports/mailpit.js.map +10 -0
  10. package/package.json +15 -2
  11. package/site/content/docs/ai/llms-txt.mdx +47 -2
  12. package/site/content/docs/channels/email.mdx +3 -0
  13. package/site/content/docs/channels/index.mdx +3 -1
  14. package/site/content/docs/channels/push.mdx +4 -0
  15. package/site/content/docs/decorators/fallback.mdx +5 -0
  16. package/site/content/docs/decorators/index.mdx +2 -1
  17. package/site/content/docs/decorators/preview.mdx +52 -9
  18. package/site/content/docs/get-started/entrypoints.mdx +5 -3
  19. package/site/content/docs/get-started/index.mdx +8 -2
  20. package/site/content/docs/get-started/introduction.mdx +7 -3
  21. package/site/content/docs/get-started/meta.json +4 -1
  22. package/site/content/docs/get-started/migrate-nodemailer.mdx +132 -5
  23. package/site/content/docs/get-started/non-goals.mdx +57 -0
  24. package/site/content/docs/get-started/stability.mdx +69 -0
  25. package/site/content/docs/get-started/support-matrix.mdx +62 -0
  26. package/site/content/docs/guides/compare.mdx +87 -0
  27. package/site/content/docs/guides/failover.mdx +121 -0
  28. package/site/content/docs/guides/index.mdx +2 -0
  29. package/site/content/docs/guides/meta.json +2 -0
  30. package/site/content/docs/guides/security.mdx +38 -8
  31. package/site/content/docs/index.mdx +78 -4
  32. package/site/content/docs/meta.json +0 -1
  33. package/site/content/docs/reference/exports.mdx +5 -2
  34. package/site/content/docs/reference/push-options.mdx +14 -0
  35. package/site/content/docs/transports/fcm.mdx +8 -4
  36. package/site/content/docs/transports/index.mdx +9 -3
  37. package/site/content/docs/transports/mailpit.mdx +123 -0
  38. package/site/content/docs/transports/meta.json +1 -0
  39. package/site/content/docs/transports/smtp.mdx +61 -22
  40. package/site/content/docs/quick-start/email.mdx +0 -40
  41. package/site/content/docs/quick-start/index.mdx +0 -15
  42. package/site/content/docs/quick-start/meta.json +0 -5
  43. package/site/content/docs/quick-start/push.mdx +0 -53
  44. package/site/content/docs/quick-start/sms.mdx +0 -36
  45. package/site/content/docs/quick-start/whatsapp.mdx +0 -38
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # sently — agent contract
2
2
 
3
- sently is a runtime-agnostic TypeScript messaging library (email, SMS, WhatsApp, Web Push, FCM) for Node.js, Bun, Deno, and Cloudflare Workers. ESM-only, zero runtime dependencies.
3
+ sently is a runtime-agnostic TypeScript channel-delivery library (email, SMS, WhatsApp, Web Push, FCM) for Node.js, Bun, Deno, and Cloudflare Workers. ESM-only, zero runtime dependencies. One sender shape / error model / retry path as channels grow — not a hosted orchestration platform.
4
4
 
5
5
  ## Sently-first provider model
6
6
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.0.0] — 2026-08-01
6
+
7
+ First stable release. Channel contracts and Supported transports are semver-frozen;
8
+ npm publishes ship with provenance attestations and Trusted Publishing (OIDC).
9
+ sently is the channel-delivery layer — preference centers, digests, workflows, and
10
+ in-app inboxes sit on top (custom or Novu / Knock / Courier), not instead of it.
11
+
12
+ ### ✨ Added
13
+
14
+ - **Stability policy** — what is frozen at 1.x vs allowed without a major
15
+ (`PushOptions` union, `providerIndex`, email-only preview / idempotency)
16
+ - **Support matrix** — Supported vs Available transports and runtimes
17
+ - **Non-goals** — library-not-platform boundaries (no preference centers /
18
+ digests / workflow builders inside sently)
19
+ - **Security policy** — root `SECURITY.md` (private reporting, response targets,
20
+ supported versions for patches, Trusted Publisher setup notes)
21
+ - **Compare guide** — positioning vs vendor SDK piles, Nodemailer, and
22
+ Novu / Knock / Courier
23
+ - **Failover guide** — cross-channel retry-then-fallback recipes
24
+ - **Mailpit transport** — `sently/transports/mailpit` for local email capture
25
+ (SMTP defaults `localhost:1025`, REST helpers for list / get / delete)
26
+
27
+ ### ♻️ Changed
28
+
29
+ - **Messaging positioning** — homepage, README, `llms.txt`, and package
30
+ description/keywords lead with multi-channel scope creep (one sender / error /
31
+ retry model); bundle size as supporting evidence for edge / Workers; FCM
32
+ documented as current HTTP API support, not a feature boast
33
+ - **Handbook landing** — install → send loop, channel map, where-next table,
34
+ and library-vs-platform troubleshooting
35
+ - **Docs IA** — remove duplicate Quick start section; Channels is the first-send
36
+ path; cross-links for stability, support matrix, failover, and security
37
+ - **Migrate from Nodemailer** — diff-style transporter and `sendMail` → `send`
38
+ swaps, field map, SMTP → HTTP follow-on, fuller options tables
39
+ - **npm publish** — OIDC Trusted Publishing first, optional `NPM_TOKEN`
40
+ fallback; continues to use `npm publish --provenance` from CI (npm CLI ≥11.5.1
41
+ check in the publish workflow)
42
+ - **Semver** — public channel factories, contracts, hooks, `SentlyError` codes,
43
+ `ChannelSendResult`, and published subpaths follow 1.x stability
44
+
5
45
  ## [0.10.0] — 2026-08-01
6
46
 
7
47
  ### ✨ Added
@@ -14,8 +54,8 @@
14
54
  WhatsApp / push senders wire `onRetry` / `onFallback` like email
15
55
  - **Unifonic SMS** — `sently/transports/unifonic` (`AppSid` + el.cloud REST
16
56
  send); webhook parser at `sently/webhooks/unifonic`
17
- - **FCM push** — `sently/transports/fcm` (HTTP v1 + service-account JWT, zero
18
- Google SDK)
57
+ - **FCM push** — `sently/transports/fcm` (current FCM HTTP API + service-account
58
+ JWT, no Google SDK)
19
59
  - **Non-email webhooks** — `DeliveryEvent` plus parsers for Twilio SMS and
20
60
  WhatsApp Cloud (`sently/webhooks/twilio-sms`, `whatsapp-cloud`);
21
61
  `toDeliveryEvent` maps `EmailEvent` into the shared shape; Twilio and
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  </p>
24
24
 
25
25
  <p align="center">
26
- <em>Stop wiring vendor SDKs into every channel. One sender shape for email, SMS, WhatsApp, and push swap the transport, keep your call sites. Node, Bun, Deno, Workers.</em>
26
+ <em>Start with email, add SMS and push later — one sender shape, one error model, one retry path. Swap the transport; keep your call sites. Node, Bun, Deno, Workers.</em>
27
27
  </p>
28
28
 
29
29
  <p align="center">
@@ -33,11 +33,6 @@
33
33
  <a href="https://www.npmjs.com/package/sently"><code>sently</code></a>
34
34
  </p>
35
35
 
36
- > [!WARNING]
37
- > **Early development (`v0.x`) — API may change.**
38
- >
39
- > Pin an exact version for production until v1.0.0. See [CHANGELOG](CHANGELOG.md).
40
-
41
36
  ## Install
42
37
 
43
38
  ```bash
@@ -78,18 +73,24 @@ Full walkthrough: [Get started](https://sently.omqkhafi.dev/docs/get-started).
78
73
 
79
74
  ## Why sently?
80
75
 
81
- Nodemailer is Node.js–only and ships the full mail stack on every import (~59 KB gzip for [v9.0.3](https://bundlephobia.com/package/nodemailer@9.0.3)). sently is tree-shakeable, multi-runtime, and multi-channel.
76
+ Teams usually start on one channel (often email), then add SMS and push. Each vendor SDK brings its own auth, retries, and error shapes. sently keeps **one sender shape**, **one `SentlyError` model**, and **one retry/fallback path** as you add channels.
77
+
78
+ **Library, not platform.** sently is the channel-delivery layer. Preference centers, digests, workflow builders, and in-app inboxes — custom logic or tools like [Novu](https://novu.co), [Knock](https://knock.app), or [Courier](https://www.courier.com) — sit **on top of** sently, not instead of it.
79
+
80
+ Compared with email-only stacks (e.g. Nodemailer) and a pile of vendor clients:
81
+
82
+ | | Typical stack | sently |
83
+ | ----------------- | ------------------------------------- | ------------------------------------------- |
84
+ | As channels grow | New SDK per channel | Same sender factories |
85
+ | Failures | Per-vendor exceptions | Stable `SentlyError` codes |
86
+ | Reliability | Ad-hoc per client | `RetryTransport` + `FallbackTransport` |
87
+ | Providers | Vendor clients in app code | Pluggable transports |
88
+ | Runtimes | Often Node only | Node, Bun, Deno, CF Workers |
89
+ | Edge / Workers | Fat SDKs hurt cold start | Tree-shakeable (~6.3 KB HTTP · ~14.9 KB SMTP) |
90
+ | Module format | Often CJS | ESM only |
91
+ | Dependencies | Varies | 0 runtime deps |
82
92
 
83
- | | Nodemailer | sently |
84
- | ----------------- | ----------------------- | ------------------------------------------- |
85
- | Bundle size | ~59 KB gzip always | ~6.3 KB HTTP · ~14.9 KB SMTP |
86
- | Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
87
- | Module format | CommonJS | ESM only |
88
- | Dependencies | 0 | 0 |
89
- | Channels | Email | Email · SMS · WhatsApp · Push |
90
- | HTTP transports | via plugins | built-in subpaths |
91
- | Provider failover | — | `FallbackTransport` + weighted routing |
92
- | TypeScript | `@types/nodemailer` | built-in |
93
+ More detail: [Compare](https://sently.omqkhafi.dev/docs/guides/compare) · [Stability](https://sently.omqkhafi.dev/docs/get-started/stability) · [Support matrix](https://sently.omqkhafi.dev/docs/get-started/support-matrix).
93
94
 
94
95
  ## Entrypoints
95
96
 
@@ -113,10 +114,12 @@ Nodemailer is Node.js–only and ships the full mail stack on every import (~59
113
114
  | Get started | [/docs/get-started](https://sently.omqkhafi.dev/docs/get-started) |
114
115
  | Channels | [/docs/channels](https://sently.omqkhafi.dev/docs/channels) |
115
116
  | Transports | [/docs/transports](https://sently.omqkhafi.dev/docs/transports) |
117
+ | Compare | [/docs/guides/compare](https://sently.omqkhafi.dev/docs/guides/compare) |
116
118
  | Agents index | [/llms.txt](https://sently.omqkhafi.dev/llms.txt) |
117
119
  | Changelog | [`CHANGELOG.md`](CHANGELOG.md) |
120
+ | Security | [`SECURITY.md`](SECURITY.md) |
118
121
  | Agents | [`AGENTS.md`](AGENTS.md) |
119
122
 
120
123
  Local docs: `bun run site:dev`. Verify: `bun run verify`.
121
124
 
122
- Pre-1.0. Published on [npm](https://www.npmjs.com/package/sently) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
125
+ **1.0.0** — semver-stable channel contracts. Published on [npm](https://www.npmjs.com/package/sently) (provenance + Trusted Publishing) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
package/SECURITY.md ADDED
@@ -0,0 +1,59 @@
1
+ # Security Policy
2
+
3
+ sently is a messaging library that holds provider API keys and tokens in the installing application at runtime. Treat compromised publishes and credential leaks as high severity.
4
+
5
+ ## Reporting a vulnerability
6
+
7
+ **Do not open a public GitHub issue for security reports.**
8
+
9
+ Use [GitHub Security Advisories](https://github.com/alialnaghmoush/sently/security/advisories/new) for this repository so the report stays private until a fix is ready.
10
+
11
+ Include:
12
+
13
+ - Affected package version(s)
14
+ - Impact (credential exfiltration, unauthorized send, SSRF, signature bypass, …)
15
+ - Reproduction steps or a minimal proof of concept when possible
16
+
17
+ ## Response targets
18
+
19
+ | Stage | Target |
20
+ | --- | --- |
21
+ | Initial acknowledgement | Within **72 hours** |
22
+ | Triage (severity / severity) | Within **7 days** of acknowledgement |
23
+ | Fix or mitigation guidance | As soon as practical; critical issues are prioritized over feature work |
24
+
25
+ These are goals, not SLAs. Complex issues may take longer; we will keep the reporter updated.
26
+
27
+ ## Supported versions (security patches)
28
+
29
+ After **1.0.0**, security fixes land on the current **1.x** line (latest minor/patch). See the [stability policy](https://sently.omqkhafi.dev/docs/get-started/stability) for what is semver-frozen.
30
+
31
+ | Version | Security patches |
32
+ | --- | --- |
33
+ | `1.x` | Yes |
34
+ | `0.x` | Best effort only (pre-1.0; prefer upgrading to 1.x) |
35
+
36
+ ## Release integrity
37
+
38
+ npm publishes from CI (`.github/workflows/publish.yml`) use:
39
+
40
+ - **`npm publish --provenance`** — attestations linking the tarball to this public repository and the build
41
+ - **Trusted Publishing (OIDC)** — preferred path so a long-lived `NPM_TOKEN` is not required
42
+
43
+ ### One-time setup (package owner)
44
+
45
+ On [npmjs.com/package/sently](https://www.npmjs.com/package/sently) → **Settings** → **Trusted Publisher**:
46
+
47
+ 1. Bind **GitHub Actions**
48
+ 2. Repository: `alialnaghmoush/sently`
49
+ 3. Workflow: `publish.yml` (`.github/workflows/publish.yml`)
50
+ 4. Environment: `production` (must match the workflow `environment`)
51
+ 5. Confirm the npm package repository URL matches `package.json` → `repository`
52
+
53
+ Until one OIDC publish has succeeded, CI may fall back to a repository secret `NPM_TOKEN` / `NODE_AUTH_TOKEN` if present. Remove that secret after OIDC is verified.
54
+
55
+ ## Related hardening in the library
56
+
57
+ - Web Push endpoint allowlisting / SSRF checks
58
+ - Redaction of push endpoints and FCM tokens in hooks
59
+ - Webhook signature verification helpers for Supported providers
@@ -2,7 +2,7 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/core/push-types.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Shared push channel contracts — sibling types to email MailOptions/Transport/Hooks.\n *\n * Sently-first: this contract is the stable push surface. Provider transports\n * implement {@link PushTransport}. Web Push uses a browser subscription; FCM\n * uses a device registration token.\n */\nimport type { VerifyResult } from \"./types.js\";\n\n/** Browser push subscription as returned by the Push API. */\nexport interface PushSubscription {\n /** Push service endpoint URL. */\n endpoint: string;\n /** Client encryption keys for RFC 8291 payload encryption. */\n keys: {\n /** Base64url-encoded P-256 ECDH public key. */\n p256dh: string;\n /** Base64url-encoded authentication secret. */\n auth: string;\n };\n}\n\n/** Shared notification fields for Web Push and FCM. */\ninterface PushNotificationFields {\n /** Notification title. */\n title: string;\n /** Notification body text. */\n body: string;\n /** Arbitrary application data attached to the notification. */\n data?: Record<string, unknown>;\n /** Notification icon URL (Web Push / some FCM platforms). */\n icon?: string;\n /** Time-to-live in seconds for the push message. */\n ttl?: number;\n /** Client-supplied message identifier when known. */\n messageId?: string;\n}\n\n/** Options for sending a Web Push notification (VAPID / browser subscription). */\nexport interface WebPushOptions extends PushNotificationFields {\n /** Target browser push subscription. */\n subscription: PushSubscription;\n}\n\n/**\n * Options for sending via Firebase Cloud Messaging HTTP v1 (device token).\n * Data values are stringified by the FCM transport when needed.\n */\nexport interface FcmPushOptions extends PushNotificationFields {\n /** FCM registration token for the target device. */\n token: string;\n /** Optional image URL for the notification. */\n image?: string;\n}\n\n/** Options for sending a push notification (Web Push or FCM). */\nexport type PushOptions = WebPushOptions | FcmPushOptions;\n\n/** True when options target FCM (device token) rather than Web Push. */\nexport function isFcmPushOptions(options: PushOptions): options is FcmPushOptions {\n return \"token\" in options && typeof (options as FcmPushOptions).token === \"string\";\n}\n\n/** True when options target Web Push (browser subscription). */\nexport function isWebPushOptions(options: PushOptions): options is WebPushOptions {\n return (\n \"subscription\" in options &&\n typeof (options as WebPushOptions).subscription?.endpoint === \"string\"\n );\n}\n\n/** Result returned after a push notification is accepted by the push service. */\nexport interface PushSendResult {\n /** Client-generated or assigned message identifier. */\n messageId: string;\n /** Delivery status string (e.g. `\"accepted\"`). */\n status: string;\n /** Human-readable or raw status text (often the HTTP status). */\n response: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider?: string;\n /**\n * Zero-based index of the transport that handled the send in a fallback chain.\n * Set by {@link FallbackTransport}.\n */\n providerIndex?: number;\n}\n\n/** Pluggable push delivery backend (Web Push, FCM, …). */\nexport interface PushTransport {\n /**\n * Stable provider identifier for observability (e.g. `\"webpush\"`, `\"fcm\"`).\n * Prefer this over constructor-name inference in hooks.\n */\n readonly provider?: string;\n /** Send a push notification through this transport. */\n send(options: PushOptions): Promise<PushSendResult>;\n /** Test connectivity and credentials without sending. */\n verify?(): Promise<VerifyResult>;\n /** Release resources held by the transport. */\n close?(): Promise<void>;\n}\n\n/**\n * Context passed to push lifecycle hooks.\n * Uses a redacted subscription endpoint or device-token fingerprint (no natural `to`).\n */\nexport interface PushHookContext {\n /** Message-ID when known (from options or send result). */\n messageId?: string;\n /**\n * Redacted push target for observability: `origin/#<short-hash>` (Web Push)\n * or `fcm:#<short-hash>` (FCM device token).\n * Never the full endpoint URL or raw device token.\n */\n endpoint: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider: string;\n}\n\n/** Optional lifecycle hooks for metrics, tracing, and observability on every push send. */\nexport interface PushHooks {\n /** Fired before the transport sends the notification. */\n onSend?: (ctx: PushHookContext) => void | Promise<void>;\n /**\n * Fired after a successful send.\n * @param durationMs — elapsed milliseconds from send start to success (optional third argument).\n */\n onSuccess?: (\n ctx: PushHookContext,\n result: PushSendResult,\n durationMs?: number,\n ) => void | Promise<void>;\n /**\n * Fired when a send throws (error is re-thrown after the hook runs).\n * @param durationMs — elapsed milliseconds from send start to failure (optional third argument).\n */\n onError?: (ctx: PushHookContext, error: unknown, durationMs?: number) => void | Promise<void>;\n /** Fired before each retry attempt (requires {@link RetryTransport}). */\n onRetry?: (ctx: PushHookContext, attempt: number, error: unknown) => void | Promise<void>;\n /**\n * Fired when {@link FallbackTransport} fails over to the next provider.\n * Requires a fallback (or weighted fallback) transport in the push stack.\n */\n onFallback?: (\n ctx: PushHookContext,\n failedProvider: string,\n nextProvider: string,\n error: unknown,\n ) => void | Promise<void>;\n}\n\n/**\n * A push plugin transforms {@link PushOptions} before the transport sends.\n * Plugins run sequentially. Each receives the output of the previous.\n * Return a new options object — do not mutate the input.\n */\nexport type PushPlugin =\n | ((options: PushOptions) => PushOptions)\n | ((options: PushOptions) => Promise<PushOptions>);\n"
5
+ "/**\n * @module\n * Shared push channel contracts — sibling types to email MailOptions/Transport/Hooks.\n *\n * Sently-first: this contract is the stable push surface. Provider transports\n * implement {@link PushTransport}. Web Push uses a browser subscription; FCM\n * uses a device registration token.\n */\nimport type { VerifyResult } from \"./types.js\";\n\n/** Browser push subscription as returned by the Push API. */\nexport interface PushSubscription {\n /** Push service endpoint URL. */\n endpoint: string;\n /** Client encryption keys for RFC 8291 payload encryption. */\n keys: {\n /** Base64url-encoded P-256 ECDH public key. */\n p256dh: string;\n /** Base64url-encoded authentication secret. */\n auth: string;\n };\n}\n\n/** Shared notification fields for Web Push and FCM. */\ninterface PushNotificationFields {\n /** Notification title. */\n title: string;\n /** Notification body text. */\n body: string;\n /** Arbitrary application data attached to the notification. */\n data?: Record<string, unknown>;\n /** Notification icon URL (Web Push / some FCM platforms). */\n icon?: string;\n /** Time-to-live in seconds for the push message. */\n ttl?: number;\n /** Client-supplied message identifier when known. */\n messageId?: string;\n}\n\n/** Options for sending a Web Push notification (VAPID / browser subscription). */\nexport interface WebPushOptions extends PushNotificationFields {\n /** Target browser push subscription. */\n subscription: PushSubscription;\n}\n\n/**\n * Options for sending via Firebase Cloud Messaging (device token).\n * Data values are stringified by the FCM transport when needed.\n */\nexport interface FcmPushOptions extends PushNotificationFields {\n /** FCM registration token for the target device. */\n token: string;\n /** Optional image URL for the notification. */\n image?: string;\n}\n\n/** Options for sending a push notification (Web Push or FCM). */\nexport type PushOptions = WebPushOptions | FcmPushOptions;\n\n/** True when options target FCM (device token) rather than Web Push. */\nexport function isFcmPushOptions(options: PushOptions): options is FcmPushOptions {\n return \"token\" in options && typeof (options as FcmPushOptions).token === \"string\";\n}\n\n/** True when options target Web Push (browser subscription). */\nexport function isWebPushOptions(options: PushOptions): options is WebPushOptions {\n return (\n \"subscription\" in options &&\n typeof (options as WebPushOptions).subscription?.endpoint === \"string\"\n );\n}\n\n/** Result returned after a push notification is accepted by the push service. */\nexport interface PushSendResult {\n /** Client-generated or assigned message identifier. */\n messageId: string;\n /** Delivery status string (e.g. `\"accepted\"`). */\n status: string;\n /** Human-readable or raw status text (often the HTTP status). */\n response: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider?: string;\n /**\n * Zero-based index of the transport that handled the send in a fallback chain.\n * Set by {@link FallbackTransport}.\n */\n providerIndex?: number;\n}\n\n/** Pluggable push delivery backend (Web Push, FCM, …). */\nexport interface PushTransport {\n /**\n * Stable provider identifier for observability (e.g. `\"webpush\"`, `\"fcm\"`).\n * Prefer this over constructor-name inference in hooks.\n */\n readonly provider?: string;\n /** Send a push notification through this transport. */\n send(options: PushOptions): Promise<PushSendResult>;\n /** Test connectivity and credentials without sending. */\n verify?(): Promise<VerifyResult>;\n /** Release resources held by the transport. */\n close?(): Promise<void>;\n}\n\n/**\n * Context passed to push lifecycle hooks.\n * Uses a redacted subscription endpoint or device-token fingerprint (no natural `to`).\n */\nexport interface PushHookContext {\n /** Message-ID when known (from options or send result). */\n messageId?: string;\n /**\n * Redacted push target for observability: `origin/#<short-hash>` (Web Push)\n * or `fcm:#<short-hash>` (FCM device token).\n * Never the full endpoint URL or raw device token.\n */\n endpoint: string;\n /** Transport or provider identifier (e.g. `\"webpush\"`, `\"fcm\"`). */\n provider: string;\n}\n\n/** Optional lifecycle hooks for metrics, tracing, and observability on every push send. */\nexport interface PushHooks {\n /** Fired before the transport sends the notification. */\n onSend?: (ctx: PushHookContext) => void | Promise<void>;\n /**\n * Fired after a successful send.\n * @param durationMs — elapsed milliseconds from send start to success (optional third argument).\n */\n onSuccess?: (\n ctx: PushHookContext,\n result: PushSendResult,\n durationMs?: number,\n ) => void | Promise<void>;\n /**\n * Fired when a send throws (error is re-thrown after the hook runs).\n * @param durationMs — elapsed milliseconds from send start to failure (optional third argument).\n */\n onError?: (ctx: PushHookContext, error: unknown, durationMs?: number) => void | Promise<void>;\n /** Fired before each retry attempt (requires {@link RetryTransport}). */\n onRetry?: (ctx: PushHookContext, attempt: number, error: unknown) => void | Promise<void>;\n /**\n * Fired when {@link FallbackTransport} fails over to the next provider.\n * Requires a fallback (or weighted fallback) transport in the push stack.\n */\n onFallback?: (\n ctx: PushHookContext,\n failedProvider: string,\n nextProvider: string,\n error: unknown,\n ) => void | Promise<void>;\n}\n\n/**\n * A push plugin transforms {@link PushOptions} before the transport sends.\n * Plugins run sequentially. Each receives the output of the previous.\n * Return a new options object — do not mutate the input.\n */\nexport type PushPlugin =\n | ((options: PushOptions) => PushOptions)\n | ((options: PushOptions) => Promise<PushOptions>);\n"
6
6
  ],
7
7
  "mappings": "AA4DO,SAAS,CAAgB,CAAC,EAAiD,CAChF,MAAO,UAAW,GAAW,OAAQ,EAA2B,QAAU,SAIrE,SAAS,CAAgB,CAAC,EAAiD,CAChF,MACE,iBAAkB,GAClB,OAAQ,EAA2B,cAAc,WAAa",
8
8
  "debugId": "F4A2DC6CC43BC46D64756E2164756E21",
@@ -40,7 +40,7 @@ export interface WebPushOptions extends PushNotificationFields {
40
40
  subscription: PushSubscription;
41
41
  }
42
42
  /**
43
- * Options for sending via Firebase Cloud Messaging HTTP v1 (device token).
43
+ * Options for sending via Firebase Cloud Messaging (device token).
44
44
  * Data values are stringified by the FCM transport when needed.
45
45
  */
46
46
  export interface FcmPushOptions extends PushNotificationFields {
@@ -0,0 +1,166 @@
1
+ import { SentlyError } from "../core/errors.js";
2
+ import type { MailOptions, SendResult, SMTPAuth, SocketAdapter, TLSOptions, Transport, VerifyResult } from "../core/types.js";
3
+ /** Default SMTP host for a local Mailpit instance. */
4
+ export declare const MAILPIT_DEFAULT_HOST = "localhost";
5
+ /** Default SMTP port for a local Mailpit instance. */
6
+ export declare const MAILPIT_DEFAULT_PORT = 1025;
7
+ /** Default web UI / REST API base URL for a local Mailpit instance. */
8
+ export declare const MAILPIT_DEFAULT_API_URL = "http://localhost:8025";
9
+ /** Mailpit development transport configuration. */
10
+ export interface MailpitConfig {
11
+ /** SMTP hostname. Default: `"localhost"`. */
12
+ host?: string;
13
+ /** SMTP port. Default: `1025`. */
14
+ port?: number;
15
+ /** Use implicit TLS on connect. Default: `false` (Mailpit is plain SMTP). */
16
+ secure?: boolean;
17
+ /**
18
+ * Refuse AUTH over a non-TLS connection.
19
+ * Default: `false` so optional local SMTP auth works without TLS.
20
+ */
21
+ requireTLS?: boolean;
22
+ /** Optional SMTP authentication (Mailpit can accept any credentials). */
23
+ auth?: SMTPAuth;
24
+ /** TLS options when STARTTLS or implicit TLS is enabled. */
25
+ tls?: TLSOptions;
26
+ /** Socket connect timeout in milliseconds. */
27
+ connectionTimeout?: number;
28
+ /** Runtime socket adapter. Auto-detected on first send when omitted. */
29
+ adapter?: SocketAdapter;
30
+ /**
31
+ * Mailpit web UI / REST API base URL (no trailing slash).
32
+ * Default: `"http://localhost:8025"`.
33
+ */
34
+ apiUrl?: string;
35
+ /** Basic auth for the Mailpit UI/API when the instance requires it. */
36
+ apiAuth?: {
37
+ user: string;
38
+ pass: string;
39
+ };
40
+ }
41
+ /** Address object returned by the Mailpit REST API. */
42
+ export interface MailpitAddress {
43
+ /** Display name, may be empty. */
44
+ Name: string;
45
+ /** Email address. */
46
+ Address: string;
47
+ }
48
+ /** Summary row from `GET /api/v1/messages`. */
49
+ export interface MailpitMessageSummary {
50
+ /** Mailpit message id. */
51
+ ID: string;
52
+ /** MIME Message-ID. */
53
+ MessageID: string;
54
+ /** Sender. */
55
+ From: MailpitAddress;
56
+ /** Recipients. */
57
+ To: MailpitAddress[];
58
+ /** Subject line. */
59
+ Subject: string;
60
+ /** ISO created timestamp. */
61
+ Created: string;
62
+ /** Attachment count. */
63
+ Attachments: number;
64
+ /** Whether the message has been read in the UI. */
65
+ Read: boolean;
66
+ /** Short plain-text snippet. */
67
+ Snippet: string;
68
+ }
69
+ /** Response from `GET /api/v1/messages`. */
70
+ export interface MailpitMessageList {
71
+ /** Total messages stored. */
72
+ total: number;
73
+ /** Unread message count. */
74
+ unread: number;
75
+ /** Messages returned in this page. */
76
+ count: number;
77
+ /** Message summaries (newest first). */
78
+ messages: MailpitMessageSummary[];
79
+ }
80
+ /** Full message from `GET /api/v1/message/{id}`. */
81
+ export interface MailpitMessage {
82
+ /** Mailpit message id. */
83
+ ID: string;
84
+ /** MIME Message-ID. */
85
+ MessageID: string;
86
+ /** Sender. */
87
+ From: MailpitAddress;
88
+ /** Recipients. */
89
+ To: MailpitAddress[];
90
+ /** CC recipients. */
91
+ Cc?: MailpitAddress[];
92
+ /** BCC recipients. */
93
+ Bcc?: MailpitAddress[];
94
+ /** Subject line. */
95
+ Subject: string;
96
+ /** Plain-text body. */
97
+ Text: string;
98
+ /** HTML body. */
99
+ HTML: string;
100
+ /** ISO created timestamp. */
101
+ Date: string;
102
+ /** Attachment count. */
103
+ Attachments: number;
104
+ }
105
+ /** Options for {@link MailpitTransport.messages}. */
106
+ export interface MailpitMessagesOptions {
107
+ /** Max messages to return. */
108
+ limit?: number;
109
+ /** Pagination offset. */
110
+ start?: number;
111
+ }
112
+ /** Error thrown when the Mailpit REST API returns a non-success response. */
113
+ export declare class MailpitError extends SentlyError {
114
+ readonly statusCode: number;
115
+ readonly apiError: unknown;
116
+ /** Creates a Mailpit API error with status code (`0` = network/connect failure). */
117
+ constructor(message: string, statusCode: number, apiError: unknown);
118
+ }
119
+ /**
120
+ * Development transport for [Mailpit](https://github.com/axllent/mailpit).
121
+ *
122
+ * Sends via SMTP (defaults: `localhost:1025`) and exposes REST helpers for
123
+ * listing, reading, and deleting captured messages.
124
+ */
125
+ export declare class MailpitTransport implements Transport {
126
+ readonly provider = "mailpit";
127
+ private readonly host;
128
+ private readonly port;
129
+ private readonly secure;
130
+ private readonly requireTLS;
131
+ private readonly auth?;
132
+ private readonly tls?;
133
+ private readonly connectionTimeout?;
134
+ private readonly adapter?;
135
+ private readonly apiUrl;
136
+ private readonly apiAuth?;
137
+ private smtp;
138
+ /** Creates a Mailpit transport with local-dev defaults. */
139
+ constructor(config?: MailpitConfig);
140
+ /** Web UI base URL (same origin as the REST API). */
141
+ get webUrl(): string;
142
+ /** Sends an email to Mailpit over SMTP. */
143
+ send(options: MailOptions): Promise<SendResult>;
144
+ /** Verifies SMTP connectivity to Mailpit. */
145
+ verify(): Promise<VerifyResult>;
146
+ /** Closes the underlying SMTP adapter if connected. */
147
+ close(): Promise<void>;
148
+ /**
149
+ * Lists captured messages via `GET /api/v1/messages`.
150
+ * Newest messages appear first.
151
+ */
152
+ messages(options?: MailpitMessagesOptions): Promise<MailpitMessageList>;
153
+ /** Fetches a full message via `GET /api/v1/message/{id}`. */
154
+ getMessage(id: string): Promise<MailpitMessage>;
155
+ /**
156
+ * Deletes messages by id via `DELETE /api/v1/messages`.
157
+ * Pass an empty array (or call {@link deleteAll}) to clear the inbox.
158
+ */
159
+ deleteMessages(ids: string[]): Promise<void>;
160
+ /** Deletes every captured message. */
161
+ deleteAll(): Promise<void>;
162
+ private getSmtp;
163
+ private apiHeaders;
164
+ private apiGet;
165
+ private apiDelete;
166
+ }
@@ -0,0 +1,3 @@
1
+ import{n as X}from"../chunk-a057jzsy.js";import"../chunk-f5x8vesr.js";import"../chunk-yg3kj723.js";import"../chunk-w4c9kj70.js";import"../chunk-1ke3nmyg.js";import{M as N}from"../chunk-d5aw5vag.js";import"../chunk-8kpgbrba.js";import{da as Q}from"../chunk-hnzmn4s4.js";import{ja as V,ka as W}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var Y="localhost",Z=1025,$="http://localhost:8025";class K extends V{statusCode;apiError;constructor(w,z,G){super(w,z===0?"CONNECTION_FAILED":W(z),{...z>0?{statusCode:z}:{},provider:"mailpit",cause:G});this.statusCode=z;this.apiError=G;this.name="MailpitError"}}class k{provider="mailpit";host;port;secure;requireTLS;auth;tls;connectionTimeout;adapter;apiUrl;apiAuth;smtp=null;constructor(w={}){if(this.host=w.host??Y,this.port=w.port??Z,this.secure=w.secure??!1,this.requireTLS=w.requireTLS??!1,w.auth!==void 0)this.auth=w.auth;if(w.tls!==void 0)this.tls=w.tls;if(w.connectionTimeout!==void 0)this.connectionTimeout=w.connectionTimeout;if(w.adapter!==void 0)this.adapter=w.adapter;if(this.apiUrl=(w.apiUrl??$).replace(/\/$/,""),w.apiAuth!==void 0)this.apiAuth=w.apiAuth}get webUrl(){return this.apiUrl}async send(w){return(await this.getSmtp()).send(w)}async verify(){return{...await(await this.getSmtp()).verify(),provider:"mailpit"}}async close(){if(this.smtp)await this.smtp.close()}async messages(w={}){let z=new URLSearchParams;if(w.limit!==void 0)z.set("limit",String(w.limit));if(w.start!==void 0)z.set("start",String(w.start));let G=z.toString(),J=G?`/api/v1/messages?${G}`:"/api/v1/messages";return this.apiGet(J)}async getMessage(w){if(!w)throw new K("Message id is required",400,{code:"INVALID_CONFIG"});return this.apiGet(`/api/v1/message/${encodeURIComponent(w)}`)}async deleteMessages(w){await this.apiDelete("/api/v1/messages",{IDs:w})}async deleteAll(){await this.deleteMessages([])}async getSmtp(){if(!this.smtp){let w=this.adapter??await N({secure:this.secure,...this.connectionTimeout!==void 0?{connectionTimeout:this.connectionTimeout}:{},...this.tls!==void 0?{tls:this.tls}:{}});this.smtp=new X({host:this.host,port:this.port,secure:this.secure,requireTLS:this.requireTLS,adapter:w,...this.auth!==void 0?{auth:this.auth}:{},...this.tls!==void 0?{tls:this.tls}:{},...this.connectionTimeout!==void 0?{connectionTimeout:this.connectionTimeout}:{}})}return this.smtp}apiHeaders(){let w={Accept:"application/json","Content-Type":"application/json"};if(this.apiAuth){let z=Q(`${this.apiAuth.user}:${this.apiAuth.pass}`).replace(/\r\n/g,"");w.Authorization=`Basic ${z}`}return w}async apiGet(w){let z;try{z=await fetch(`${this.apiUrl}${w}`,{method:"GET",headers:this.apiHeaders()})}catch(G){throw new K(G instanceof Error?G.message:"Mailpit API request failed",0,G)}if(!z.ok){let G=await z.text().catch(()=>"");throw new K(G||`Mailpit API error (${z.status})`,z.status,G)}return await z.json()}async apiDelete(w,z){let G;try{G=await fetch(`${this.apiUrl}${w}`,{method:"DELETE",headers:this.apiHeaders(),body:JSON.stringify(z)})}catch(J){throw new K(J instanceof Error?J.message:"Mailpit API request failed",0,J)}if(!G.ok){let J=await G.text().catch(()=>"");throw new K(J||`Mailpit API error (${G.status})`,G.status,J)}}}export{k as MailpitTransport,K as MailpitError,Z as MAILPIT_DEFAULT_PORT,Y as MAILPIT_DEFAULT_HOST,$ as MAILPIT_DEFAULT_API_URL};
2
+
3
+ //# debugId=A41842358B8E380464756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/mailpit.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * Mailpit development transport — SMTP to a local Mailpit catcher with\n * REST helpers for inspecting and clearing captured messages.\n *\n * Defaults match a stock Mailpit install: SMTP `localhost:1025`,\n * UI/API `http://localhost:8025`.\n *\n * @example\n * ```ts\n * import { createMailer } from \"sently/mailer\";\n * import { MailpitTransport } from \"sently/transports/mailpit\";\n *\n * const mailpit = new MailpitTransport();\n * const mailer = await createMailer({ transport: mailpit });\n *\n * await mailer.send({\n * from: \"dev@example.com\",\n * to: \"you@example.com\",\n * subject: \"Hello\",\n * text: \"Captured by Mailpit\",\n * });\n *\n * const inbox = await mailpit.messages();\n * console.log(inbox.messages[0]?.Subject);\n * ```\n */\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPAuth,\n SocketAdapter,\n TLSOptions,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { createDefaultAdapter } from \"../detect.js\";\nimport { SMTPTransport } from \"./smtp.js\";\n\n/** Default SMTP host for a local Mailpit instance. */\nexport const MAILPIT_DEFAULT_HOST = \"localhost\";\n\n/** Default SMTP port for a local Mailpit instance. */\nexport const MAILPIT_DEFAULT_PORT = 1025;\n\n/** Default web UI / REST API base URL for a local Mailpit instance. */\nexport const MAILPIT_DEFAULT_API_URL = \"http://localhost:8025\";\n\n/** Mailpit development transport configuration. */\nexport interface MailpitConfig {\n /** SMTP hostname. Default: `\"localhost\"`. */\n host?: string;\n /** SMTP port. Default: `1025`. */\n port?: number;\n /** Use implicit TLS on connect. Default: `false` (Mailpit is plain SMTP). */\n secure?: boolean;\n /**\n * Refuse AUTH over a non-TLS connection.\n * Default: `false` so optional local SMTP auth works without TLS.\n */\n requireTLS?: boolean;\n /** Optional SMTP authentication (Mailpit can accept any credentials). */\n auth?: SMTPAuth;\n /** TLS options when STARTTLS or implicit TLS is enabled. */\n tls?: TLSOptions;\n /** Socket connect timeout in milliseconds. */\n connectionTimeout?: number;\n /** Runtime socket adapter. Auto-detected on first send when omitted. */\n adapter?: SocketAdapter;\n /**\n * Mailpit web UI / REST API base URL (no trailing slash).\n * Default: `\"http://localhost:8025\"`.\n */\n apiUrl?: string;\n /** Basic auth for the Mailpit UI/API when the instance requires it. */\n apiAuth?: { user: string; pass: string };\n}\n\n/** Address object returned by the Mailpit REST API. */\nexport interface MailpitAddress {\n /** Display name, may be empty. */\n Name: string;\n /** Email address. */\n Address: string;\n}\n\n/** Summary row from `GET /api/v1/messages`. */\nexport interface MailpitMessageSummary {\n /** Mailpit message id. */\n ID: string;\n /** MIME Message-ID. */\n MessageID: string;\n /** Sender. */\n From: MailpitAddress;\n /** Recipients. */\n To: MailpitAddress[];\n /** Subject line. */\n Subject: string;\n /** ISO created timestamp. */\n Created: string;\n /** Attachment count. */\n Attachments: number;\n /** Whether the message has been read in the UI. */\n Read: boolean;\n /** Short plain-text snippet. */\n Snippet: string;\n}\n\n/** Response from `GET /api/v1/messages`. */\nexport interface MailpitMessageList {\n /** Total messages stored. */\n total: number;\n /** Unread message count. */\n unread: number;\n /** Messages returned in this page. */\n count: number;\n /** Message summaries (newest first). */\n messages: MailpitMessageSummary[];\n}\n\n/** Full message from `GET /api/v1/message/{id}`. */\nexport interface MailpitMessage {\n /** Mailpit message id. */\n ID: string;\n /** MIME Message-ID. */\n MessageID: string;\n /** Sender. */\n From: MailpitAddress;\n /** Recipients. */\n To: MailpitAddress[];\n /** CC recipients. */\n Cc?: MailpitAddress[];\n /** BCC recipients. */\n Bcc?: MailpitAddress[];\n /** Subject line. */\n Subject: string;\n /** Plain-text body. */\n Text: string;\n /** HTML body. */\n HTML: string;\n /** ISO created timestamp. */\n Date: string;\n /** Attachment count. */\n Attachments: number;\n}\n\n/** Options for {@link MailpitTransport.messages}. */\nexport interface MailpitMessagesOptions {\n /** Max messages to return. */\n limit?: number;\n /** Pagination offset. */\n start?: number;\n}\n\n/** Error thrown when the Mailpit REST API returns a non-success response. */\nexport class MailpitError extends SentlyError {\n /** Creates a Mailpit API error with status code (`0` = network/connect failure). */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, statusCode === 0 ? \"CONNECTION_FAILED\" : httpStatusToSentlyCode(statusCode), {\n ...(statusCode > 0 ? { statusCode } : {}),\n provider: \"mailpit\",\n cause: apiError,\n });\n this.name = \"MailpitError\";\n }\n}\n\n/**\n * Development transport for [Mailpit](https://github.com/axllent/mailpit).\n *\n * Sends via SMTP (defaults: `localhost:1025`) and exposes REST helpers for\n * listing, reading, and deleting captured messages.\n */\nexport class MailpitTransport implements Transport {\n readonly provider = \"mailpit\";\n\n private readonly host: string;\n private readonly port: number;\n private readonly secure: boolean;\n private readonly requireTLS: boolean;\n private readonly auth?: SMTPAuth;\n private readonly tls?: TLSOptions;\n private readonly connectionTimeout?: number;\n private readonly adapter?: SocketAdapter;\n private readonly apiUrl: string;\n private readonly apiAuth?: { user: string; pass: string };\n\n private smtp: SMTPTransport | null = null;\n\n /** Creates a Mailpit transport with local-dev defaults. */\n constructor(config: MailpitConfig = {}) {\n this.host = config.host ?? MAILPIT_DEFAULT_HOST;\n this.port = config.port ?? MAILPIT_DEFAULT_PORT;\n this.secure = config.secure ?? false;\n this.requireTLS = config.requireTLS ?? false;\n if (config.auth !== undefined) {\n this.auth = config.auth;\n }\n if (config.tls !== undefined) {\n this.tls = config.tls;\n }\n if (config.connectionTimeout !== undefined) {\n this.connectionTimeout = config.connectionTimeout;\n }\n if (config.adapter !== undefined) {\n this.adapter = config.adapter;\n }\n this.apiUrl = (config.apiUrl ?? MAILPIT_DEFAULT_API_URL).replace(/\\/$/, \"\");\n if (config.apiAuth !== undefined) {\n this.apiAuth = config.apiAuth;\n }\n }\n\n /** Web UI base URL (same origin as the REST API). */\n get webUrl(): string {\n return this.apiUrl;\n }\n\n /** Sends an email to Mailpit over SMTP. */\n async send(options: MailOptions): Promise<SendResult> {\n return (await this.getSmtp()).send(options);\n }\n\n /** Verifies SMTP connectivity to Mailpit. */\n async verify(): Promise<VerifyResult> {\n const result = await (await this.getSmtp()).verify();\n return {\n ...result,\n provider: \"mailpit\",\n };\n }\n\n /** Closes the underlying SMTP adapter if connected. */\n async close(): Promise<void> {\n if (this.smtp) {\n await this.smtp.close();\n }\n }\n\n /**\n * Lists captured messages via `GET /api/v1/messages`.\n * Newest messages appear first.\n */\n async messages(options: MailpitMessagesOptions = {}): Promise<MailpitMessageList> {\n const params = new URLSearchParams();\n if (options.limit !== undefined) {\n params.set(\"limit\", String(options.limit));\n }\n if (options.start !== undefined) {\n params.set(\"start\", String(options.start));\n }\n const query = params.toString();\n const path = query ? `/api/v1/messages?${query}` : \"/api/v1/messages\";\n return this.apiGet<MailpitMessageList>(path);\n }\n\n /** Fetches a full message via `GET /api/v1/message/{id}`. */\n async getMessage(id: string): Promise<MailpitMessage> {\n if (!id) {\n throw new MailpitError(\"Message id is required\", 400, { code: \"INVALID_CONFIG\" });\n }\n return this.apiGet<MailpitMessage>(`/api/v1/message/${encodeURIComponent(id)}`);\n }\n\n /**\n * Deletes messages by id via `DELETE /api/v1/messages`.\n * Pass an empty array (or call {@link deleteAll}) to clear the inbox.\n */\n async deleteMessages(ids: string[]): Promise<void> {\n await this.apiDelete(\"/api/v1/messages\", { IDs: ids });\n }\n\n /** Deletes every captured message. */\n async deleteAll(): Promise<void> {\n await this.deleteMessages([]);\n }\n\n private async getSmtp(): Promise<SMTPTransport> {\n if (!this.smtp) {\n const adapter =\n this.adapter ??\n (await createDefaultAdapter({\n secure: this.secure,\n ...(this.connectionTimeout !== undefined\n ? { connectionTimeout: this.connectionTimeout }\n : {}),\n ...(this.tls !== undefined ? { tls: this.tls } : {}),\n }));\n\n this.smtp = new SMTPTransport({\n host: this.host,\n port: this.port,\n secure: this.secure,\n requireTLS: this.requireTLS,\n adapter,\n ...(this.auth !== undefined ? { auth: this.auth } : {}),\n ...(this.tls !== undefined ? { tls: this.tls } : {}),\n ...(this.connectionTimeout !== undefined\n ? { connectionTimeout: this.connectionTimeout }\n : {}),\n });\n }\n return this.smtp;\n }\n\n private apiHeaders(): HeadersInit {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n };\n if (this.apiAuth) {\n const token = encodeBase64(`${this.apiAuth.user}:${this.apiAuth.pass}`).replace(/\\r\\n/g, \"\");\n headers.Authorization = `Basic ${token}`;\n }\n return headers;\n }\n\n private async apiGet<T>(path: string): Promise<T> {\n let response: Response;\n try {\n response = await fetch(`${this.apiUrl}${path}`, {\n method: \"GET\",\n headers: this.apiHeaders(),\n });\n } catch (err) {\n throw new MailpitError(\n err instanceof Error ? err.message : \"Mailpit API request failed\",\n 0,\n err,\n );\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => \"\");\n throw new MailpitError(\n body || `Mailpit API error (${response.status})`,\n response.status,\n body,\n );\n }\n\n return (await response.json()) as T;\n }\n\n private async apiDelete(path: string, body: unknown): Promise<void> {\n let response: Response;\n try {\n response = await fetch(`${this.apiUrl}${path}`, {\n method: \"DELETE\",\n headers: this.apiHeaders(),\n body: JSON.stringify(body),\n });\n } catch (err) {\n throw new MailpitError(\n err instanceof Error ? err.message : \"Mailpit API request failed\",\n 0,\n err,\n );\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new MailpitError(\n text || `Mailpit API error (${response.status})`,\n response.status,\n text,\n );\n }\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "A41842358B8E380464756E2164756E21",
9
+ "names": []
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sently",
3
- "version": "0.10.0",
4
- "description": "Runtime-agnostic messaging library for Node.js, Bun, Deno, and Cloudflare Workers. Channel-first email, SMS, WhatsApp, and push with pluggable provider transports.",
3
+ "version": "1.0.0",
4
+ "description": "Runtime-agnostic channel-delivery library for Node.js, Bun, Deno, and Cloudflare Workers. One sender shape, one error model, and one retry path across email, SMS, WhatsApp, and push with pluggable provider transports.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.js",
@@ -11,6 +11,7 @@
11
11
  "README.md",
12
12
  "LICENSE",
13
13
  "CHANGELOG.md",
14
+ "SECURITY.md",
14
15
  "AGENTS.md",
15
16
  "site/content/docs"
16
17
  ],
@@ -58,11 +59,18 @@
58
59
  "typescript": "6.0.3"
59
60
  },
60
61
  "keywords": [
62
+ "messaging",
61
63
  "email",
64
+ "sms",
65
+ "whatsapp",
66
+ "push",
67
+ "web-push",
68
+ "fcm",
62
69
  "smtp",
63
70
  "mailer",
64
71
  "nodemailer",
65
72
  "nodemailer-alternative",
73
+ "channel-first",
66
74
  "bun",
67
75
  "deno",
68
76
  "cloudflare-workers",
@@ -75,6 +83,7 @@
75
83
  "ses",
76
84
  "aws-ses",
77
85
  "brevo",
86
+ "twilio",
78
87
  "dkim",
79
88
  "oauth2",
80
89
  "transactional-email",
@@ -179,6 +188,10 @@
179
188
  "import": "./dist/transports/mailtrap.js",
180
189
  "types": "./dist/transports/mailtrap.d.ts"
181
190
  },
191
+ "./transports/mailpit": {
192
+ "import": "./dist/transports/mailpit.js",
193
+ "types": "./dist/transports/mailpit.d.ts"
194
+ },
182
195
  "./transports/loops": {
183
196
  "import": "./dist/transports/loops.js",
184
197
  "types": "./dist/transports/loops.d.ts"
@@ -6,7 +6,52 @@ source: "site/app/llms.txt/route.ts"
6
6
  ---
7
7
 
8
8
  The docs site serves `/llms.txt` and `/llms-full.txt` for concise and complete machine-readable documentation.
9
+ Use the live site indexes when an agent needs current handbook navigation.
9
10
 
10
- Use `/llms.txt` for navigation and `/llms-full.txt` when an agent needs page content.
11
+ <Callout title="The one rule">
12
+ Prefer the docs site `/llms.txt` over the repo-root `llms.txt` file when both exist — the site index is generated from the handbook MDX.
13
+ </Callout>
11
14
 
12
- <Cards><Card title="MCP" href="/docs/ai/mcp" /></Cards>
15
+ ## Quick start
16
+
17
+ <Steps>
18
+ <Step title="Fetch the short index">
19
+
20
+ ```text
21
+ https://sently.omqkhafi.dev/llms.txt
22
+ ```
23
+
24
+ </Step>
25
+ <Step title="Fetch full page text when needed">
26
+
27
+ ```text
28
+ https://sently.omqkhafi.dev/llms-full.txt
29
+ ```
30
+
31
+ </Step>
32
+ </Steps>
33
+
34
+ ## Related agent surfaces
35
+
36
+ | Resource | Use |
37
+ | --- | --- |
38
+ | Site `/llms.txt` | Navigation + short model |
39
+ | Site `/llms-full.txt` | Full handbook dump |
40
+ | Repo `AGENTS.md` | Channel-first contract |
41
+ | [Stability](/docs/get-started/stability) | What is frozen at 1.x |
42
+ | [Compare](/docs/guides/compare) | Library vs platform positioning |
43
+
44
+ ## Troubleshooting
45
+
46
+ <Accordions>
47
+ <Accordion title="Why does the repo-root llms.txt disagree with the site?">
48
+ The live route is built from `site/content/docs`. Refresh or regenerate from the handbook after docs changes.
49
+ </Accordion>
50
+ </Accordions>
51
+
52
+ ## Next
53
+
54
+ <Cards>
55
+ <Card title="MCP" href="/docs/ai/mcp" />
56
+ <Card title="Introduction" href="/docs/get-started/introduction" />
57
+ </Cards>
@@ -91,10 +91,13 @@ const mailer = await createSMTPMailer({
91
91
  - [Email options](../reference/mail-options)
92
92
  - [Transport contracts](../reference/transport-contracts)
93
93
  - [Hooks](./hooks)
94
+ - [Mailpit](../transports/mailpit) — local catcher for development
95
+ - [Preview](../decorators/preview) — write `.eml` files to disk
94
96
 
95
97
  ## Next
96
98
 
97
99
  <Cards>
98
100
  <Card title="Transports" href="/docs/transports" />
101
+ <Card title="Mailpit" href="/docs/transports/mailpit" />
99
102
  <Card title="Send in bulk" href="/docs/guides/send-bulk" />
100
103
  </Cards>
@@ -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
 
@@ -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
@@ -78,6 +78,9 @@ Shared fields: `title`, `body`, optional `data`, `icon`, `ttl`, `messageId`.
78
78
  <Accordion title="Why does the transport reject my endpoint?">
79
79
  Web Push validates subscription endpoint hosts before sending. Add only exact private relay hostnames with `allowedEndpointHosts` when needed.
80
80
  </Accordion>
81
+ <Accordion title="Why does FCM reject my subscription object?">
82
+ `FcmTransport` requires `token`. Pass a Web Push `subscription` only to `WebPushTransport`.
83
+ </Accordion>
81
84
  <Accordion title="Why is the endpoint redacted in hooks?">
82
85
  Endpoint paths and FCM tokens are long-lived credentials, so hook context keeps only a redacted fingerprint.
83
86
  </Accordion>
@@ -88,6 +91,7 @@ Shared fields: `title`, `body`, optional `data`, `icon`, `ttl`, `messageId`.
88
91
  - [Push options](../reference/push-options)
89
92
  - [Web Push](../transports/webpush)
90
93
  - [FCM](../transports/fcm)
94
+ - [Failover](../guides/failover)
91
95
  - [Hooks](./hooks)
92
96
 
93
97
  ## Next
@@ -53,9 +53,14 @@ Successful sends include `provider` and `providerIndex` for the transport that h
53
53
  </Accordion>
54
54
  </Accordions>
55
55
 
56
+ ## Learn more
57
+
58
+ - [Failover guide](/docs/guides/failover) — retry-then-fallback recipes across channels
59
+
56
60
  ## Next
57
61
 
58
62
  <Cards>
63
+ <Card title="Failover guide" href="/docs/guides/failover" />
59
64
  <Card title="Weighted fallback" href="/docs/decorators/weighted-fallback" />
60
65
  <Card title="Retry" href="/docs/decorators/retry" />
61
66
  </Cards>