@voltro/plugin-mail 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @voltro/plugin-mail
4
4
 
5
- **Transactional email plugin — one MailProvider contract, multiple adapters (Resend / Postmark / SendGrid / SMTP + console/memory for dev/test). Exposes a MailService via the plugin services layer with transient retry and a dev allowlist.**
5
+ **Transactional and broadcast email — one MailProvider contract, multiple adapters (Lettermint / Resend / Postmark / SendGrid / SES / Mailgun / SMTP + console/memory for dev/test). Exposes a MailService via the plugin services layer with transient retry, suppression, a mounted delivery-event webhook and a dev allowlist.**
6
6
 
7
7
  [📖 Documentation](https://docs.voltro.dev/docs/plugins/mail) · [voltro.dev](https://voltro.dev) · [Voltro Cloud](https://voltro.cloud)
8
8
 
@@ -4348,7 +4348,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
4348
4348
  THE SOFTWARE.
4349
4349
  ```
4350
4350
 
4351
- ## nodemailer@9.1.1
4351
+ ## nodemailer@10.0.11
4352
4352
 
4353
4353
  License: MIT-0
4354
4354
 
@@ -5155,7 +5155,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
5155
5155
  SOFTWARE.
5156
5156
  ```
5157
5157
 
5158
- ## undici@8.10.0
5158
+ ## undici@8.11.2
5159
5159
 
5160
5160
  License: MIT
5161
5161
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Context } from 'effect';
2
2
  import { Effect } from 'effect';
3
3
  import { Layer } from 'effect';
4
+ import { PluginEgressDeclaration } from '@voltro/protocol';
5
+ import { PluginHttpRouteRequest } from '@voltro/protocol';
4
6
  import { Schema } from 'effect';
5
7
  import { VoltroPlugin } from '@voltro/protocol';
6
8
 
