@shaferllc/keel 0.59.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 (166) 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 +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
@@ -5,49 +5,135 @@
5
5
  *
6
6
  * logger().info("user registered", { userId: user.id });
7
7
  * logger().error("payment failed", { orderId, error });
8
+ *
9
+ * Output goes through a `Sink` — `console` by default, but any function, so logs
10
+ * can go to a file, an HTTP collector, or a buffer in tests. Register extra
11
+ * loggers by name (`setLogger(audit, "audit")`) when a subsystem needs its own
12
+ * level or destination.
8
13
  */
9
- const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
14
+ const LEVELS = {
15
+ trace: 10,
16
+ debug: 20,
17
+ info: 30,
18
+ warn: 40,
19
+ error: 50,
20
+ fatal: 60,
21
+ };
10
22
  const REDACTED = "[redacted]";
11
- /** Return a copy of `obj` with `path` (dot-separated) redacted — clones only along the path. */
12
- function redactPath(obj, keys) {
23
+ function normalizeRedact(redact) {
24
+ if (!redact)
25
+ return undefined;
26
+ const options = Array.isArray(redact) ? { paths: redact } : redact;
27
+ if (!options.paths.length)
28
+ return undefined;
29
+ return { paths: options.paths, censor: options.censor ?? REDACTED, remove: options.remove ?? false };
30
+ }
31
+ /**
32
+ * Return a copy of `obj` with `keys` (a split dot path) redacted. Clones only
33
+ * along the path, so untouched branches keep their identity. A `*` segment
34
+ * matches every key at that level.
35
+ */
36
+ function redactPath(obj, keys, options) {
13
37
  const [head, ...rest] = keys;
14
- if (head === undefined || !(head in obj))
38
+ if (head === undefined)
39
+ return obj;
40
+ const targets = head === "*" ? Object.keys(obj) : head in obj ? [head] : [];
41
+ if (!targets.length)
15
42
  return obj;
16
43
  const clone = { ...obj };
17
- if (rest.length === 0) {
18
- clone[head] = REDACTED;
19
- }
20
- else {
21
- const child = clone[head];
22
- if (child && typeof child === "object") {
23
- clone[head] = redactPath(child, rest);
44
+ for (const key of targets) {
45
+ if (rest.length === 0) {
46
+ if (options.remove)
47
+ delete clone[key];
48
+ else
49
+ clone[key] = options.censor;
50
+ }
51
+ else {
52
+ const child = clone[key];
53
+ if (child && typeof child === "object" && !Array.isArray(child)) {
54
+ clone[key] = redactPath(child, rest, options);
55
+ }
24
56
  }
25
57
  }
26
58
  return clone;
27
59
  }
60
+ /** The default sink: JSON to stdout, or a pretty single line when `pretty`. */
61
+ export function consoleSink(pretty = false) {
62
+ return ({ level, time, msg, fields }) => {
63
+ const write = level === "warn"
64
+ ? console.warn
65
+ : level === "error" || level === "fatal"
66
+ ? console.error
67
+ : console.log;
68
+ if (pretty) {
69
+ const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
70
+ write(`[${time}] ${level.toUpperCase().padEnd(5)} ${msg}${extra}`);
71
+ }
72
+ else {
73
+ write(JSON.stringify({ level, time, msg, ...fields }));
74
+ }
75
+ };
76
+ }
77
+ /** A sink that collects records in memory — for tests. Assert on `.records`. */
78
+ export class MemorySink {
79
+ records = [];
80
+ /** The sink function to hand to `LoggerOptions.sink`. */
81
+ sink = (record) => {
82
+ this.records.push(record);
83
+ };
84
+ /** Records at one level. */
85
+ at(level) {
86
+ return this.records.filter((r) => r.level === level);
87
+ }
88
+ /** The messages logged, in order. */
89
+ messages() {
90
+ return this.records.map((r) => r.msg);
91
+ }
92
+ clear() {
93
+ this.records.length = 0;
94
+ }
95
+ }
28
96
  export class Logger {
29
97
  options;
30
98
  threshold;
99
+ redact;
100
+ sink;
31
101
  constructor(options = {}) {
32
102
  this.options = options;
33
103
  this.threshold = LEVELS[options.level ?? "info"];
104
+ this.redact = normalizeRedact(options.redact);
105
+ this.sink = options.sink ?? consoleSink(options.pretty);
106
+ }
107
+ /**
108
+ * Whether a level would be emitted. Check it before building an expensive
109
+ * context object — the work of assembling it happens whether or not the line
110
+ * is ever written.
111
+ *
112
+ * if (logger().isLevelEnabled("debug")) {
113
+ * logger().debug("state", { snapshot: expensiveSnapshot() });
114
+ * }
115
+ */
116
+ isLevelEnabled(level) {
117
+ return this.options.enabled !== false && LEVELS[level] >= this.threshold;
118
+ }
119
+ /** Run `fn` only if `level` would be emitted — the callback form of the above. */
120
+ ifLevelEnabled(level, fn) {
121
+ if (this.isLevelEnabled(level))
122
+ fn(this);
34
123
  }
35
124
  write(level, message, context) {
36
- if (LEVELS[level] < this.threshold)
125
+ if (!this.isLevelEnabled(level))
37
126
  return;
38
- const time = new Date().toISOString();
39
127
  let fields = { ...this.options.bindings, ...context };
40
- for (const path of this.options.redact ?? []) {
41
- fields = redactPath(fields, path.split("."));
42
- }
43
- if (this.options.pretty) {
44
- const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
45
- const fn = level === "warn" ? console.warn : level === "error" ? console.error : console.log;
46
- fn(`[${time}] ${level.toUpperCase().padEnd(5)} ${message}${extra}`);
47
- }
48
- else {
49
- console.log(JSON.stringify({ level, time, msg: message, ...fields }));
128
+ if (this.redact) {
129
+ for (const path of this.redact.paths) {
130
+ fields = redactPath(fields, path.split("."), this.redact);
131
+ }
50
132
  }
133
+ this.sink({ level, time: new Date().toISOString(), msg: message, fields });
134
+ }
135
+ trace(message, context) {
136
+ this.write("trace", message, context);
51
137
  }
52
138
  debug(message, context) {
53
139
  this.write("debug", message, context);
@@ -61,6 +147,14 @@ export class Logger {
61
147
  error(message, context) {
62
148
  this.write("error", message, context);
63
149
  }
150
+ /** An unrecoverable failure — the loudest level. */
151
+ fatal(message, context) {
152
+ this.write("fatal", message, context);
153
+ }
154
+ /** Log at a level chosen at runtime. */
155
+ log(level, message, context) {
156
+ this.write(level, message, context);
157
+ }
64
158
  /** A child logger with additional bound fields (e.g. a request id). */
65
159
  child(bindings) {
66
160
  return new Logger({
@@ -69,3 +163,27 @@ export class Logger {
69
163
  });
70
164
  }
71
165
  }
166
+ /* -------------------------------- registry -------------------------------- */
167
+ const loggers = new Map();
168
+ /**
169
+ * Register a logger under a name, so a subsystem can have its own level or
170
+ * destination:
171
+ *
172
+ * setLogger(new Logger({ level: "trace", sink: auditSink }), "audit");
173
+ * namedLogger("audit").trace("permission granted", { userId });
174
+ *
175
+ * The default logger is the application's `Logger` singleton — resolve that with
176
+ * the global `logger()` helper, not this.
177
+ */
178
+ export function setLogger(instance, name) {
179
+ loggers.set(name, instance);
180
+ return instance;
181
+ }
182
+ /** A logger registered with `setLogger`. Throws for an unknown name. */
183
+ export function namedLogger(name) {
184
+ const instance = loggers.get(name);
185
+ if (!instance) {
186
+ throw new Error(`No logger named "${name}". Register it with setLogger(logger, name).`);
187
+ }
188
+ return instance;
189
+ }
@@ -13,8 +13,28 @@
13
13
  * .html("<h1>Hi</h1>")
14
14
  * .send();
15
15
  *
16
- * In tests, register an `ArrayTransport` and assert on `transport.sent`.
16
+ * Sending is slow and it fails which is exactly what a queue is for. `sendLater()`
17
+ * puts the message on the queue instead of holding the request open for an SMTP
18
+ * round trip:
19
+ *
20
+ * await mail().to(user.email).subject("Welcome").html(body).sendLater();
21
+ *
22
+ * In tests, `fakeMail()` swaps the transport for one that records and asserts.
17
23
  */
24
+ import { Job } from "./queue.js";
25
+ /** A file travelling with the message. */
26
+ export interface Attachment {
27
+ filename: string;
28
+ /** Raw bytes, or a string (UTF-8 encoded). */
29
+ content: string | Uint8Array;
30
+ /** MIME type. Inferred from the filename's extension when omitted. */
31
+ contentType?: string;
32
+ /**
33
+ * A content id, for embedding in the HTML body: an attachment with `cid: "logo"`
34
+ * is referenced as `<img src="cid:logo">`. Implies an inline disposition.
35
+ */
36
+ cid?: string;
37
+ }
18
38
  /** A normalized, ready-to-send message. */
19
39
  export interface Message {
20
40
  to: string[];
@@ -26,6 +46,7 @@ export interface Message {
26
46
  text?: string;
27
47
  html?: string;
28
48
  headers?: Record<string, string>;
49
+ attachments?: Attachment[];
29
50
  }
30
51
  /** The bridge to your email provider. */
31
52
  export interface Transport {
@@ -34,8 +55,10 @@ export interface Transport {
34
55
  export interface MailerOptions {
35
56
  /** Default `from` address for messages that don't set one. */
36
57
  from?: string;
58
+ /** Default `replyTo` for messages that don't set one. */
59
+ replyTo?: string;
37
60
  }
38
- /** A fluent, immutable-ish builder — chain setters, then `send()`. */
61
+ /** A fluent, immutable-ish builder — chain setters, then `send()` or `sendLater()`. */
39
62
  export declare class PendingMail {
40
63
  private mailer;
41
64
  private message;
@@ -49,23 +72,76 @@ export declare class PendingMail {
49
72
  text(text: string): this;
50
73
  html(html: string): this;
51
74
  header(name: string, value: string): this;
75
+ /** Attach a file. The content type is inferred from the extension if omitted. */
76
+ attach(filename: string, content: string | Uint8Array, contentType?: string): this;
77
+ /**
78
+ * Attach an image and give it a content id, so the HTML body can show it inline:
79
+ *
80
+ * .embed("logo", bytes, "logo.png").html(`<img src="cid:logo">`)
81
+ */
82
+ embed(cid: string, content: string | Uint8Array, filename?: string, contentType?: string): this;
52
83
  /** Seed several fields at once (merges into what's been chained). */
53
84
  fill(partial: Partial<{
54
85
  to: string | string[];
55
86
  cc: string | string[];
56
87
  bcc: string | string[];
57
88
  } & Omit<Message, "to" | "cc" | "bcc">>): this;
89
+ /** The message as composed so far — before the mailer applies its defaults. */
90
+ toMessage(): Message;
58
91
  /** Freeze and hand the message to the mailer. */
59
92
  send(): Promise<Message>;
93
+ /**
94
+ * Queue the message instead of sending it now — the request returns without
95
+ * waiting on the provider, and a failed send retries on the queue rather than
96
+ * failing the user's action.
97
+ *
98
+ * With the default `SyncDriver` this still sends inline; register a real queue
99
+ * driver to actually defer it.
100
+ */
101
+ sendLater(): Promise<void>;
102
+ }
103
+ /**
104
+ * A reusable, testable email as a class — the mail equivalent of a `Job`.
105
+ * Implement `build()` to compose the message; the mailer calls it for you.
106
+ *
107
+ * export class WelcomeEmail extends BaseMail {
108
+ * constructor(private user: User) { super(); }
109
+ *
110
+ * build(message: PendingMail) {
111
+ * message.to(this.user.email).subject("Welcome").html(`<h1>Hi ${this.user.name}</h1>`);
112
+ * }
113
+ * }
114
+ *
115
+ * await send(new WelcomeEmail(user)); // or sendLater(new WelcomeEmail(user))
116
+ */
117
+ export declare abstract class BaseMail {
118
+ abstract build(message: PendingMail): void | Promise<void>;
119
+ }
120
+ /** The job that carries a queued message. Exported so a driver can recognize it. */
121
+ export declare class SendMailJob extends Job {
122
+ readonly message: Message;
123
+ readonly mailerName: string;
124
+ constructor(message: Message, mailerName: string);
125
+ handle(): Promise<void>;
60
126
  }
61
127
  export declare class Mailer {
62
128
  private transport;
63
129
  private options;
64
- constructor(transport: Transport, options?: MailerOptions);
130
+ /** The name this mailer is registered under — carried onto queued jobs. */
131
+ readonly name: string;
132
+ constructor(transport: Transport, options?: MailerOptions,
133
+ /** The name this mailer is registered under — carried onto queued jobs. */
134
+ name?: string);
65
135
  /** Start composing a message. */
66
136
  message(): PendingMail;
137
+ /** Apply this mailer's defaults and check the message is sendable. */
138
+ protected prepare(message: Message): Message;
67
139
  /** Validate, apply defaults, and dispatch through the transport. */
68
140
  send(message: Message): Promise<Message>;
141
+ /** Validate, apply defaults, and put the message on the queue. */
142
+ sendLater(message: Message): Promise<Message>;
143
+ /** The underlying transport, for provider-specific operations. */
144
+ get driver(): Transport;
69
145
  }
70
146
  /** Collects sent messages in memory — the default, and ideal for tests. */
71
147
  export declare class ArrayTransport implements Transport {
@@ -86,9 +162,54 @@ export interface FetchTransportOptions {
86
162
  }
87
163
  /** A generic HTTP transport for provider APIs (Resend, Postmark, …), via `fetch`. */
88
164
  export declare function fetchTransport(options: FetchTransportOptions): Transport;
89
- /** Register the default mailer used by `mail()`. */
90
- export declare function setMailer(transport: Transport, options?: MailerOptions): Mailer;
165
+ /** A message recorded by the fake, and whether it was sent now or queued. */
166
+ export interface RecordedMail {
167
+ message: Message;
168
+ queued: boolean;
169
+ }
170
+ /**
171
+ * A `Mailer` that records instead of delivering, with assertions — what
172
+ * `fakeMail()` installs so tests never talk to a provider.
173
+ */
174
+ export declare class FakeMailer extends Mailer {
175
+ readonly mails: RecordedMail[];
176
+ constructor(name?: string, options?: MailerOptions);
177
+ send(message: Message): Promise<Message>;
178
+ sendLater(message: Message): Promise<Message>;
179
+ /** Everything sent immediately. */
180
+ sent(): Message[];
181
+ /** Everything queued with `sendLater()`. */
182
+ queued(): Message[];
183
+ assertSent(where?: (message: Message) => boolean): void;
184
+ assertNotSent(where?: (message: Message) => boolean): void;
185
+ assertSentCount(expected: number): void;
186
+ assertQueued(where?: (message: Message) => boolean): void;
187
+ assertNotQueued(where?: (message: Message) => boolean): void;
188
+ assertQueuedCount(expected: number): void;
189
+ /** Nothing sent and nothing queued. */
190
+ assertNothingSent(): void;
191
+ private summary;
192
+ }
193
+ /** Register a mailer, optionally under a name (default: `"default"`). */
194
+ export declare function setMailer(transport: Transport, options?: MailerOptions, name?: string): Mailer;
195
+ /** The default mailer, or a named one registered with `setMailer(…, …, name)`. */
196
+ export declare function mailer(name?: string): Mailer;
91
197
  /** The default mailer instance. */
92
198
  export declare function getMailer(): Mailer;
93
- /** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
94
- export declare function mail(): PendingMail;
199
+ /** Start a message on a mailer: `mail().to(…).subject(…).send()`. */
200
+ export declare function mail(name?: string): PendingMail;
201
+ /** Send a class-based mail now. */
202
+ export declare function send(email: BaseMail, name?: string): Promise<Message>;
203
+ /** Queue a class-based mail. */
204
+ export declare function sendLater(email: BaseMail, name?: string): Promise<Message>;
205
+ /**
206
+ * Swap a mailer for a `FakeMailer` so tests never talk to a provider, and assert
207
+ * on what was sent or queued. Undo with `restoreMail()`.
208
+ *
209
+ * const mail = fakeMail();
210
+ * await registerUser();
211
+ * mail.assertQueued((m) => m.subject === "Welcome");
212
+ */
213
+ export declare function fakeMail(name?: string): FakeMailer;
214
+ /** Restore the real mailer after `fakeMail()`. With no name, restores them all. */
215
+ export declare function restoreMail(name?: string): void;