@mandujs/core 0.53.3 → 0.54.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 +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/email/index.ts
CHANGED
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mandujs/core/email
|
|
3
|
-
*
|
|
4
|
-
* Minimal transactional-email primitive: an `EmailSender` interface and two
|
|
5
|
-
* concrete adapters (`memory`, `resend`). Authentication flows (Phase 5.3 —
|
|
6
|
-
* email verification, password reset) consume this interface; they do not
|
|
7
|
-
* care which provider is wired up.
|
|
8
|
-
*
|
|
9
|
-
* Design constraints:
|
|
10
|
-
* - **No external deps.** The resend adapter speaks HTTP via `fetch`.
|
|
11
|
-
* - **Send-only.** No MIME parsing inbound, no attachments (deferred to v2),
|
|
12
|
-
* no templating — callers build their own HTML.
|
|
13
|
-
* - **No queue / retry.** The caller (or a job runner) owns retries. This
|
|
14
|
-
* module is a transport primitive, not a mail pipeline.
|
|
15
|
-
* - **SMTP is stubbed.** See {@link ./smtp.ts} for the planned design.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* import { createResendSender } from "@mandujs/core/email";
|
|
20
|
-
*
|
|
21
|
-
* const mail = createResendSender({ apiKey: process.env.RESEND_API_KEY! });
|
|
22
|
-
* await mail.send({
|
|
23
|
-
* from: "Mandu <no-reply@mandu.dev>",
|
|
24
|
-
* to: "user@example.com",
|
|
25
|
-
* subject: "Verify your email",
|
|
26
|
-
* html: "<p>Click <a href=\"...\">here</a> to verify.</p>",
|
|
27
|
-
* });
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* @example Tests — swap in the memory adapter:
|
|
31
|
-
* ```ts
|
|
32
|
-
* import { createMemoryEmailSender } from "@mandujs/core/email";
|
|
33
|
-
*
|
|
34
|
-
* const mail = createMemoryEmailSender();
|
|
35
|
-
* await handler({ mail }); // your code under test
|
|
36
|
-
* await mail.waitFor(1); // race-free assertion
|
|
37
|
-
* expect(mail.sent[0].subject).toBe("Verify your email");
|
|
38
|
-
* ```
|
|
39
|
-
*
|
|
40
|
-
* @module email
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import { newId } from "../id/index.js";
|
|
44
|
-
|
|
45
|
-
// ─── Public types ───────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
/** A single outbound message. At least one of `html` / `text` is required. */
|
|
48
|
-
export interface EmailMessage {
|
|
49
|
-
/**
|
|
50
|
-
* Sender address. Accepted forms:
|
|
51
|
-
* - `"sender@domain.com"`
|
|
52
|
-
* - `"Display Name <sender@domain.com>"`
|
|
53
|
-
*/
|
|
54
|
-
from: string;
|
|
55
|
-
/** Single address or an array of addresses (≥ 1). */
|
|
56
|
-
to: string | string[];
|
|
57
|
-
/** Non-empty subject line. */
|
|
58
|
-
subject: string;
|
|
59
|
-
/** HTML body. Required if `text` is absent. */
|
|
60
|
-
html?: string;
|
|
61
|
-
/** Plain-text body. Required if `html` is absent. */
|
|
62
|
-
text?: string;
|
|
63
|
-
/** CC recipients (optional). */
|
|
64
|
-
cc?: string | string[];
|
|
65
|
-
/** BCC recipients (optional). */
|
|
66
|
-
bcc?: string | string[];
|
|
67
|
-
/** Reply-To address (optional). Mapped to `reply_to` on provider payloads. */
|
|
68
|
-
replyTo?: string;
|
|
69
|
-
/**
|
|
70
|
-
* Arbitrary provider-specific headers. Keys are lowercased before being
|
|
71
|
-
* forwarded to the provider. Non-ASCII values may be rejected by the
|
|
72
|
-
* provider — we do not validate here.
|
|
73
|
-
*/
|
|
74
|
-
headers?: Record<string, string>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Result of a successful send. */
|
|
78
|
-
export interface EmailSendResult {
|
|
79
|
-
/** Provider's message id. For the memory adapter, a synthetic UUIDv7. */
|
|
80
|
-
id: string;
|
|
81
|
-
/** Unix ms when the send call settled. */
|
|
82
|
-
sentAt: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Abstract transport. */
|
|
86
|
-
export interface EmailSender {
|
|
87
|
-
/**
|
|
88
|
-
* Sends a single message. Throws:
|
|
89
|
-
* - `TypeError` on validation failure (malformed message shape).
|
|
90
|
-
* - `Error` with status/context on transport failure (non-2xx, network).
|
|
91
|
-
*/
|
|
92
|
-
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** In-process adapter for tests + dev. Single-process only — NOT distributed. */
|
|
96
|
-
export interface MemoryEmailSender extends EmailSender {
|
|
97
|
-
/** All messages sent since construction. Read-only, order preserved. */
|
|
98
|
-
readonly sent: ReadonlyArray<EmailMessage & { id: string; sentAt: number }>;
|
|
99
|
-
/** Empty the spool. Useful between test cases. */
|
|
100
|
-
clear(): void;
|
|
101
|
-
/**
|
|
102
|
-
* Resolves once `sent.length >= n`. Rejects after `timeoutMs`. Useful for
|
|
103
|
-
* tests that race with a request handler.
|
|
104
|
-
*
|
|
105
|
-
* @param n Target count. Default 1.
|
|
106
|
-
* @param timeoutMs Abort threshold in ms. Default 1000.
|
|
107
|
-
*/
|
|
108
|
-
waitFor(n?: number, timeoutMs?: number): Promise<void>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Accepts either a bare address (`"a@b.com"`) or display-name form
|
|
115
|
-
* (`"Name <a@b.com>"`). We deliberately do NOT implement RFC 5322 — that
|
|
116
|
-
* regex is famously 6kB and still imperfect. The provider will reject
|
|
117
|
-
* addresses it doesn't like.
|
|
118
|
-
*/
|
|
119
|
-
const BARE_EMAIL_RE = /^[^\s@]+@[^\s@]+$/;
|
|
120
|
-
const DISPLAY_NAME_RE = /^.+\s<[^\s@]+@[^\s@]+>$/;
|
|
121
|
-
|
|
122
|
-
function isValidFrom(from: string): boolean {
|
|
123
|
-
if (typeof from !== "string" || from.length === 0) return false;
|
|
124
|
-
return BARE_EMAIL_RE.test(from) || DISPLAY_NAME_RE.test(from);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function normalizeRecipients(
|
|
128
|
-
value: string | string[] | undefined,
|
|
129
|
-
): string[] | undefined {
|
|
130
|
-
if (value === undefined) return undefined;
|
|
131
|
-
if (typeof value === "string") return value.length > 0 ? [value] : [];
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Validates `message` shape. Throws `TypeError` on the first problem found.
|
|
137
|
-
*
|
|
138
|
-
* Exported for tests and for callers who want to pre-check a message before
|
|
139
|
-
* enqueueing it.
|
|
140
|
-
*
|
|
141
|
-
* @internal
|
|
142
|
-
*/
|
|
143
|
-
export function _validateMessage(message: EmailMessage): void {
|
|
144
|
-
if (!message || typeof message !== "object") {
|
|
145
|
-
throw new TypeError("[@mandujs/core/email] send: message must be an object.");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!isValidFrom(message.from)) {
|
|
149
|
-
throw new TypeError(
|
|
150
|
-
`[@mandujs/core/email] send: 'from' must be an email address or "Name <addr>" form (got ${JSON.stringify(
|
|
151
|
-
message.from,
|
|
152
|
-
)}).`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const to = normalizeRecipients(message.to);
|
|
157
|
-
if (!to || to.length === 0) {
|
|
158
|
-
throw new TypeError(
|
|
159
|
-
"[@mandujs/core/email] send: 'to' must be a non-empty string or non-empty array.",
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
for (const addr of to) {
|
|
163
|
-
if (typeof addr !== "string" || addr.length === 0) {
|
|
164
|
-
throw new TypeError(
|
|
165
|
-
"[@mandujs/core/email] send: 'to' entries must be non-empty strings.",
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (typeof message.subject !== "string" || message.subject.length === 0) {
|
|
171
|
-
throw new TypeError(
|
|
172
|
-
"[@mandujs/core/email] send: 'subject' must be a non-empty string.",
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const hasHtml = typeof message.html === "string" && message.html.length > 0;
|
|
177
|
-
const hasText = typeof message.text === "string" && message.text.length > 0;
|
|
178
|
-
if (!hasHtml && !hasText) {
|
|
179
|
-
throw new TypeError(
|
|
180
|
-
"[@mandujs/core/email] send: message must include at least one of 'html' or 'text'.",
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Shared coercion used by provider adapters: turn a `EmailMessage` into a
|
|
187
|
-
* `{ to, cc, bcc }` bundle of string arrays for the provider payload. Omits
|
|
188
|
-
* undefined-valued fields entirely so the provider doesn't see `null`s.
|
|
189
|
-
*
|
|
190
|
-
* @internal
|
|
191
|
-
*/
|
|
192
|
-
export function _coerceRecipients(message: EmailMessage): {
|
|
193
|
-
to: string[];
|
|
194
|
-
cc?: string[];
|
|
195
|
-
bcc?: string[];
|
|
196
|
-
} {
|
|
197
|
-
const out: { to: string[]; cc?: string[]; bcc?: string[] } = {
|
|
198
|
-
to: normalizeRecipients(message.to) ?? [],
|
|
199
|
-
};
|
|
200
|
-
const cc = normalizeRecipients(message.cc);
|
|
201
|
-
if (cc && cc.length > 0) out.cc = cc;
|
|
202
|
-
const bcc = normalizeRecipients(message.bcc);
|
|
203
|
-
if (bcc && bcc.length > 0) out.bcc = bcc;
|
|
204
|
-
return out;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Lowercases the keys of a headers map. Keeps duplicate-key semantics to
|
|
209
|
-
* whichever one the caller supplied last — same as the JS object itself.
|
|
210
|
-
*
|
|
211
|
-
* @internal
|
|
212
|
-
*/
|
|
213
|
-
export function _lowercaseHeaders(
|
|
214
|
-
headers: Record<string, string> | undefined,
|
|
215
|
-
): Record<string, string> | undefined {
|
|
216
|
-
if (!headers) return undefined;
|
|
217
|
-
const out: Record<string, string> = {};
|
|
218
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
219
|
-
out[k.toLowerCase()] = v;
|
|
220
|
-
}
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ─── Memory adapter ─────────────────────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Creates an in-process email sender. Messages never leave the current
|
|
228
|
-
* process — perfect for unit tests and dev environments.
|
|
229
|
-
*/
|
|
230
|
-
export function createMemoryEmailSender(): MemoryEmailSender {
|
|
231
|
-
const spool: Array<EmailMessage & { id: string; sentAt: number }> = [];
|
|
232
|
-
|
|
233
|
-
async function send(message: EmailMessage): Promise<EmailSendResult> {
|
|
234
|
-
_validateMessage(message);
|
|
235
|
-
const id = newId();
|
|
236
|
-
const sentAt = Date.now();
|
|
237
|
-
// Freeze a shallow snapshot so the spool entry doesn't mutate if the
|
|
238
|
-
// caller reuses the `message` object.
|
|
239
|
-
spool.push({
|
|
240
|
-
...message,
|
|
241
|
-
id,
|
|
242
|
-
sentAt,
|
|
243
|
-
});
|
|
244
|
-
return { id, sentAt };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function clear(): void {
|
|
248
|
-
spool.length = 0;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function waitFor(n: number = 1, timeoutMs: number = 1000): Promise<void> {
|
|
252
|
-
if (n <= 0 || spool.length >= n) return;
|
|
253
|
-
const deadline = Date.now() + timeoutMs;
|
|
254
|
-
// Poll on a 10ms cadence — simpler than wiring a notifier and plenty
|
|
255
|
-
// responsive for test assertions.
|
|
256
|
-
while (spool.length < n) {
|
|
257
|
-
if (Date.now() >= deadline) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`[@mandujs/core/email] waitFor timed out after ${timeoutMs}ms (got ${spool.length}/${n} messages).`,
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
send,
|
|
268
|
-
clear,
|
|
269
|
-
waitFor,
|
|
270
|
-
// Expose the array itself as a readonly view. `ReadonlyArray` is purely
|
|
271
|
-
// a TS-level guarantee; consumers that mutate via `as unknown as` are
|
|
272
|
-
// reaching past the type system and get what they deserve.
|
|
273
|
-
get sent() {
|
|
274
|
-
return spool as ReadonlyArray<EmailMessage & { id: string; sentAt: number }>;
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// ─── Re-exports ─────────────────────────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
export { createResendSender, type ResendOptions } from "./resend.js";
|
|
282
|
-
export { createSmtpSender, type SmtpOptions } from "./smtp.js";
|
|
1
|
+
/**
|
|
2
|
+
* @mandujs/core/email
|
|
3
|
+
*
|
|
4
|
+
* Minimal transactional-email primitive: an `EmailSender` interface and two
|
|
5
|
+
* concrete adapters (`memory`, `resend`). Authentication flows (Phase 5.3 —
|
|
6
|
+
* email verification, password reset) consume this interface; they do not
|
|
7
|
+
* care which provider is wired up.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints:
|
|
10
|
+
* - **No external deps.** The resend adapter speaks HTTP via `fetch`.
|
|
11
|
+
* - **Send-only.** No MIME parsing inbound, no attachments (deferred to v2),
|
|
12
|
+
* no templating — callers build their own HTML.
|
|
13
|
+
* - **No queue / retry.** The caller (or a job runner) owns retries. This
|
|
14
|
+
* module is a transport primitive, not a mail pipeline.
|
|
15
|
+
* - **SMTP is stubbed.** See {@link ./smtp.ts} for the planned design.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createResendSender } from "@mandujs/core/email";
|
|
20
|
+
*
|
|
21
|
+
* const mail = createResendSender({ apiKey: process.env.RESEND_API_KEY! });
|
|
22
|
+
* await mail.send({
|
|
23
|
+
* from: "Mandu <no-reply@mandu.dev>",
|
|
24
|
+
* to: "user@example.com",
|
|
25
|
+
* subject: "Verify your email",
|
|
26
|
+
* html: "<p>Click <a href=\"...\">here</a> to verify.</p>",
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example Tests — swap in the memory adapter:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createMemoryEmailSender } from "@mandujs/core/email";
|
|
33
|
+
*
|
|
34
|
+
* const mail = createMemoryEmailSender();
|
|
35
|
+
* await handler({ mail }); // your code under test
|
|
36
|
+
* await mail.waitFor(1); // race-free assertion
|
|
37
|
+
* expect(mail.sent[0].subject).toBe("Verify your email");
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @module email
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { newId } from "../id/index.js";
|
|
44
|
+
|
|
45
|
+
// ─── Public types ───────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** A single outbound message. At least one of `html` / `text` is required. */
|
|
48
|
+
export interface EmailMessage {
|
|
49
|
+
/**
|
|
50
|
+
* Sender address. Accepted forms:
|
|
51
|
+
* - `"sender@domain.com"`
|
|
52
|
+
* - `"Display Name <sender@domain.com>"`
|
|
53
|
+
*/
|
|
54
|
+
from: string;
|
|
55
|
+
/** Single address or an array of addresses (≥ 1). */
|
|
56
|
+
to: string | string[];
|
|
57
|
+
/** Non-empty subject line. */
|
|
58
|
+
subject: string;
|
|
59
|
+
/** HTML body. Required if `text` is absent. */
|
|
60
|
+
html?: string;
|
|
61
|
+
/** Plain-text body. Required if `html` is absent. */
|
|
62
|
+
text?: string;
|
|
63
|
+
/** CC recipients (optional). */
|
|
64
|
+
cc?: string | string[];
|
|
65
|
+
/** BCC recipients (optional). */
|
|
66
|
+
bcc?: string | string[];
|
|
67
|
+
/** Reply-To address (optional). Mapped to `reply_to` on provider payloads. */
|
|
68
|
+
replyTo?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Arbitrary provider-specific headers. Keys are lowercased before being
|
|
71
|
+
* forwarded to the provider. Non-ASCII values may be rejected by the
|
|
72
|
+
* provider — we do not validate here.
|
|
73
|
+
*/
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Result of a successful send. */
|
|
78
|
+
export interface EmailSendResult {
|
|
79
|
+
/** Provider's message id. For the memory adapter, a synthetic UUIDv7. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** Unix ms when the send call settled. */
|
|
82
|
+
sentAt: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Abstract transport. */
|
|
86
|
+
export interface EmailSender {
|
|
87
|
+
/**
|
|
88
|
+
* Sends a single message. Throws:
|
|
89
|
+
* - `TypeError` on validation failure (malformed message shape).
|
|
90
|
+
* - `Error` with status/context on transport failure (non-2xx, network).
|
|
91
|
+
*/
|
|
92
|
+
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** In-process adapter for tests + dev. Single-process only — NOT distributed. */
|
|
96
|
+
export interface MemoryEmailSender extends EmailSender {
|
|
97
|
+
/** All messages sent since construction. Read-only, order preserved. */
|
|
98
|
+
readonly sent: ReadonlyArray<EmailMessage & { id: string; sentAt: number }>;
|
|
99
|
+
/** Empty the spool. Useful between test cases. */
|
|
100
|
+
clear(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Resolves once `sent.length >= n`. Rejects after `timeoutMs`. Useful for
|
|
103
|
+
* tests that race with a request handler.
|
|
104
|
+
*
|
|
105
|
+
* @param n Target count. Default 1.
|
|
106
|
+
* @param timeoutMs Abort threshold in ms. Default 1000.
|
|
107
|
+
*/
|
|
108
|
+
waitFor(n?: number, timeoutMs?: number): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Accepts either a bare address (`"a@b.com"`) or display-name form
|
|
115
|
+
* (`"Name <a@b.com>"`). We deliberately do NOT implement RFC 5322 — that
|
|
116
|
+
* regex is famously 6kB and still imperfect. The provider will reject
|
|
117
|
+
* addresses it doesn't like.
|
|
118
|
+
*/
|
|
119
|
+
const BARE_EMAIL_RE = /^[^\s@]+@[^\s@]+$/;
|
|
120
|
+
const DISPLAY_NAME_RE = /^.+\s<[^\s@]+@[^\s@]+>$/;
|
|
121
|
+
|
|
122
|
+
function isValidFrom(from: string): boolean {
|
|
123
|
+
if (typeof from !== "string" || from.length === 0) return false;
|
|
124
|
+
return BARE_EMAIL_RE.test(from) || DISPLAY_NAME_RE.test(from);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeRecipients(
|
|
128
|
+
value: string | string[] | undefined,
|
|
129
|
+
): string[] | undefined {
|
|
130
|
+
if (value === undefined) return undefined;
|
|
131
|
+
if (typeof value === "string") return value.length > 0 ? [value] : [];
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates `message` shape. Throws `TypeError` on the first problem found.
|
|
137
|
+
*
|
|
138
|
+
* Exported for tests and for callers who want to pre-check a message before
|
|
139
|
+
* enqueueing it.
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
export function _validateMessage(message: EmailMessage): void {
|
|
144
|
+
if (!message || typeof message !== "object") {
|
|
145
|
+
throw new TypeError("[@mandujs/core/email] send: message must be an object.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isValidFrom(message.from)) {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
`[@mandujs/core/email] send: 'from' must be an email address or "Name <addr>" form (got ${JSON.stringify(
|
|
151
|
+
message.from,
|
|
152
|
+
)}).`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const to = normalizeRecipients(message.to);
|
|
157
|
+
if (!to || to.length === 0) {
|
|
158
|
+
throw new TypeError(
|
|
159
|
+
"[@mandujs/core/email] send: 'to' must be a non-empty string or non-empty array.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
for (const addr of to) {
|
|
163
|
+
if (typeof addr !== "string" || addr.length === 0) {
|
|
164
|
+
throw new TypeError(
|
|
165
|
+
"[@mandujs/core/email] send: 'to' entries must be non-empty strings.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof message.subject !== "string" || message.subject.length === 0) {
|
|
171
|
+
throw new TypeError(
|
|
172
|
+
"[@mandujs/core/email] send: 'subject' must be a non-empty string.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const hasHtml = typeof message.html === "string" && message.html.length > 0;
|
|
177
|
+
const hasText = typeof message.text === "string" && message.text.length > 0;
|
|
178
|
+
if (!hasHtml && !hasText) {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
"[@mandujs/core/email] send: message must include at least one of 'html' or 'text'.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Shared coercion used by provider adapters: turn a `EmailMessage` into a
|
|
187
|
+
* `{ to, cc, bcc }` bundle of string arrays for the provider payload. Omits
|
|
188
|
+
* undefined-valued fields entirely so the provider doesn't see `null`s.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
export function _coerceRecipients(message: EmailMessage): {
|
|
193
|
+
to: string[];
|
|
194
|
+
cc?: string[];
|
|
195
|
+
bcc?: string[];
|
|
196
|
+
} {
|
|
197
|
+
const out: { to: string[]; cc?: string[]; bcc?: string[] } = {
|
|
198
|
+
to: normalizeRecipients(message.to) ?? [],
|
|
199
|
+
};
|
|
200
|
+
const cc = normalizeRecipients(message.cc);
|
|
201
|
+
if (cc && cc.length > 0) out.cc = cc;
|
|
202
|
+
const bcc = normalizeRecipients(message.bcc);
|
|
203
|
+
if (bcc && bcc.length > 0) out.bcc = bcc;
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Lowercases the keys of a headers map. Keeps duplicate-key semantics to
|
|
209
|
+
* whichever one the caller supplied last — same as the JS object itself.
|
|
210
|
+
*
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
export function _lowercaseHeaders(
|
|
214
|
+
headers: Record<string, string> | undefined,
|
|
215
|
+
): Record<string, string> | undefined {
|
|
216
|
+
if (!headers) return undefined;
|
|
217
|
+
const out: Record<string, string> = {};
|
|
218
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
219
|
+
out[k.toLowerCase()] = v;
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Memory adapter ─────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Creates an in-process email sender. Messages never leave the current
|
|
228
|
+
* process — perfect for unit tests and dev environments.
|
|
229
|
+
*/
|
|
230
|
+
export function createMemoryEmailSender(): MemoryEmailSender {
|
|
231
|
+
const spool: Array<EmailMessage & { id: string; sentAt: number }> = [];
|
|
232
|
+
|
|
233
|
+
async function send(message: EmailMessage): Promise<EmailSendResult> {
|
|
234
|
+
_validateMessage(message);
|
|
235
|
+
const id = newId();
|
|
236
|
+
const sentAt = Date.now();
|
|
237
|
+
// Freeze a shallow snapshot so the spool entry doesn't mutate if the
|
|
238
|
+
// caller reuses the `message` object.
|
|
239
|
+
spool.push({
|
|
240
|
+
...message,
|
|
241
|
+
id,
|
|
242
|
+
sentAt,
|
|
243
|
+
});
|
|
244
|
+
return { id, sentAt };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function clear(): void {
|
|
248
|
+
spool.length = 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function waitFor(n: number = 1, timeoutMs: number = 1000): Promise<void> {
|
|
252
|
+
if (n <= 0 || spool.length >= n) return;
|
|
253
|
+
const deadline = Date.now() + timeoutMs;
|
|
254
|
+
// Poll on a 10ms cadence — simpler than wiring a notifier and plenty
|
|
255
|
+
// responsive for test assertions.
|
|
256
|
+
while (spool.length < n) {
|
|
257
|
+
if (Date.now() >= deadline) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`[@mandujs/core/email] waitFor timed out after ${timeoutMs}ms (got ${spool.length}/${n} messages).`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
send,
|
|
268
|
+
clear,
|
|
269
|
+
waitFor,
|
|
270
|
+
// Expose the array itself as a readonly view. `ReadonlyArray` is purely
|
|
271
|
+
// a TS-level guarantee; consumers that mutate via `as unknown as` are
|
|
272
|
+
// reaching past the type system and get what they deserve.
|
|
273
|
+
get sent() {
|
|
274
|
+
return spool as ReadonlyArray<EmailMessage & { id: string; sentAt: number }>;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Re-exports ─────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
export { createResendSender, type ResendOptions } from "./resend.js";
|
|
282
|
+
export { createSmtpSender, type SmtpOptions } from "./smtp.js";
|