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
package/AGENTS.md CHANGED
@@ -24,10 +24,11 @@ Providers are transports under those senders. Vendor extras (OTP, account utilit
24
24
 
25
25
  ```sh
26
26
  bun install
27
- bun test
27
+ bun test # unit/integration only — never runs *.live.test.ts
28
+ bun run test:live # local-only provider live suites (requires *_LIVE=1)
28
29
  bun run site:dev
29
30
  bun run site:build
30
- bun run verify
31
+ bun run verify # lint, typecheck, build, site:build, then bun test
31
32
  ```
32
33
 
33
34
  Do not invent APIs. Prefer imports from published subpaths (`sently/mailer`, `sently/sms`, `sently/transports/*`).
package/CHANGELOG.md CHANGED
@@ -2,6 +2,103 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.1] — 2026-08-02
6
+
7
+ ### 🐛 Fixed
8
+
9
+ - **Publish CI Test job** — build `dist` before `bun test` so parallel Verify no
10
+ longer fails api-surface suites that import published bundles
11
+ - **`bun test` skips live suites** — clears `TAQNYAT_LIVE` / `SNDR_LIVE` /
12
+ `INBUCKET_LIVE` and ignores `**/*.live.test.ts`; `bun run test:live` is
13
+ local-only (refuses `CI` / `GITHUB_ACTIONS`)
14
+
15
+ ## [1.1.0] — 2026-08-02
16
+
17
+ ### ✨ Added
18
+
19
+ - **Web Push live verified** — homepage transport strip marks Web Push verified
20
+ after okengine delivered a real browser notification with VAPID; docs page
21
+ shows the green `LiveVerified` callout
22
+ - **Web Push brand mark** — React `WebPushLogo` / `WebPushLogoIcon` (blue
23
+ notification tile + bell + badge); docs title badge + sidebar/marquee use
24
+ the mark
25
+ - **Web Push protocol knobs** — `urgency` / `topic` headers (RFC 8030), rich
26
+ Notification fields (`badge`, `image`, `tag`, `actions`, `requireInteraction`,
27
+ `renotify`), `silent` / data-only payloads, and `generateVapidKeys()` on
28
+ `sently/transports/webpush`
29
+ - **Inbucket transport** — `sently/transports/inbucket` for local SMTP capture
30
+ (defaults `localhost:2500` / UI `http://localhost:9000`) with REST helpers
31
+ `listMailbox`, `getMessage`, `getSource`, `markSeen`, `deleteMessage`,
32
+ `purgeMailbox`, and `mailboxForAddress`; opt-in live suite via
33
+ `INBUCKET_LIVE=1`; docs under a new **Email Dev** sidebar group with Mailpit
34
+ - **Mailpit vendor extras** — `search`, `getHeaders`, `htmlCheck`, `linkCheck`,
35
+ and `setRead` on `MailpitTransport` for local inbox assertions; docs cover
36
+ the helpers and link to the Mailpit API
37
+ - **Mailpit brand mark** — React `MailpitLogo` / `MailpitLogoIcon` from the
38
+ official SVG; docs title badge + sidebar/marquee use the mark; Mailpit is
39
+ live-verified in the transport strip
40
+ - **Taqnyat live suite** — opt-in `tests/integration/taqnyat.live.test.ts`
41
+ (`TAQNYAT_LIVE=1`) with free preflight before SMS / Mail / WhatsApp sends;
42
+ live env keys documented in `.env.example`
43
+ - **Taqnyat transport docs** — single `/docs/transports/taqnyat` page under a
44
+ new **Multi-channel** sidebar group (SMS / WhatsApp / Email sections);
45
+ green `LiveVerified` on SMS and WhatsApp only
46
+ - **Taqnyat SMS vendor extras** — `getBalance`, `listSenders`, `schedule`,
47
+ `deleteScheduled` on `TaqnyatSmsTransport` (OTP helpers unchanged)
48
+ - **Taqnyat WhatsApp vendor extras** — `listTemplates`, `createTemplate`,
49
+ `deleteTemplate`, `optIn` / `optOut`, `sendWithFailover` on
50
+ `TaqnyatWhatsAppTransport`
51
+ - **Taqnyat brand marks** — React `TaqnyatLogo` / `TaqnyatLogoIcon` replace the
52
+ `TQ` monogram; marquee uses the full wordmark alone, sidebar uses a square
53
+ single-dot icon (docs page keeps the brand wordmark)
54
+ - **Transport strip tiers** — homepage marquee legend:
55
+ color = live verified, muted = available, sponsors reserved;
56
+ verified chips use the same icon+label rhythm as the rest (brand-colored
57
+ mark); full wordmarks stay on docs pages; priority marks sit after a ~25%
58
+ available lead-in
59
+ - **SNDR brand mark** — React `SndrLogo` / `SndrLogoIcon` from
60
+ [sndr.sh](https://www.sndr.sh/sndr-logo.svg); docs page rewritten with
61
+ LiveVerified, contact resources, and platform coverage table
62
+ - **SNDR webhooks** — map `email.queued`, `failed`, `complained`, `opened`,
63
+ and `clicked` into normalized `EmailEvent` types
64
+
65
+ ### ♻️ Changed
66
+
67
+ - **Homepage messaging** — clearer TypeScript notification/messaging
68
+ positioning (Email · SMS · WhatsApp · Push), one-package signal, provider
69
+ swap strip (`SMTP → SES → Resend`), tighter why cards; no section removals
70
+ - **`/llms.txt`** — agent preamble states notification/messaging library
71
+ (not a queue); Fumadocs handbook index unchanged underneath
72
+ - **Publish CI** — `.github/workflows/publish.yml` runs only on `v*` tag
73
+ pushes; Verify (typecheck, lint, build, test) and npm / JSR publish jobs
74
+ run in parallel (no longer sequential npm-then-JSR on push to `main`)
75
+ - **README** — shieldcn grid/left header with cleaned logo SVGs (no
76
+ clip-path square), drop Bundlephobia badge, add npm downloads chart;
77
+ version badges match the okengine pattern (`/npm/…`, `/jsr/…`)
78
+
79
+ ### 🔥 Removed
80
+
81
+ - **`test.yml` workflow** — PR / push CI matrix removed; release Verify lives
82
+ in `publish.yml` on version tags
83
+
84
+ ### 🐛 Fixed
85
+
86
+ - **Taqnyat WhatsApp queued accept** — treat `statuses: "PENDING"` (and
87
+ template/text accepts without `message_id` yet) as success instead of an
88
+ empty id edge case
89
+ - **`bun run verify` never hits provider live suites** — clears `TAQNYAT_LIVE` /
90
+ `SNDR_LIVE` / `INBUCKET_LIVE` and ignores `**/*.live.test.ts` so a leftover
91
+ shell export cannot send real SMS / email / WhatsApp during verify; use
92
+ `bun run test:live` to opt in
93
+
94
+ ## [1.0.1] — 2026-08-01
95
+
96
+ ### 🐛 Fixed
97
+
98
+ - **Web Push `subject` validation** — `WebPushTransport` rejects invalid
99
+ VAPID `subject` values at construction (`mailto:` / `https:` only) instead
100
+ of failing later with an opaque push-service 403
101
+
5
102
  ## [1.0.0] — 2026-08-01
6
103
 
7
104
  First stable release. Channel contracts and Supported transports are semver-frozen;
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
  <picture>
3
3
  <source
4
4
  media="(prefers-color-scheme: dark)"
5
- srcset="https://shieldcn.dev/header/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-w.svg&theme=zinc&size=banner&mode=dark&font=geist"
5
+ srcset="https://shieldcn.dev/header/grid.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/dev/site/public/sentlyIconLogo-w.svg&theme=zinc&size=banner&mode=dark&font=geist&align=left"
6
6
  />
7
7
  <img
8
8
  alt="sently — One API. Four channels. Every runtime."
9
- src="https://shieldcn.dev/header/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-k.svg&theme=zinc&size=banner&mode=light&font=geist"
9
+ src="https://shieldcn.dev/header/grid.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/dev/site/public/sentlyIconLogo-k.svg&theme=zinc&size=banner&mode=light&font=geist&align=left"
10
10
  width="750"
11
11
  />
12
12
  </picture>
@@ -15,13 +15,28 @@
15
15
  <p align="center">
16
16
  <a href="https://www.npmjs.com/package/sently"><img alt="npm" src="https://shieldcn.dev/npm/sently.svg?size=sm&variant=outline" /></a>
17
17
  <a href="https://jsr.io/@alialnaghmoush/sently"><img alt="JSR" src="https://shieldcn.dev/jsr/@alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
18
- <a href="https://bundlephobia.com/package/sently"><img alt="bundle" src="https://shieldcn.dev/bundlephobia/minzip/sently.svg?size=sm&variant=outline" /></a>
19
18
  <a href="https://opensource.org/licenses/MIT"><img alt="MIT" src="https://shieldcn.dev/npm/license/sently.svg?size=sm&variant=outline" /></a>
20
- <a href="https://bun.sh"><img alt="Bun" src="https://shieldcn.dev/badge/Bun-ready-000000.svg?logo=bun&size=sm&variant=outline" /></a>
19
+ <a href="https://bun.sh"><img alt="Bun" src="https://shieldcn.dev/badge/Bun-ready.svg?logo=bun&size=sm&variant=outline" /></a>
21
20
  <a href="https://github.com/alialnaghmoush/sently/stargazers"><img alt="stars" src="https://shieldcn.dev/github/stars/alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
22
21
  <a href="https://github.com/alialnaghmoush/sently/actions"><img alt="CI" src="https://shieldcn.dev/github/ci/alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
23
22
  </p>
24
23
 
24
+ <p align="center">
25
+ <a href="https://www.npmjs.com/package/sently">
26
+ <picture>
27
+ <source
28
+ media="(prefers-color-scheme: dark)"
29
+ srcset="https://shieldcn.dev/chart/npm/sently.svg?theme=zinc&mode=dark&width=750&height=280"
30
+ />
31
+ <img
32
+ alt="npm downloads"
33
+ src="https://shieldcn.dev/chart/npm/sently.svg?theme=zinc&mode=light&width=750&height=280"
34
+ width="750"
35
+ />
36
+ </picture>
37
+ </a>
38
+ </p>
39
+
25
40
  <p align="center">
26
41
  <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
42
  </p>
@@ -122,4 +137,4 @@ More detail: [Compare](https://sently.omqkhafi.dev/docs/guides/compare) · [Stab
122
137
 
123
138
  Local docs: `bun run site:dev`. Verify: `bun run verify`.
124
139
 
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.
140
+ Semver-stable channel contracts. Published on [npm](https://www.npmjs.com/package/sently) (provenance + Trusted Publishing) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
@@ -2,9 +2,9 @@
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 (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 /**\n * Notification title.\n * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.\n */\n title?: string;\n /**\n * Notification body text.\n * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.\n */\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/** RFC 8030 `Urgency` values for Web Push delivery priority. */\nexport type WebPushUrgency = \"very-low\" | \"low\" | \"normal\" | \"high\";\n\n/** Browser Notification API action button. */\nexport interface WebPushAction {\n /** Action identifier delivered to the service worker. */\n action: string;\n /** Button label. */\n title: string;\n /** Optional action icon URL. */\n icon?: 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 * RFC 8030 `Urgency` header — delivery priority hint for the push service.\n * Defaults to omitting the header (service default, typically `normal`).\n */\n urgency?: WebPushUrgency;\n /**\n * RFC 8030 `Topic` header — collapse key so a newer message replaces a pending\n * one with the same topic (ASCII, max 32 characters).\n */\n topic?: string;\n /** Notification badge URL (small monochrome icon). */\n badge?: string;\n /** Large image URL shown with the notification. */\n image?: string;\n /** Tag for replacing an existing notification with the same tag. */\n tag?: string;\n /** Action buttons shown on the notification. */\n actions?: WebPushAction[];\n /** Keep the notification open until the user interacts. */\n requireInteraction?: boolean;\n /** Re-alert when replacing a notification with the same `tag`. */\n renotify?: boolean;\n /**\n * Encrypt only `data` (no visible notification fields). Requires `data`.\n * Use for background sync without showing a system notification — the service\n * worker must handle `push` without calling `showNotification`.\n */\n silent?: boolean;\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 /** Notification title (required for FCM). */\n title: string;\n /** Notification body text (required for FCM). */\n body: 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
- "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",
7
+ "mappings": "AA+GO,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",
9
9
  "names": []
10
10
  }
@@ -21,10 +21,16 @@ export interface PushSubscription {
21
21
  }
22
22
  /** Shared notification fields for Web Push and FCM. */
23
23
  interface PushNotificationFields {
24
- /** Notification title. */
25
- title: string;
26
- /** Notification body text. */
27
- body: string;
24
+ /**
25
+ * Notification title.
26
+ * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.
27
+ */
28
+ title?: string;
29
+ /**
30
+ * Notification body text.
31
+ * Required for FCM and for visible Web Push; omit with `data` / `silent` for Web Push data-only.
32
+ */
33
+ body?: string;
28
34
  /** Arbitrary application data attached to the notification. */
29
35
  data?: Record<string, unknown>;
30
36
  /** Notification icon URL (Web Push / some FCM platforms). */
@@ -34,10 +40,49 @@ interface PushNotificationFields {
34
40
  /** Client-supplied message identifier when known. */
35
41
  messageId?: string;
36
42
  }
43
+ /** RFC 8030 `Urgency` values for Web Push delivery priority. */
44
+ export type WebPushUrgency = "very-low" | "low" | "normal" | "high";
45
+ /** Browser Notification API action button. */
46
+ export interface WebPushAction {
47
+ /** Action identifier delivered to the service worker. */
48
+ action: string;
49
+ /** Button label. */
50
+ title: string;
51
+ /** Optional action icon URL. */
52
+ icon?: string;
53
+ }
37
54
  /** Options for sending a Web Push notification (VAPID / browser subscription). */
38
55
  export interface WebPushOptions extends PushNotificationFields {
39
56
  /** Target browser push subscription. */
40
57
  subscription: PushSubscription;
58
+ /**
59
+ * RFC 8030 `Urgency` header — delivery priority hint for the push service.
60
+ * Defaults to omitting the header (service default, typically `normal`).
61
+ */
62
+ urgency?: WebPushUrgency;
63
+ /**
64
+ * RFC 8030 `Topic` header — collapse key so a newer message replaces a pending
65
+ * one with the same topic (ASCII, max 32 characters).
66
+ */
67
+ topic?: string;
68
+ /** Notification badge URL (small monochrome icon). */
69
+ badge?: string;
70
+ /** Large image URL shown with the notification. */
71
+ image?: string;
72
+ /** Tag for replacing an existing notification with the same tag. */
73
+ tag?: string;
74
+ /** Action buttons shown on the notification. */
75
+ actions?: WebPushAction[];
76
+ /** Keep the notification open until the user interacts. */
77
+ requireInteraction?: boolean;
78
+ /** Re-alert when replacing a notification with the same `tag`. */
79
+ renotify?: boolean;
80
+ /**
81
+ * Encrypt only `data` (no visible notification fields). Requires `data`.
82
+ * Use for background sync without showing a system notification — the service
83
+ * worker must handle `push` without calling `showNotification`.
84
+ */
85
+ silent?: boolean;
41
86
  }
42
87
  /**
43
88
  * Options for sending via Firebase Cloud Messaging (device token).
@@ -46,6 +91,10 @@ export interface WebPushOptions extends PushNotificationFields {
46
91
  export interface FcmPushOptions extends PushNotificationFields {
47
92
  /** FCM registration token for the target device. */
48
93
  token: string;
94
+ /** Notification title (required for FCM). */
95
+ title: string;
96
+ /** Notification body text (required for FCM). */
97
+ body: string;
49
98
  /** Optional image URL for the notification. */
50
99
  image?: string;
51
100
  }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @module
3
+ * Inbucket development transport — SMTP to a local Inbucket catcher with
4
+ * REST helpers for inspecting, reading, and purging mailbox messages.
5
+ *
6
+ * Defaults match a stock Inbucket install: SMTP `localhost:2500`,
7
+ * UI/API `http://localhost:9000`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { createMailer } from "sently/mailer";
12
+ * import { InbucketTransport } from "sently/transports/inbucket";
13
+ *
14
+ * const inbucket = new InbucketTransport();
15
+ * const mailer = await createMailer({ transport: inbucket });
16
+ *
17
+ * await mailer.send({
18
+ * from: "dev@example.com",
19
+ * to: "you@example.com",
20
+ * subject: "Hello",
21
+ * text: "Captured by Inbucket",
22
+ * });
23
+ *
24
+ * const mailbox = inbucket.mailboxForAddress("you@example.com");
25
+ * const inbox = await inbucket.listMailbox(mailbox);
26
+ * console.log(inbox[0]?.subject);
27
+ * ```
28
+ */
29
+ import { SentlyError } from "../core/errors.js";
30
+ import type { MailOptions, SendResult, SMTPAuth, SocketAdapter, TLSOptions, Transport, VerifyResult } from "../core/types.js";
31
+ /** Default SMTP host for a local Inbucket instance. */
32
+ export declare const INBUCKET_DEFAULT_HOST = "localhost";
33
+ /** Default SMTP port for a local Inbucket instance. */
34
+ export declare const INBUCKET_DEFAULT_PORT = 2500;
35
+ /** Default web UI / REST API base URL for a local Inbucket instance. */
36
+ export declare const INBUCKET_DEFAULT_API_URL = "http://localhost:9000";
37
+ /**
38
+ * How Inbucket maps an email address to a mailbox name.
39
+ * Matches `INBUCKET_MAILBOXNAMING` on the catcher (`local` by default).
40
+ */
41
+ export type InbucketMailboxNaming = "local" | "full" | "domain";
42
+ /** Inbucket development transport configuration. */
43
+ export interface InbucketConfig {
44
+ /** SMTP hostname. Default: `"localhost"`. */
45
+ host?: string;
46
+ /** SMTP port. Default: `2500`. */
47
+ port?: number;
48
+ /** Use implicit TLS on connect. Default: `false` (Inbucket is plain SMTP). */
49
+ secure?: boolean;
50
+ /**
51
+ * Refuse AUTH over a non-TLS connection.
52
+ * Default: `false` so optional local SMTP auth works without TLS.
53
+ */
54
+ requireTLS?: boolean;
55
+ /** Optional SMTP authentication. */
56
+ auth?: SMTPAuth;
57
+ /** TLS options when STARTTLS or implicit TLS is enabled. */
58
+ tls?: TLSOptions;
59
+ /** Socket connect timeout in milliseconds. */
60
+ connectionTimeout?: number;
61
+ /** Runtime socket adapter. Auto-detected on first send when omitted. */
62
+ adapter?: SocketAdapter;
63
+ /**
64
+ * Inbucket web UI / REST API base URL (no trailing slash).
65
+ * Default: `"http://localhost:9000"`.
66
+ */
67
+ apiUrl?: string;
68
+ /**
69
+ * Mailbox naming strategy for {@link InbucketTransport.mailboxForAddress}.
70
+ * Default: `"local"` (local-part only), matching stock Inbucket.
71
+ */
72
+ mailboxNaming?: InbucketMailboxNaming;
73
+ }
74
+ /** Summary row from `GET /api/v1/mailbox/{name}`. */
75
+ export interface InbucketMessageHeader {
76
+ /** Mailbox name. */
77
+ mailbox: string;
78
+ /** Inbucket message id. */
79
+ id: string;
80
+ /** Sender address string. */
81
+ from: string;
82
+ /** Recipient address strings. */
83
+ to: string[];
84
+ /** Subject line. */
85
+ subject: string;
86
+ /** ISO created timestamp. */
87
+ date: string;
88
+ /** Unix epoch milliseconds. */
89
+ "posix-millis": number;
90
+ /** Message size in bytes. */
91
+ size: number;
92
+ /** Whether the message has been seen in the UI/API. */
93
+ seen: boolean;
94
+ }
95
+ /** Attachment metadata from a full Inbucket message. */
96
+ export interface InbucketAttachment {
97
+ /** Attachment filename. */
98
+ filename: string;
99
+ /** MIME content type. */
100
+ "content-type": string;
101
+ /** Absolute download URL served by Inbucket. */
102
+ "download-link": string;
103
+ /** Absolute view URL served by Inbucket. */
104
+ "view-link": string;
105
+ /** MD5 checksum of the attachment bytes. */
106
+ md5: string;
107
+ }
108
+ /** Body parts from a full Inbucket message. */
109
+ export interface InbucketMessageBody {
110
+ /** Plain-text body. */
111
+ text: string;
112
+ /** HTML body. */
113
+ html: string;
114
+ }
115
+ /** Full message from `GET /api/v1/mailbox/{name}/{id}`. */
116
+ export interface InbucketMessage extends InbucketMessageHeader {
117
+ /** Text and HTML bodies. */
118
+ body: InbucketMessageBody;
119
+ /** Parsed header map. */
120
+ header: Record<string, string[]>;
121
+ /** Attachments (may be empty). */
122
+ attachments: InbucketAttachment[];
123
+ }
124
+ /** Error thrown when the Inbucket REST API returns a non-success response. */
125
+ export declare class InbucketError extends SentlyError {
126
+ readonly statusCode: number;
127
+ readonly apiError: unknown;
128
+ /** Creates an Inbucket API error with status code (`0` = network/connect failure). */
129
+ constructor(message: string, statusCode: number, apiError: unknown);
130
+ }
131
+ /**
132
+ * Development transport for [Inbucket](https://inbucket.org/).
133
+ *
134
+ * Sends via SMTP (defaults: `localhost:2500`) and exposes REST helpers for
135
+ * listing, reading, marking seen, deleting, and purging mailbox messages.
136
+ */
137
+ export declare class InbucketTransport implements Transport {
138
+ readonly provider = "inbucket";
139
+ private readonly host;
140
+ private readonly port;
141
+ private readonly secure;
142
+ private readonly requireTLS;
143
+ private readonly auth?;
144
+ private readonly tls?;
145
+ private readonly connectionTimeout?;
146
+ private readonly adapter?;
147
+ private readonly apiUrl;
148
+ private readonly mailboxNaming;
149
+ private smtp;
150
+ /** Creates an Inbucket transport with local-dev defaults. */
151
+ constructor(config?: InbucketConfig);
152
+ /** Web UI base URL (same origin as the REST API). */
153
+ get webUrl(): string;
154
+ /** Sends an email to Inbucket over SMTP. */
155
+ send(options: MailOptions): Promise<SendResult>;
156
+ /** Verifies SMTP connectivity to Inbucket. */
157
+ verify(): Promise<VerifyResult>;
158
+ /** Closes the underlying SMTP adapter if connected. */
159
+ close(): Promise<void>;
160
+ /**
161
+ * Maps an email address to an Inbucket mailbox name using {@link mailboxNaming}.
162
+ * Stock Inbucket uses `"local"` (the part before `@`).
163
+ */
164
+ mailboxForAddress(address: string): string;
165
+ /**
166
+ * Lists messages in a mailbox via `GET /api/v1/mailbox/{name}`.
167
+ * Newest messages appear last in a stock Inbucket response.
168
+ */
169
+ listMailbox(mailbox: string): Promise<InbucketMessageHeader[]>;
170
+ /**
171
+ * Fetches a full message via `GET /api/v1/mailbox/{name}/{id}`.
172
+ */
173
+ getMessage(mailbox: string, id: string): Promise<InbucketMessage>;
174
+ /**
175
+ * Fetches the raw message source via `GET /api/v1/mailbox/{name}/{id}/source`.
176
+ */
177
+ getSource(mailbox: string, id: string): Promise<string>;
178
+ /**
179
+ * Marks a message as seen via `PATCH /api/v1/mailbox/{name}/{id}`.
180
+ */
181
+ markSeen(mailbox: string, id: string): Promise<void>;
182
+ /**
183
+ * Deletes one message via `DELETE /api/v1/mailbox/{name}/{id}`.
184
+ */
185
+ deleteMessage(mailbox: string, id: string): Promise<void>;
186
+ /**
187
+ * Deletes every message in a mailbox via `DELETE /api/v1/mailbox/{name}`.
188
+ */
189
+ purgeMailbox(mailbox: string): Promise<void>;
190
+ private encodeSegment;
191
+ private getSmtp;
192
+ private apiHeaders;
193
+ private apiFetch;
194
+ private apiJson;
195
+ private apiOk;
196
+ }
@@ -0,0 +1,3 @@
1
+ import{n as Y}from"../chunk-a057jzsy.js";import"../chunk-f5x8vesr.js";import"../chunk-yg3kj723.js";import"../chunk-w4c9kj70.js";import"../chunk-1ke3nmyg.js";import{M as V}from"../chunk-d5aw5vag.js";import"../chunk-8kpgbrba.js";import"../chunk-hnzmn4s4.js";import{ja as W,ka as X}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var Z="localhost",$=2500,H="http://localhost:9000";class Q extends W{statusCode;apiError;constructor(q,w,z){super(q,w===0?"CONNECTION_FAILED":X(w),{...w>0?{statusCode:w}:{},provider:"inbucket",cause:z});this.statusCode=w;this.apiError=z;this.name="InbucketError"}}class M{provider="inbucket";host;port;secure;requireTLS;auth;tls;connectionTimeout;adapter;apiUrl;mailboxNaming;smtp=null;constructor(q={}){if(this.host=q.host??Z,this.port=q.port??$,this.secure=q.secure??!1,this.requireTLS=q.requireTLS??!1,q.auth!==void 0)this.auth=q.auth;if(q.tls!==void 0)this.tls=q.tls;if(q.connectionTimeout!==void 0)this.connectionTimeout=q.connectionTimeout;if(q.adapter!==void 0)this.adapter=q.adapter;this.apiUrl=(q.apiUrl??H).replace(/\/$/,""),this.mailboxNaming=q.mailboxNaming??"local"}get webUrl(){return this.apiUrl}async send(q){return(await this.getSmtp()).send(q)}async verify(){return{...await(await this.getSmtp()).verify(),provider:"inbucket"}}async close(){if(this.smtp)await this.smtp.close()}mailboxForAddress(q){let w=q.trim();if(!w)throw new Q("Email address is required",400,{code:"INVALID_CONFIG"});let z=w.lastIndexOf("@");if(z<=0||z===w.length-1)throw new Q(`Invalid email address: ${q}`,400,{code:"INVALID_CONFIG"});let G=w.slice(0,z),J=w.slice(z+1);switch(this.mailboxNaming){case"full":return w.toLowerCase();case"domain":return J.toLowerCase();default:return G.toLowerCase()}}async listMailbox(q){return this.apiJson("GET",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}`)}async getMessage(q,w){return this.apiJson("GET",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}/${this.encodeSegment(w,"Message id")}`)}async getSource(q,w){return(await this.apiFetch("GET",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}/${this.encodeSegment(w,"Message id")}/source`)).text()}async markSeen(q,w){await this.apiOk("PATCH",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}/${this.encodeSegment(w,"Message id")}`,{seen:!0})}async deleteMessage(q,w){await this.apiOk("DELETE",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}/${this.encodeSegment(w,"Message id")}`)}async purgeMailbox(q){await this.apiOk("DELETE",`/api/v1/mailbox/${this.encodeSegment(q,"Mailbox name")}`)}encodeSegment(q,w){if(!q)throw new Q(`${w} is required`,400,{code:"INVALID_CONFIG"});return encodeURIComponent(q)}async getSmtp(){if(!this.smtp){let q=this.adapter??await V({secure:this.secure,...this.connectionTimeout!==void 0?{connectionTimeout:this.connectionTimeout}:{},...this.tls!==void 0?{tls:this.tls}:{}});this.smtp=new Y({host:this.host,port:this.port,secure:this.secure,requireTLS:this.requireTLS,adapter:q,...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(){return{Accept:"application/json","Content-Type":"application/json"}}async apiFetch(q,w,z){let G;try{G=await fetch(`${this.apiUrl}${w}`,{method:q,headers:this.apiHeaders(),...z!==void 0?{body:JSON.stringify(z)}:{}})}catch(J){throw new Q(J instanceof Error?J.message:"Inbucket API request failed",0,J)}if(!G.ok){let J=await G.text().catch(()=>"");throw new Q(J||`Inbucket API error (${G.status})`,G.status,J)}return G}async apiJson(q,w,z){return await(await this.apiFetch(q,w,z)).json()}async apiOk(q,w,z){await this.apiFetch(q,w,z)}}export{M as InbucketTransport,Q as InbucketError,$ as INBUCKET_DEFAULT_PORT,Z as INBUCKET_DEFAULT_HOST,H as INBUCKET_DEFAULT_API_URL};
2
+
3
+ //# debugId=D6BB1EA239505BBC64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/inbucket.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * Inbucket development transport — SMTP to a local Inbucket catcher with\n * REST helpers for inspecting, reading, and purging mailbox messages.\n *\n * Defaults match a stock Inbucket install: SMTP `localhost:2500`,\n * UI/API `http://localhost:9000`.\n *\n * @example\n * ```ts\n * import { createMailer } from \"sently/mailer\";\n * import { InbucketTransport } from \"sently/transports/inbucket\";\n *\n * const inbucket = new InbucketTransport();\n * const mailer = await createMailer({ transport: inbucket });\n *\n * await mailer.send({\n * from: \"dev@example.com\",\n * to: \"you@example.com\",\n * subject: \"Hello\",\n * text: \"Captured by Inbucket\",\n * });\n *\n * const mailbox = inbucket.mailboxForAddress(\"you@example.com\");\n * const inbox = await inbucket.listMailbox(mailbox);\n * console.log(inbox[0]?.subject);\n * ```\n */\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 Inbucket instance. */\nexport const INBUCKET_DEFAULT_HOST = \"localhost\";\n\n/** Default SMTP port for a local Inbucket instance. */\nexport const INBUCKET_DEFAULT_PORT = 2500;\n\n/** Default web UI / REST API base URL for a local Inbucket instance. */\nexport const INBUCKET_DEFAULT_API_URL = \"http://localhost:9000\";\n\n/**\n * How Inbucket maps an email address to a mailbox name.\n * Matches `INBUCKET_MAILBOXNAMING` on the catcher (`local` by default).\n */\nexport type InbucketMailboxNaming = \"local\" | \"full\" | \"domain\";\n\n/** Inbucket development transport configuration. */\nexport interface InbucketConfig {\n /** SMTP hostname. Default: `\"localhost\"`. */\n host?: string;\n /** SMTP port. Default: `2500`. */\n port?: number;\n /** Use implicit TLS on connect. Default: `false` (Inbucket 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. */\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 * Inbucket web UI / REST API base URL (no trailing slash).\n * Default: `\"http://localhost:9000\"`.\n */\n apiUrl?: string;\n /**\n * Mailbox naming strategy for {@link InbucketTransport.mailboxForAddress}.\n * Default: `\"local\"` (local-part only), matching stock Inbucket.\n */\n mailboxNaming?: InbucketMailboxNaming;\n}\n\n/** Summary row from `GET /api/v1/mailbox/{name}`. */\nexport interface InbucketMessageHeader {\n /** Mailbox name. */\n mailbox: string;\n /** Inbucket message id. */\n id: string;\n /** Sender address string. */\n from: string;\n /** Recipient address strings. */\n to: string[];\n /** Subject line. */\n subject: string;\n /** ISO created timestamp. */\n date: string;\n /** Unix epoch milliseconds. */\n \"posix-millis\": number;\n /** Message size in bytes. */\n size: number;\n /** Whether the message has been seen in the UI/API. */\n seen: boolean;\n}\n\n/** Attachment metadata from a full Inbucket message. */\nexport interface InbucketAttachment {\n /** Attachment filename. */\n filename: string;\n /** MIME content type. */\n \"content-type\": string;\n /** Absolute download URL served by Inbucket. */\n \"download-link\": string;\n /** Absolute view URL served by Inbucket. */\n \"view-link\": string;\n /** MD5 checksum of the attachment bytes. */\n md5: string;\n}\n\n/** Body parts from a full Inbucket message. */\nexport interface InbucketMessageBody {\n /** Plain-text body. */\n text: string;\n /** HTML body. */\n html: string;\n}\n\n/** Full message from `GET /api/v1/mailbox/{name}/{id}`. */\nexport interface InbucketMessage extends InbucketMessageHeader {\n /** Text and HTML bodies. */\n body: InbucketMessageBody;\n /** Parsed header map. */\n header: Record<string, string[]>;\n /** Attachments (may be empty). */\n attachments: InbucketAttachment[];\n}\n\n/** Error thrown when the Inbucket REST API returns a non-success response. */\nexport class InbucketError extends SentlyError {\n /** Creates an Inbucket 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: \"inbucket\",\n cause: apiError,\n });\n this.name = \"InbucketError\";\n }\n}\n\n/**\n * Development transport for [Inbucket](https://inbucket.org/).\n *\n * Sends via SMTP (defaults: `localhost:2500`) and exposes REST helpers for\n * listing, reading, marking seen, deleting, and purging mailbox messages.\n */\nexport class InbucketTransport implements Transport {\n readonly provider = \"inbucket\";\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 mailboxNaming: InbucketMailboxNaming;\n\n private smtp: SMTPTransport | null = null;\n\n /** Creates an Inbucket transport with local-dev defaults. */\n constructor(config: InbucketConfig = {}) {\n this.host = config.host ?? INBUCKET_DEFAULT_HOST;\n this.port = config.port ?? INBUCKET_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 ?? INBUCKET_DEFAULT_API_URL).replace(/\\/$/, \"\");\n this.mailboxNaming = config.mailboxNaming ?? \"local\";\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 Inbucket over SMTP. */\n async send(options: MailOptions): Promise<SendResult> {\n return (await this.getSmtp()).send(options);\n }\n\n /** Verifies SMTP connectivity to Inbucket. */\n async verify(): Promise<VerifyResult> {\n const result = await (await this.getSmtp()).verify();\n return {\n ...result,\n provider: \"inbucket\",\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 * Maps an email address to an Inbucket mailbox name using {@link mailboxNaming}.\n * Stock Inbucket uses `\"local\"` (the part before `@`).\n */\n mailboxForAddress(address: string): string {\n const trimmed = address.trim();\n if (!trimmed) {\n throw new InbucketError(\"Email address is required\", 400, { code: \"INVALID_CONFIG\" });\n }\n const at = trimmed.lastIndexOf(\"@\");\n if (at <= 0 || at === trimmed.length - 1) {\n throw new InbucketError(`Invalid email address: ${address}`, 400, {\n code: \"INVALID_CONFIG\",\n });\n }\n const local = trimmed.slice(0, at);\n const domain = trimmed.slice(at + 1);\n switch (this.mailboxNaming) {\n case \"full\":\n return trimmed.toLowerCase();\n case \"domain\":\n return domain.toLowerCase();\n default:\n return local.toLowerCase();\n }\n }\n\n /**\n * Lists messages in a mailbox via `GET /api/v1/mailbox/{name}`.\n * Newest messages appear last in a stock Inbucket response.\n */\n async listMailbox(mailbox: string): Promise<InbucketMessageHeader[]> {\n return this.apiJson<InbucketMessageHeader[]>(\n \"GET\",\n `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}`,\n );\n }\n\n /**\n * Fetches a full message via `GET /api/v1/mailbox/{name}/{id}`.\n */\n async getMessage(mailbox: string, id: string): Promise<InbucketMessage> {\n return this.apiJson<InbucketMessage>(\n \"GET\",\n `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}/${this.encodeSegment(id, \"Message id\")}`,\n );\n }\n\n /**\n * Fetches the raw message source via `GET /api/v1/mailbox/{name}/{id}/source`.\n */\n async getSource(mailbox: string, id: string): Promise<string> {\n const response = await this.apiFetch(\n \"GET\",\n `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}/${this.encodeSegment(id, \"Message id\")}/source`,\n );\n return response.text();\n }\n\n /**\n * Marks a message as seen via `PATCH /api/v1/mailbox/{name}/{id}`.\n */\n async markSeen(mailbox: string, id: string): Promise<void> {\n await this.apiOk(\n \"PATCH\",\n `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}/${this.encodeSegment(id, \"Message id\")}`,\n { seen: true },\n );\n }\n\n /**\n * Deletes one message via `DELETE /api/v1/mailbox/{name}/{id}`.\n */\n async deleteMessage(mailbox: string, id: string): Promise<void> {\n await this.apiOk(\n \"DELETE\",\n `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}/${this.encodeSegment(id, \"Message id\")}`,\n );\n }\n\n /**\n * Deletes every message in a mailbox via `DELETE /api/v1/mailbox/{name}`.\n */\n async purgeMailbox(mailbox: string): Promise<void> {\n await this.apiOk(\"DELETE\", `/api/v1/mailbox/${this.encodeSegment(mailbox, \"Mailbox name\")}`);\n }\n\n private encodeSegment(value: string, label: string): string {\n if (!value) {\n throw new InbucketError(`${label} is required`, 400, { code: \"INVALID_CONFIG\" });\n }\n return encodeURIComponent(value);\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 return {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n };\n }\n\n private async apiFetch(method: string, path: string, body?: unknown): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(`${this.apiUrl}${path}`, {\n method,\n headers: this.apiHeaders(),\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n } catch (err) {\n throw new InbucketError(\n err instanceof Error ? err.message : \"Inbucket API request failed\",\n 0,\n err,\n );\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new InbucketError(\n text || `Inbucket API error (${response.status})`,\n response.status,\n text,\n );\n }\n\n return response;\n }\n\n private async apiJson<T>(method: string, path: string, body?: unknown): Promise<T> {\n const response = await this.apiFetch(method, path, body);\n return (await response.json()) as T;\n }\n\n private async apiOk(method: string, path: string, body?: unknown): Promise<void> {\n await this.apiFetch(method, path, body);\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "D6BB1EA239505BBC64756E2164756E21",
9
+ "names": []
10
+ }