@@ -121,6 +123,21 @@ export declare interface IdempotencyStore {
121
123
  * exports (name + subject fn + render fn + a props schema). */
122
124
  export declare const isEmailTemplate: (v: unknown) => v is AnyEmailTemplate;
123
125
 
126
+ export declare interface LettermintOptions {
127
+ /** Project API token (`lm_…`), sent as `x-lettermint-token`. */
128
+ readonly projectToken: string;
129
+ /** Default `https://api.lettermint.co/v1`. */
130
+ readonly baseUrl?: string;
131
+ /** Route slug per stream. Default: the project's default route (its
132
+ * transactional one) for `transactional`, `broadcast` for `broadcast`. */
133
+ readonly routes?: {
134
+ readonly transactional?: string;
135
+ readonly broadcast?: string;
136
+ };
137
+ }
138
+
139
+ export declare const lettermintProvider: (options: LettermintOptions) => MailProvider;
140
+
124
141
  /** Names (+ locale variants) of every globally-registered template. */
125
142
  export declare const listGlobalTemplates: () => ReadonlyArray<{
126
143
  readonly name: string;
@@ -141,6 +158,12 @@ export declare interface MailAttachment {
141
158
  readonly contentType?: string;
142
159
  }
143
160
 
161
+ /** What mail egress a provider choice declares. */
162
+ export declare const mailEgressOf: (options: {
163
+ readonly provider?: MailProviderName | MailProvider;
164
+ readonly env?: Readonly<Record<string, string | undefined>>;
165
+ }) => ReadonlyArray<PluginEgressDeclaration>;
166
+
144
167
  /**
145
168
  * Mail failure. `transient` drives retry — HTTP 408 / 429 / 5xx and network
146
169
  * failures are retryable; other HTTP 4xx are not. SMTP instead retries 4xx
@@ -164,10 +187,21 @@ export declare interface MailEvent {
164
187
  readonly email: string;
165
188
  readonly provider: string;
166
189
  readonly messageId?: string;
190
+ /** The provider's id for this event — the key to act on a redelivered
191
+ * webhook only once. */
192
+ readonly eventId?: string;
193
+ /** What the message was sent with as `metadata`, returned by the provider. */
194
+ readonly metadata?: Readonly<Record<string, string>>;
167
195
  readonly raw: unknown;
168
196
  }
169
197
 
170
- export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'opened' | 'clicked' | 'unknown';
198
+ /** `unsubscribed`: the recipient left a broadcast through the provider's own
199
+ * unsubscribe (a provider that `managesUnsubscribe`). It is NOT suppressed
200
+ * app-wide — it ended one list, not the person's transactional mail.
201
+ * `unsuppressed`: the provider lifted a suppression (someone removed it in
202
+ * its dashboard) — lifted here too, or the app would keep dropping mail the
203
+ * provider would now deliver. */
204
+ export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'unsubscribed' | 'unsuppressed' | 'opened' | 'clicked' | 'unknown';
171
205
 
172
206
  export declare interface MailgunOptions {
173
207
  readonly apiKey: string;
@@ -208,12 +242,30 @@ export declare interface MailMessage {
208
242
  * step. The first send with a given key delivers; a replay with the same
209
243
  * key is a no-op returning the recorded result. Scoped per tenant. */
210
244
  readonly idempotencyKey?: string;
245
+ /** `transactional` (default) or `broadcast`. See `MailStream`. */
246
+ readonly stream?: MailStream;
247
+ /** How a recipient leaves a broadcast. See `MailUnsubscribe`. */
248
+ readonly unsubscribe?: MailUnsubscribe;
249
+ /** Key/value pairs the provider stores with the message and returns on its
250
+ * delivery events (`MailEvent.metadata`) — how a bounce finds its way back
251
+ * to the row that caused it. Never added to the mail itself. Lettermint and
252
+ * Postmark carry it; other providers ignore it. */
253
+ readonly metadata?: Readonly<Record<string, string>>;
211
254
  }
212
255
 
213
- export declare const mailPlugin: (options?: MailPluginOptions) => VoltroPlugin;
256
+ /**
257
+ * The mail plugin, with the service it built — the same one handlers yield as
258
+ * `MailService`. For wiring that happens in `app.config.ts`, before any
259
+ * handler runs: `authRoutesPlugin({ sendEmail: mailSender(mail.service) })`.
260
+ */
261
+ export declare type MailPlugin = VoltroPlugin & {
262
+ readonly service: MailServiceShape;
263
+ };
264
+
265
+ export declare const mailPlugin: (options?: MailPluginOptions) => MailPlugin;
214
266
 
215
267
  export declare interface MailPluginOptions {
216
- /** `'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
268
+ /** `'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
217
269
  * (built from env) or a `MailProvider`. Default: `MAIL_PROVIDER` env, else
218
270
  * `'console'`. */
219
271
  readonly provider?: MailProviderName | MailProvider;
@@ -247,7 +299,9 @@ export declare interface MailPluginOptions {
247
299
  /** How often (ms) the scheduled-send flush loop wakes to deliver due held
248
300
  * messages. Default 1000. Ignored when the provider schedules natively. */
249
301
  readonly scheduleTickMs?: number;
250
- /** Disambiguates multiple instances of this plugin in one app. */
302
+ /** Mount the provider's delivery-event webhook. Needs a provider with a
303
+ * `webhook` verifier (Lettermint). See `MailWebhookOptions`. */
304
+ readonly webhook?: MailWebhookOptions;
251
305
  /**
252
306
  * Namespace for this plugin's inspect endpoints. Default `mail`.
253
307
  *
@@ -290,9 +344,24 @@ export declare interface MailProvider {
290
344
  /** One request for N personalized messages. Each keeps its own subject /
291
345
  * body / template vars. Returns one `SendResult` per input, in order. */
292
346
  readonly sendBatch?: (messages: ReadonlyArray<ResolvedMessage>) => Effect.Effect<ReadonlyArray<SendResult>, MailError>;
347
+ /** True if the provider adds its own unsubscribe link and one-click headers
348
+ * to broadcast mail and suppresses whoever uses them (Lettermint's
349
+ * broadcast route). The service then sends no `List-Unsubscribe` of its own
350
+ * — two competing unsubscribe mechanisms would each only half work. */
351
+ readonly managesUnsubscribe?: boolean;
352
+ /** Verifies and parses the provider's delivery-event webhook. Present →
353
+ * `mailPlugin({ webhook })` can mount the endpoint. */
354
+ readonly webhook?: MailProviderWebhook;
293
355
  }
294
356
 
295
- export declare type MailProviderName = 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
357
+ export declare type MailProviderName = 'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
358
+
359
+ /** A provider's delivery-event webhook: signature check, then normalisation. */
360
+ export declare interface MailProviderWebhook {
361
+ /** Verify the signature over the RAW body and decode it. Fails with
362
+ * `MailWebhookError` for a missing, stale or forged signature. */
363
+ readonly verify: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
364
+ }
296
365
 
297
366
  export declare interface MailSendRecord {
298
367
  readonly id: string;
@@ -356,6 +425,69 @@ export declare interface MailServiceShape {
356
425
  readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
357
426
  /** Is this address currently suppressed for the tenant? */
358
427
  readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect<boolean>;
428
+ /** The provider behind the service. `managesUnsubscribe` tells a broadcast
429
+ * sender whether the provider already puts an unsubscribe link in the mail
430
+ * (then the body needs none of its own). Absent on a hand-built service. */
431
+ readonly provider?: {
432
+ readonly name: string;
433
+ readonly managesUnsubscribe: boolean;
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Which kind of mail this is. `transactional` (the default) is one-to-one mail
439
+ * a person's own action triggered — an invitation, a receipt, a password
440
+ * reset. `broadcast` is one-to-many mail — a newsletter, a campaign. Providers
441
+ * that separate the two (Lettermint routes, Postmark message streams) send
442
+ * them from different infrastructure, so a newsletter's complaints never cost
443
+ * a password reset its inbox placement.
444
+ */
445
+ export declare type MailStream = 'transactional' | 'broadcast';
446
+
447
+ /**
448
+ * How a recipient leaves a broadcast. `oneClickUrl` is the HTTPS endpoint a
449
+ * mail client POSTs `List-Unsubscribe=One-Click` to (RFC 8058); the service
450
+ * turns it into the `List-Unsubscribe` + `List-Unsubscribe-Post` headers.
451
+ * Required on a broadcast unless the provider unsubscribes recipients itself
452
+ * (`MailProvider.managesUnsubscribe`).
453
+ */
454
+ export declare interface MailUnsubscribe {
455
+ readonly oneClickUrl: string;
456
+ /** Optional `mailto:` alternative listed beside the URL. */
457
+ readonly mailto?: string;
458
+ }
459
+
460
+ /** A webhook that failed verification — answer 401, never process it. */
461
+ export declare class MailWebhookError extends MailWebhookError_base {
462
+ }
463
+
464
+ declare const MailWebhookError_base: Schema.TaggedErrorClass<MailWebhookError, "MailWebhookError", {
465
+ readonly _tag: Schema.tag<"MailWebhookError">;
466
+ } & {
467
+ provider: typeof Schema.String;
468
+ message: typeof Schema.String;
469
+ }>;
470
+
471
+ /**
472
+ * The provider's delivery-event webhook, mounted by the plugin. Bounces and
473
+ * spam complaints suppress the address in the plugin's OWN suppression store
474
+ * — the one every send checks — then `onEvent` sees every event.
475
+ */
476
+ export declare interface MailWebhookOptions {
477
+ /** Signing secret. Default: `MAIL_WEBHOOK_SECRET`. Without one the endpoint
478
+ * answers 503, and a production boot refuses. */
479
+ readonly secret?: string;
480
+ /** Default `/webhooks/mail`. */
481
+ readonly path?: string;
482
+ /**
483
+ * Every verified event, after suppression — an unsubscribe to record, a
484
+ * bounce to mark on the row the message's `metadata` names. A failure
485
+ * answers 500 and the provider redelivers, so act idempotently
486
+ * (`event.eventId`). `store` is the app's boot store, as on any plugin route.
487
+ */
488
+ readonly onEvent?: (event: MailEvent, ctx: {
489
+ readonly store: PluginHttpRouteRequest['store'];
490
+ }) => Effect.Effect<void, unknown> | Promise<void> | void;
359
491
  }
360
492
 
361
493
  export declare const makeScheduler: (options: SchedulerOptions) => Scheduler;
@@ -402,6 +534,10 @@ export declare const postgresSuppressionStore: (options?: PostgresSuppressionOpt
402
534
 
403
535
  export declare const postmarkProvider: (options: {
404
536
  readonly serverToken: string;
537
+ readonly streams?: {
538
+ readonly transactional?: string;
539
+ readonly broadcast?: string;
540
+ };
405
541
  }) => MailProvider;
406
542
 
407
543
  /** Stable snapshot of everything the memory provider has "sent". */
@@ -558,8 +694,19 @@ export declare interface TemplateMessage {
558
694
  readonly scheduledAt?: Date;
559
695
  /** See `MailMessage.idempotencyKey`. */
560
696
  readonly idempotencyKey?: string;
697
+ /** See `MailMessage.stream`. */
698
+ readonly stream?: MailStream;
699
+ /** See `MailMessage.unsubscribe`. */
700
+ readonly unsubscribe?: MailUnsubscribe;
701
+ /** See `MailMessage.metadata`. */
702
+ readonly metadata?: Readonly<Record<string, string>>;
561
703
  }
562
704
 
705
+ /** Lettermint signs `${t}.${rawBody}` with HMAC-SHA256 under the webhook
706
+ * secret: `x-lettermint-signature: t=<unix>,v1=<hex>` (several `v1` during a
707
+ * secret rotation), `x-lettermint-delivery: <unix>` must equal `t`. */
708
+ export declare const verifyLettermintSignature: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number, toleranceSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
709
+
563
710
  export { VoltroPlugin }
564
711
 
565
712
  export { }