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.
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +97 -0
- package/README.md +20 -5
- package/dist/chunk-z1589fjk.js.map +2 -2
- package/dist/core/push-types.d.ts +53 -4
- package/dist/transports/inbucket.d.ts +196 -0
- package/dist/transports/inbucket.js +3 -0
- package/dist/transports/inbucket.js.map +10 -0
- package/dist/transports/mailpit.d.ts +108 -8
- package/dist/transports/mailpit.js +2 -2
- package/dist/transports/mailpit.js.map +3 -3
- package/dist/transports/taqnyat-sms.d.ts +85 -0
- package/dist/transports/taqnyat-sms.js +2 -2
- package/dist/transports/taqnyat-sms.js.map +3 -3
- package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
- package/dist/transports/taqnyat-whatsapp.js +2 -2
- package/dist/transports/taqnyat-whatsapp.js.map +3 -3
- package/dist/transports/webpush.d.ts +24 -1
- package/dist/transports/webpush.js +2 -2
- package/dist/transports/webpush.js.map +3 -3
- package/dist/webhooks/sndr.js +2 -2
- package/dist/webhooks/sndr.js.map +3 -3
- package/package.json +7 -2
- package/site/content/docs/ai/llms-txt.mdx +2 -0
- package/site/content/docs/channels/email.mdx +2 -1
- package/site/content/docs/channels/push.mdx +3 -1
- package/site/content/docs/decorators/preview.mdx +2 -1
- package/site/content/docs/get-started/entrypoints.mdx +1 -1
- package/site/content/docs/get-started/support-matrix.mdx +2 -2
- package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
- package/site/content/docs/guides/webhooks.mdx +2 -0
- package/site/content/docs/guides/webpush-interop.mdx +9 -1
- package/site/content/docs/reference/exports.mdx +1 -1
- package/site/content/docs/reference/push-options.mdx +22 -5
- package/site/content/docs/transports/inbucket.mdx +200 -0
- package/site/content/docs/transports/index.mdx +2 -3
- package/site/content/docs/transports/mailpit.mdx +114 -14
- package/site/content/docs/transports/meta.json +5 -4
- package/site/content/docs/transports/smtp.mdx +3 -2
- package/site/content/docs/transports/sndr.mdx +68 -5
- package/site/content/docs/transports/taqnyat.mdx +361 -0
- package/site/content/docs/transports/webpush.mdx +208 -17
- package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
- package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
- 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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|