@shaferllc/keel 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
package/docs/mail.md ADDED
@@ -0,0 +1,678 @@
1
+ # Mail
2
+
3
+ Send email through a pluggable **transport**. Compose a message with a fluent
4
+ builder and dispatch it — the API mirrors the database layer (`setMailer` /
5
+ `mail()` are to mail what `setConnection` / `db()` are to the database). The core
6
+ imports no SDK: the built-in transports use `fetch`, `console`, or memory, so it
7
+ runs on Node and the edge.
8
+
9
+ ## Sending
10
+
11
+ ```ts
12
+ import { mail } from "@shaferllc/keel/core";
13
+
14
+ await mail()
15
+ .to("ada@example.com")
16
+ .subject("Welcome aboard")
17
+ .html("<h1>Hi Ada</h1>")
18
+ .send();
19
+ ```
20
+
21
+ Every setter is chainable, and several accept multiple values:
22
+
23
+ ```ts
24
+ await mail()
25
+ .to("a@x.com", "b@x.com")
26
+ .cc("team@x.com")
27
+ .bcc("audit@x.com")
28
+ .replyTo("support@x.com")
29
+ .from("hello@x.com") // optional if a default is configured
30
+ .subject("Report")
31
+ .text("Plain-text body")
32
+ .html("<p>HTML body</p>")
33
+ .header("X-Campaign", "weekly")
34
+ .send();
35
+ ```
36
+
37
+ Seed several fields at once with `fill()`:
38
+
39
+ ```ts
40
+ await mail().fill({ to: "a@x.com", subject: "Hi", text: "body" }).send();
41
+ ```
42
+
43
+ `send()` resolves to the **finalized message** — the same object the transport
44
+ received, with the default `from` already applied. Handy for logging or
45
+ assertions:
46
+
47
+ ```ts
48
+ const sent = await mail().to("ada@example.com").subject("Hi").text("hey").send();
49
+ sent.from; // the resolved from address
50
+ sent.to; // ["ada@example.com"]
51
+ ```
52
+
53
+ ## Validation & error behavior
54
+
55
+ A message needs at least one recipient, a subject, a body (`text` or `html`),
56
+ and a `from` — `send()` throws a clear `Error` otherwise, before the transport is
57
+ ever called. The checks run in this order:
58
+
59
+ | Missing | Message |
60
+ |---------|---------|
61
+ | `to` (empty) | `Mail: at least one recipient (to) is required.` |
62
+ | `subject` | `Mail: a subject is required.` |
63
+ | `text` **and** `html` | `Mail: a text or html body is required.` |
64
+ | `from` (and no default) | `Mail: a from address is required (set one or a default).` |
65
+
66
+ The `from` default from `setMailer(..., { from })` is applied first, so a
67
+ configured default satisfies the last check without any per-message `from`.
68
+
69
+ ## Configuring the transport
70
+
71
+ Register a default transport once (typically in a service provider):
72
+
73
+ ```ts
74
+ import { setMailer, fetchTransport } from "@shaferllc/keel/core";
75
+
76
+ setMailer(
77
+ fetchTransport({
78
+ url: "https://api.resend.com/emails",
79
+ headers: { Authorization: `Bearer ${env("RESEND_API_KEY")}` },
80
+ body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
81
+ }),
82
+ { from: "hello@myapp.com" }, // default `from` for messages that omit one
83
+ );
84
+ ```
85
+
86
+ `fetchTransport` POSTs JSON to any provider API (Resend, Postmark, Mailgun, …).
87
+ The optional `body` mapper shapes the request for that provider; without it the
88
+ message is sent as-is. A non-2xx response throws
89
+ `Mail: transport responded <status> <statusText>`.
90
+
91
+ ## Built-in transports
92
+
93
+ | Transport | Use |
94
+ |-----------|-----|
95
+ | `ArrayTransport` | Collects messages in `.sent` — the default, and ideal for tests |
96
+ | `LogTransport` | Logs each message via the logger instead of delivering — local dev |
97
+ | `fetchTransport(opts)` | POSTs to a provider HTTP API via `fetch` — production |
98
+
99
+ Until you call `setMailer`, the default mailer is a fresh `ArrayTransport` — so
100
+ `mail()` never throws for want of a transport, it just buffers in memory.
101
+
102
+ ## Writing your own transport
103
+
104
+ A transport is one method:
105
+
106
+ ```ts
107
+ import type { Transport } from "@shaferllc/keel/core";
108
+
109
+ const transport: Transport = {
110
+ async send(message) {
111
+ // hand `message` to any SDK or API you like
112
+ },
113
+ };
114
+ setMailer(transport, { from: "hello@myapp.com" });
115
+ ```
116
+
117
+ The `message` your `send` receives is already validated and has `from` resolved,
118
+ so a transport can trust every required field is present.
119
+
120
+ ## Queueing: `sendLater()`
121
+
122
+ Sending is slow and it fails. Holding a request open for an SMTP round trip means
123
+ the user waits on your provider, and a provider hiccup turns "sign up" into an
124
+ error page. Put the message on the [queue](./queues.md) instead:
125
+
126
+ ```ts
127
+ await mail().to(user.email).subject("Welcome").html(body).sendLater();
128
+ ```
129
+
130
+ The request returns immediately, and a failed send **retries on the queue** rather
131
+ than failing the user's action. Everything else is identical — same builder, same
132
+ transport.
133
+
134
+ The message is **validated at the call site**, not on the worker: a missing
135
+ recipient throws where you composed it, where the stack trace means something,
136
+ rather than surfacing in a worker log an hour later.
137
+
138
+ With the default `SyncDriver` this still sends inline (nothing is deferred until
139
+ you register a real driver), so `sendLater()` is safe to adopt before you have a
140
+ queue.
141
+
142
+ ## Attachments
143
+
144
+ ```ts
145
+ await mail()
146
+ .to("ada@example.com")
147
+ .subject("Your invoice")
148
+ .html('<p>Attached. <img src="cid:logo"></p>')
149
+ .attach("invoice.pdf", pdfBytes) // content type inferred: application/pdf
150
+ .attach("data.csv", "a,b,c", "text/csv") // ...or set it
151
+ .embed("logo", logoBytes, "logo.png") // inline, referenced as cid:logo
152
+ .send();
153
+ ```
154
+
155
+ `attach(filename, content, contentType?)` takes a string or `Uint8Array`; the
156
+ content type is inferred from the extension when you don't give one.
157
+
158
+ `embed(cid, content, filename?, contentType?)` is the same thing with a **content
159
+ id**, so the HTML body can display it inline via `<img src="cid:logo">` instead of
160
+ linking out to a hosted image.
161
+
162
+ ## Class-based mails
163
+
164
+ A one-liner is fine until the email has real content. `BaseMail` is to mail what
165
+ `Job` is to the queue — a reusable, testable class:
166
+
167
+ ```ts
168
+ import { BaseMail, type PendingMail } from "@shaferllc/keel/core";
169
+
170
+ export class WelcomeEmail extends BaseMail {
171
+ constructor(private user: User) {
172
+ super();
173
+ }
174
+
175
+ build(message: PendingMail) {
176
+ message
177
+ .to(this.user.email)
178
+ .subject(`Welcome, ${this.user.name}`)
179
+ .html(`<h1>Hi ${this.user.name}</h1>`);
180
+ }
181
+ }
182
+ ```
183
+
184
+ ```ts
185
+ import { send, sendLater } from "@shaferllc/keel/core";
186
+
187
+ await send(new WelcomeEmail(user));
188
+ await sendLater(new WelcomeEmail(user)); // ...or queue it
189
+ ```
190
+
191
+ `build()` may be async, so it can render a template or fetch what it needs.
192
+
193
+ ## Multiple mailers
194
+
195
+ Register mailers by name — a transactional provider and a marketing one, say — and
196
+ pick one with `mail(name)`:
197
+
198
+ ```ts
199
+ setMailer(postmark, { from: "hi@app.com" }); // the default
200
+ setMailer(resend, { from: "news@app.com" }, "marketing");
201
+
202
+ await mail().to(user.email).subject("Receipt").text(body).send();
203
+ await mail("marketing").to(user.email).subject("This month").html(body).send();
204
+ ```
205
+
206
+ `send(email, name)` and `sendLater(email, name)` take a mailer name too.
207
+
208
+ ## In tests
209
+
210
+ `fakeMail()` swaps the mailer for one that **records instead of delivering**, so
211
+ tests never talk to a provider. `restoreMail()` puts the real one back.
212
+
213
+ ```ts
214
+ import { fakeMail, restoreMail } from "@shaferllc/keel/core";
215
+
216
+ const mailer = fakeMail();
217
+
218
+ await registerUser();
219
+
220
+ mailer.assertSent();
221
+ mailer.assertSent((m) => m.subject === "Welcome");
222
+ mailer.assertSentCount(1);
223
+ mailer.assertQueued((m) => m.to.includes("ada@example.com")); // sent with sendLater()
224
+ mailer.assertNotSent((m) => m.subject === "Password reset");
225
+ mailer.assertNothingSent();
226
+
227
+ restoreMail();
228
+ ```
229
+
230
+ The fake keeps **sent** and **queued** separate — `assertSent` only matches
231
+ `send()`, `assertQueued` only `sendLater()` — so a test can tell "we emailed them"
232
+ from "we queued an email". A faked `sendLater()` doesn't touch the real queue
233
+ either; recording the intent is the point.
234
+
235
+ It still **validates** the message, so a fake can't paper over a message the real
236
+ mailer would reject.
237
+
238
+ `mailer.sent()` and `mailer.queued()` return the raw messages if you'd rather
239
+ assert by hand.
240
+
241
+ If you want the transport-level view instead, `ArrayTransport` still works:
242
+
243
+ ```ts
244
+ const transport = new ArrayTransport();
245
+ setMailer(transport, { from: "hi@app.com" });
246
+
247
+ await mail().to("ada@example.com").subject("Welcome").text("hi").send();
248
+
249
+ assert.equal(transport.sent[0].subject, "Welcome");
250
+ ```
251
+
252
+ You can also hold your own `Mailer` instead of the global one — construct it with
253
+ a transport and reuse it, leaving the process-wide `mail()` untouched:
254
+
255
+ ```ts
256
+ import { Mailer, ArrayTransport } from "@shaferllc/keel/core";
257
+
258
+ const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
259
+ await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
260
+ ```
261
+
262
+ ## Events
263
+
264
+ Every send fires [events](./events.md), so logging, metrics, and auditing can hang
265
+ off mail without touching the mailer:
266
+
267
+ | Event | When |
268
+ |-------|------|
269
+ | `mail.sending` | before the transport is called |
270
+ | `mail.sent` | after it returns |
271
+ | `mail.queued` | a `sendLater()` message reached the queue |
272
+
273
+ Each carries the final `Message` — after defaults are applied.
274
+
275
+ ```ts
276
+ listen("mail.sent", (message) => logger().info("mail sent", { subject: message.subject }));
277
+ ```
278
+
279
+ ## Related
280
+
281
+ The mail layer stands alone, but the [database](./database.md) builder shares its
282
+ shape (`setConnection`/`db` mirror `setMailer`/`mail`) — the same register-once,
283
+ call-anywhere pattern.
284
+
285
+ ---
286
+
287
+ ## API reference
288
+
289
+ ### `mail()`
290
+
291
+ `mail(): PendingMail`
292
+
293
+ Starts composing a message on the default (global) mailer.
294
+
295
+ ```ts
296
+ await mail().to("ada@example.com").subject("Hi").text("hey").send();
297
+ ```
298
+
299
+ **Notes:** a thin shortcut for `getMailer().message()`. Uses whatever transport
300
+ and options were last passed to `setMailer` (an in-memory `ArrayTransport` if you
301
+ never called it).
302
+
303
+ ### `setMailer(transport, options?)`
304
+
305
+ `setMailer(transport: Transport, options?: MailerOptions): Mailer`
306
+
307
+ Replaces the global mailer with a new one built from `transport` and `options`,
308
+ and returns it.
309
+
310
+ ```ts
311
+ setMailer(fetchTransport({ url }), { from: "hello@myapp.com" });
312
+ ```
313
+
314
+ **Notes:** global — the last call wins. Returns the constructed `Mailer` if you
315
+ want a direct handle. `options` defaults to `{}` (no default `from`).
316
+
317
+ ### `getMailer()`
318
+
319
+ `getMailer(): Mailer`
320
+
321
+ Returns the current global `Mailer` instance.
322
+
323
+ ```ts
324
+ const mailer = getMailer();
325
+ await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
326
+ ```
327
+
328
+ **Notes:** before any `setMailer` call this is a `Mailer` wrapping a fresh
329
+ `ArrayTransport`.
330
+
331
+ ### `fetchTransport(options)`
332
+
333
+ `fetchTransport(options: FetchTransportOptions): Transport`
334
+
335
+ Builds a `Transport` that POSTs each message as JSON to a provider HTTP API via
336
+ `fetch`.
337
+
338
+ ```ts
339
+ const transport = fetchTransport({
340
+ url: "https://api.resend.com/emails",
341
+ headers: { Authorization: `Bearer ${apiKey}` },
342
+ body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
343
+ });
344
+ ```
345
+
346
+ **Notes:** always sets `Content-Type: application/json`; your `headers` merge on
347
+ top. Without a `body` mapper the raw `Message` is serialized. Throws
348
+ `Mail: transport responded <status> <statusText>` on any non-`ok` response.
349
+
350
+ ### `Mailer`
351
+
352
+ The engine that validates a message, applies defaults, and hands it to the
353
+ transport. Construct one directly (`new Mailer(transport, options?)`) for a
354
+ scoped mailer, or reach the global one via `getMailer()` / `setMailer()`.
355
+
356
+ #### `new Mailer(transport, options?)`
357
+
358
+ `new Mailer(transport: Transport, options?: MailerOptions)`
359
+
360
+ Wraps a transport and its options.
361
+
362
+ ```ts
363
+ const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
364
+ ```
365
+
366
+ **Notes:** `options` defaults to `{}`. The transport is fixed for this instance —
367
+ build a new `Mailer` to swap it.
368
+
369
+ #### `message()`
370
+
371
+ `message(): PendingMail`
372
+
373
+ Starts a new `PendingMail` bound to this mailer.
374
+
375
+ ```ts
376
+ const pending = mailer.message();
377
+ ```
378
+
379
+ **Notes:** each call returns a fresh builder; nothing is shared between messages.
380
+
381
+ #### `send(message)`
382
+
383
+ `send(message: Message): Promise<Message>`
384
+
385
+ Applies the default `from`, validates the message, dispatches it through the
386
+ transport, and resolves to the finalized message.
387
+
388
+ ```ts
389
+ const sent = await mailer.send({ to: ["ada@x.com"], subject: "Hi", text: "hey" });
390
+ ```
391
+
392
+ **Notes:** throws (before touching the transport) if `to` is empty, or `subject`,
393
+ a body, or `from` is missing — see [Validation](#validation--error-behavior).
394
+ `PendingMail.send()` funnels through here. The returned object is a shallow copy
395
+ with `from` resolved.
396
+
397
+ ### `PendingMail`
398
+
399
+ The fluent builder. You get one from `mail()` or `mailer.message()`, never
400
+ `new`. Every setter returns `this`, so calls chain in any order; nothing is sent
401
+ until `send()`.
402
+
403
+ #### `to(...addresses)`
404
+
405
+ `to(...addresses: string[]): this`
406
+
407
+ Appends one or more recipients.
408
+
409
+ ```ts
410
+ mail().to("a@x.com", "b@x.com");
411
+ ```
412
+
413
+ **Notes:** additive — repeated calls accumulate recipients rather than replace.
414
+
415
+ #### `from(address)`
416
+
417
+ `from(address: string): this`
418
+
419
+ Sets the sender, overriding the mailer's default `from`.
420
+
421
+ ```ts
422
+ mail().from("hello@x.com");
423
+ ```
424
+
425
+ **Notes:** a single value (not variadic). Optional when a default `from` is
426
+ configured on the mailer.
427
+
428
+ #### `cc(...addresses)` / `bcc(...addresses)`
429
+
430
+ `cc(...addresses: string[]): this`
431
+ `bcc(...addresses: string[]): this`
432
+
433
+ Append carbon-copy / blind-carbon-copy recipients.
434
+
435
+ ```ts
436
+ mail().cc("team@x.com").bcc("audit@x.com");
437
+ ```
438
+
439
+ **Notes:** both additive, like `to`. The underlying arrays are created lazily on
440
+ first use.
441
+
442
+ #### `replyTo(address)`
443
+
444
+ `replyTo(address: string): this`
445
+
446
+ Sets the `Reply-To` address.
447
+
448
+ ```ts
449
+ mail().replyTo("support@x.com");
450
+ ```
451
+
452
+ **Notes:** a single value; a later call replaces the prior one.
453
+
454
+ #### `subject(subject)`
455
+
456
+ `subject(subject: string): this`
457
+
458
+ Sets the subject line.
459
+
460
+ ```ts
461
+ mail().subject("Welcome aboard");
462
+ ```
463
+
464
+ **Notes:** required — `send()` throws if it's empty. A later call replaces it.
465
+
466
+ #### `text(text)` / `html(html)`
467
+
468
+ `text(text: string): this`
469
+ `html(html: string): this`
470
+
471
+ Set the plain-text / HTML body. At least one is required.
472
+
473
+ ```ts
474
+ mail().text("Plain body").html("<p>Rich body</p>");
475
+ ```
476
+
477
+ **Notes:** you can set both (a multipart message); `send()` throws only if
478
+ *neither* is present. Each later call replaces its body.
479
+
480
+ #### `header(name, value)`
481
+
482
+ `header(name: string, value: string): this`
483
+
484
+ Adds a custom header.
485
+
486
+ ```ts
487
+ mail().header("X-Campaign", "weekly");
488
+ ```
489
+
490
+ **Notes:** additive per name — repeated calls with distinct names accumulate;
491
+ the same name overwrites. The `headers` object is created lazily.
492
+
493
+ #### `fill(partial)`
494
+
495
+ `fill(partial: Partial<{ to: string | string[]; cc: string | string[]; bcc: string | string[] } & Omit<Message, "to" | "cc" | "bcc">>): this`
496
+
497
+ Seeds several fields at once, merging into whatever's been chained.
498
+
499
+ ```ts
500
+ mail().fill({ to: ["a@x.com", "b@x.com"], subject: "Hi", text: "body" });
501
+ ```
502
+
503
+ **Notes:** `to`/`cc`/`bcc` accept a single string or an array and are **appended**
504
+ to any existing recipients. The other fields (`from`, `subject`, `text`, `html`,
505
+ `replyTo`, `headers`) are assigned, **replacing** prior values — passing
506
+ `headers` here overwrites the whole header map rather than merging.
507
+
508
+ #### `send()`
509
+
510
+ `send(): Promise<Message>`
511
+
512
+ Hands the composed message to the mailer and resolves to the finalized message.
513
+
514
+ ```ts
515
+ const sent = await mail().to("ada@x.com").subject("Hi").text("hey").send();
516
+ ```
517
+
518
+ **Notes:** delegates to `Mailer.send`, so the same validation and default-`from`
519
+ handling apply; it throws on a missing required field.
520
+
521
+ ### `ArrayTransport`
522
+
523
+ An in-memory transport that records every message. The default transport, and
524
+ the one to use in tests.
525
+
526
+ #### `new ArrayTransport()`
527
+
528
+ `new ArrayTransport()`
529
+
530
+ Creates a transport with an empty `sent` array.
531
+
532
+ ```ts
533
+ const transport = new ArrayTransport();
534
+ ```
535
+
536
+ #### `sent`
537
+
538
+ `readonly sent: Message[]`
539
+
540
+ The messages this transport has received, in order.
541
+
542
+ ```ts
543
+ const transport = new ArrayTransport();
544
+ setMailer(transport);
545
+ // ...after sending...
546
+ transport.sent.length; // number of messages queued
547
+ transport.sent[0]?.subject; // first message's subject
548
+ ```
549
+
550
+ **Notes:** `readonly` binding but the array is mutated on each `send` — assert on
551
+ `.length` and elements.
552
+
553
+ #### `send(message)`
554
+
555
+ `send(message: Message): Promise<void>`
556
+
557
+ Pushes the message onto `sent`.
558
+
559
+ ```ts
560
+ await new ArrayTransport().send(message);
561
+ ```
562
+
563
+ **Notes:** never throws; delivers nothing. Called for you by `Mailer.send`.
564
+
565
+ ### `LogTransport`
566
+
567
+ A transport that logs each message (to, from, subject) via the framework logger
568
+ instead of delivering it — for local development.
569
+
570
+ #### `new LogTransport()`
571
+
572
+ `new LogTransport()`
573
+
574
+ Creates the transport.
575
+
576
+ ```ts
577
+ setMailer(new LogTransport(), { from: "dev@localhost" });
578
+ ```
579
+
580
+ #### `send(message)`
581
+
582
+ `send(message: Message): Promise<void>`
583
+
584
+ Logs `to`, `from`, and `subject` at info level; sends nothing.
585
+
586
+ ```ts
587
+ await new LogTransport().send(message);
588
+ ```
589
+
590
+ **Notes:** the body is not logged, only the envelope fields.
591
+
592
+ ### Interfaces & types
593
+
594
+ #### `Message`
595
+
596
+ ```ts
597
+ interface Message {
598
+ to: string[];
599
+ from?: string;
600
+ cc?: string[];
601
+ bcc?: string[];
602
+ replyTo?: string;
603
+ subject: string;
604
+ text?: string;
605
+ html?: string;
606
+ headers?: Record<string, string>;
607
+ }
608
+ ```
609
+
610
+ The normalized, ready-to-send message. The builder produces one; a `Transport`
611
+ receives one (already validated, with `from` resolved). You can also build one by
612
+ hand and pass it to `Mailer.send`.
613
+
614
+ ```ts
615
+ const message: Message = {
616
+ to: ["ada@x.com"],
617
+ from: "hi@app.com",
618
+ subject: "Hi",
619
+ text: "hey",
620
+ };
621
+ ```
622
+
623
+ #### `Transport`
624
+
625
+ ```ts
626
+ interface Transport {
627
+ send(message: Message): Promise<void>;
628
+ }
629
+ ```
630
+
631
+ The seam between the mailer and your email provider — one method. Implement it to
632
+ bridge any SDK or API; register it with `setMailer`.
633
+
634
+ ```ts
635
+ const transport: Transport = {
636
+ async send(message) {
637
+ await myProviderSdk.emails.send(message);
638
+ },
639
+ };
640
+ setMailer(transport, { from: "hi@app.com" });
641
+ ```
642
+
643
+ #### `MailerOptions`
644
+
645
+ ```ts
646
+ interface MailerOptions {
647
+ from?: string;
648
+ }
649
+ ```
650
+
651
+ Options for a `Mailer`. Currently just a default `from` applied to messages that
652
+ don't set one.
653
+
654
+ ```ts
655
+ setMailer(transport, { from: "hello@myapp.com" });
656
+ ```
657
+
658
+ #### `FetchTransportOptions`
659
+
660
+ ```ts
661
+ interface FetchTransportOptions {
662
+ url: string;
663
+ headers?: Record<string, string>;
664
+ body?: (message: Message) => unknown;
665
+ }
666
+ ```
667
+
668
+ Configuration for `fetchTransport`. `url` is the provider endpoint; `headers`
669
+ merge over the automatic `Content-Type: application/json`; `body` maps a
670
+ `Message` to the provider's request shape (defaults to the message itself).
671
+
672
+ ```ts
673
+ const opts: FetchTransportOptions = {
674
+ url: "https://api.resend.com/emails",
675
+ headers: { Authorization: `Bearer ${apiKey}` },
676
+ body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
677
+ };
678
+ ```