@lunora/mail 1.0.0-alpha.6 → 1.0.0-alpha.60

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 (43) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/inbound/index.d.mts +316 -71
  3. package/dist/inbound/index.d.ts +316 -71
  4. package/dist/inbound/index.mjs +1 -2
  5. package/dist/index.d.mts +101 -94
  6. package/dist/index.d.ts +101 -94
  7. package/dist/index.mjs +1 -7
  8. package/dist/packem_shared/address-soPj2Z0j.mjs +3 -0
  9. package/dist/packem_shared/capture-transport.d-8CDAv51Z.d.mts +126 -0
  10. package/dist/packem_shared/capture-transport.d-8CDAv51Z.d.ts +126 -0
  11. package/dist/packem_shared/consumeQueuedSend-CRfALnCU.mjs +1 -0
  12. package/dist/packem_shared/createCaptureSink-4T2LJOYC.mjs +1 -0
  13. package/dist/packem_shared/createCaptureTransport-CKc6NpZR.mjs +1 -0
  14. package/dist/packem_shared/createCloudflareTransport-DpfCxn16.mjs +1 -0
  15. package/dist/packem_shared/createInboundEmailHandler-C6V47mz5.mjs +1 -0
  16. package/dist/packem_shared/createMailer-BJs2hnV-.mjs +1 -0
  17. package/dist/packem_shared/createResendTransport-DDkOitck.mjs +1 -0
  18. package/dist/packem_shared/parseInboundEmail-BG0NU_e9.mjs +1 -0
  19. package/dist/packem_shared/provider-transport-CR4w-R0A.mjs +1 -0
  20. package/dist/packem_shared/renderEmail-BOUnr6i3.mjs +1 -0
  21. package/dist/packem_shared/shard-C0aRRoVy.mjs +1 -0
  22. package/dist/packem_shared/shard.d-DVADjmEJ.d.mts +29 -0
  23. package/dist/packem_shared/shard.d-DVADjmEJ.d.ts +29 -0
  24. package/dist/testing.d.mts +9 -9
  25. package/dist/testing.d.ts +9 -9
  26. package/dist/testing.mjs +1 -63
  27. package/package.json +5 -5
  28. package/dist/packem_shared/address-vSUAVU2T.mjs +0 -64
  29. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.mts +0 -117
  30. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.ts +0 -117
  31. package/dist/packem_shared/consumeQueuedSend-B9hTDOZ6.mjs +0 -77
  32. package/dist/packem_shared/createCaptureSink-Di-5AJUs.mjs +0 -65
  33. package/dist/packem_shared/createCaptureTransport-Crz_8822.mjs +0 -11
  34. package/dist/packem_shared/createCloudflareTransport-DXh1nUNi.mjs +0 -28
  35. package/dist/packem_shared/createInboundEmailHandler-BW_s0-Vc.mjs +0 -84
  36. package/dist/packem_shared/createMailer-B4Z_Cgiu.mjs +0 -81
  37. package/dist/packem_shared/createResendTransport-CMQWjZoi.mjs +0 -16
  38. package/dist/packem_shared/parseInboundEmail-6lafQBT1.mjs +0 -72
  39. package/dist/packem_shared/provider-transport-C0xHi3oy.mjs +0 -48
  40. package/dist/packem_shared/renderEmail-hyS1bpVP.mjs +0 -8
  41. package/dist/packem_shared/shard-CJ-TvmfT.mjs +0 -13
  42. package/dist/packem_shared/shard.d-CL2Lmliv.d.mts +0 -39
  43. package/dist/packem_shared/shard.d-CL2Lmliv.d.ts +0 -39
package/LICENSE.md CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
@@ -1,5 +1,5 @@
1
- import { D as DurableObjectJurisdiction, S as ShardNamespaceLike } from "../packem_shared/shard.d-CL2Lmliv.mjs";
2
- export type { a as ShardStubLike } from "../packem_shared/shard.d-CL2Lmliv.mjs";
1
+ import { D as DurableObjectJurisdiction, S as ShardNamespaceLike } from "../packem_shared/shard.d-DVADjmEJ.mjs";
2
+ export type { a as ShardStubLike } from "../packem_shared/shard.d-DVADjmEJ.mjs";
3
3
  /** A raw RFC 822 message as accepted by the parser. */
4
4
  type RawInboundEmail = ArrayBuffer | ReadableStream<Uint8Array> | string | Uint8Array;
5
5
  /** One parsed attachment. `content` is preserved as the parser decoded it. */
@@ -16,33 +16,62 @@ interface InboundAttachment {
16
16
  mimeType: string;
17
17
  }
18
18
  /**
19
- * Sender-authentication verdicts pulled from the `Authentication-Results` header
20
- * the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
21
- *
22
- * SECURITY: Cloudflare Email Routing authenticates only the *recipient* domain,
23
- * **not** the sender. The envelope `from` and message content are trivially
24
- * spoofable, so a downstream handler MUST NOT make trust/authorization decisions
25
- * on `email.from` alone — gate on these verdicts (or your own policy) instead.
26
- * Verdicts are best-effort: when the receiving MX did not stamp an
27
- * `Authentication-Results` header, every field is `null` ("unknown").
28
- */
19
+ * Sender-authentication verdicts pulled from the `Authentication-Results` header
20
+ * the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
21
+ *
22
+ * SECURITY: Cloudflare Email Routing authenticates only the *recipient* domain,
23
+ * **not** the sender. The envelope `from` and message content are trivially
24
+ * spoofable, so a downstream handler MUST NOT make trust/authorization decisions
25
+ * on `email.from` alone — gate on these verdicts (or your own policy) instead.
26
+ * Verdicts are best-effort: when the receiving MX did not stamp an
27
+ * `Authentication-Results` header, every field is `null` ("unknown").
28
+ *
29
+ * SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
30
+ * authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
31
+ * (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
32
+ * dkim=pass header.d=evil.example` is routine for a message whose `From` says
33
+ * `ceo@victim.example`. Each verdict therefore carries the identifier it is
34
+ * about (`*Domain`): an SPF or DKIM pass only vouches for `from` when that
35
+ * domain equals the `From` address's domain (RFC 7489 strict alignment). Only a
36
+ * DMARC pass already checked alignment for you. A pass with no identifier
37
+ * reported cannot be aligned and must be treated as unauthenticated.
38
+ *
39
+ * SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
40
+ * header in document order. The receiving MX prepends its own genuine header per
41
+ * RFC 8601, so the topmost occurrence is the trustworthy one; any lower
42
+ * occurrences (which an untrusted sender can inject into the raw message) are
43
+ * ignored. As defense-in-depth a consumer may additionally verify the topmost
44
+ * header's `authserv-id` matches its receiving MX (e.g. Cloudflare) — that needs
45
+ * config this runtime-agnostic parser does not carry, so it is left to the host.
46
+ */
29
47
  interface InboundAuthentication {
30
48
  /** DKIM verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
31
49
  dkim: string | null;
50
+ /** Signing domain the DKIM verdict is about (`header.d=`, lowercased), or `null` when not reported. */
51
+ dkimDomain: string | null;
32
52
  /** DMARC verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
33
53
  dmarc: string | null;
54
+ /** `From` domain the DMARC verdict evaluated (`header.from=`, lowercased), or `null` when not reported. */
55
+ dmarcDomain: string | null;
34
56
  /** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
35
57
  spf: string | null;
58
+ /** Envelope `MAIL FROM` domain the SPF verdict is about (`smtp.mailfrom=`, local part dropped, lowercased), or `null` when not reported. */
59
+ spfDomain: string | null;
36
60
  }
37
61
  /** Normalised, transport-agnostic view of a received message. */
38
62
  interface InboundEmail {
39
63
  /** Decoded attachments (empty array when none). */
40
64
  attachments: InboundAttachment[];
41
65
  /**
42
- * Sender-authentication verdicts (DKIM/SPF/DMARC) parsed from the receiving
43
- * MX's `Authentication-Results` header. SECURITY: see {@link InboundAuthentication}
44
- * `from` is spoofable; gate trust on these verdicts, not on `from`.
45
- */
66
+ * Sender-authentication verdicts (DKIM/SPF/DMARC) and the domain each is
67
+ * about, parsed from the receiving MX's **first/topmost**
68
+ * `Authentication-Results` header. SECURITY: see
69
+ * {@link InboundAuthentication} — `from` is spoofable, and an SPF/DKIM pass
70
+ * vouches for it only when its `*Domain` equals the `From` domain (a DMARC
71
+ * pass checked that already). Reading the raw `headers["authentication-results"]`
72
+ * map instead exposes last-wins (a lower, potentially attacker-injected)
73
+ * value — trust `authentication`, not the raw map.
74
+ */
46
75
  authentication: InboundAuthentication;
47
76
  /** Sender mailbox (`from`), CR/LF-checked. Empty string when the message omitted it. SECURITY: spoofable — do not trust for authorization. */
48
77
  from: string;
@@ -64,17 +93,17 @@ interface InboundEmail {
64
93
  to: string[];
65
94
  }
66
95
  /**
67
- * Parse a raw RFC 822 message into a normalised {@link InboundEmail}. Accepts the
68
- * shapes a Cloudflare Email Worker can hand off — `ReadableStream`, `ArrayBuffer`,
69
- * `Uint8Array`, or a decoded string.
70
- */
96
+ * Parse a raw RFC 822 message into a normalised {@link InboundEmail}. Accepts the
97
+ * shapes a Cloudflare Email Worker can hand off — `ReadableStream`, `ArrayBuffer`,
98
+ * `Uint8Array`, or a decoded string.
99
+ */
71
100
  declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
72
101
  /**
73
- * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
74
- * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
75
- * members the handler touches are modelled, so the host can pass the real
76
- * runtime object without `@lunora/mail` importing `cloudflare:email`.
77
- */
102
+ * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
103
+ * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
104
+ * members the handler touches are modelled, so the host can pass the real
105
+ * runtime object without `@lunora/mail` importing `cloudflare:email`.
106
+ */
78
107
  interface ForwardableEmailMessageLike {
79
108
  /** Forward this message to a verified destination address. */
80
109
  forward: (rcptTo: string, headers?: Headers) => Promise<unknown>;
@@ -90,7 +119,14 @@ interface ForwardableEmailMessageLike {
90
119
  raw: string;
91
120
  to: string;
92
121
  }) => Promise<unknown>;
93
- /** Reject the message with a permanent SMTP error (Cloudflare bounces/retries). */
122
+ /**
123
+ * Reject the message with a PERMANENT SMTP error — Cloudflare returns it to
124
+ * the connecting client with this reason, and it is never redelivered.
125
+ * Documented at https://developers.cloudflare.com/email-routing/email-workers/runtime-api/
126
+ * ("Reject this email message by returning a permanent SMTP error back to
127
+ * the connecting client, including the given reason") and mirrored in
128
+ * workerd's own `types/defines/email.d.ts`.
129
+ */
94
130
  setReject: (reason: string) => void;
95
131
  /** Envelope `To`. */
96
132
  readonly to: string;
@@ -107,43 +143,122 @@ interface InboundDispatchContext<TEnv = Record<string, unknown>> {
107
143
  /** Routes a parsed message into a Lunora function (or anywhere). */
108
144
  type InboundDispatch<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<void>;
109
145
  /**
110
- * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
111
- * the parsed message. Return `false` (or throw) to reject the message before it
112
- * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
113
- * `email.authentication`, an allow-list, etc. Returning `true`/`undefined`
114
- * proceeds.
115
- */
146
+ * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
147
+ * the parsed message. Return `false` (or throw) to reject the message before it
148
+ * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
149
+ * `email.authentication`, an allow-list, etc. Returning `true`/`undefined`
150
+ * proceeds.
151
+ */
116
152
  type InboundVerify<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<boolean | void> | boolean | void;
153
+ /**
154
+ * Opt-in durable sink for a failed `dispatch`. Hand the parsed message to
155
+ * something that owns the retry — a queue producer, a Durable Object, an alarm
156
+ * — and the handler ACCEPTS the SMTP session instead of bouncing, because the
157
+ * message is now owned rather than lost. Returning normally means "I have it";
158
+ * throwing means the hand-off itself failed and the message bounces with the
159
+ * generic reason (see {@link createInboundEmailHandler}).
160
+ *
161
+ * `error` is the dispatch failure, for classification/logging by the sink.
162
+ *
163
+ * NOTE: binary attachment `content` is an `ArrayBuffer`/`Uint8Array`, which
164
+ * survives structured clone (Cloudflare Queues, DO storage) but is corrupted by
165
+ * `JSON.stringify` — encode it yourself if the sink is JSON-bodied.
166
+ */
167
+ type InboundRetain<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>, error: unknown) => Promise<void> | void;
117
168
  /** Options for {@link createInboundEmailHandler}. */
118
169
  interface InboundEmailHandlerOptions<TEnv = Record<string, unknown>> {
119
170
  /** Routes the parsed message onward (e.g. {@link dispatchToLunoraFunction}). */
120
171
  dispatch: InboundDispatch<TEnv>;
121
172
  /**
122
- * Called when `parse`/`verify`/`dispatch` throws. The default rejects the
123
- * message via `message.setReject` so Cloudflare bounces/retries rather than
124
- * silently dropping it. SECURITY: the reject reason is delivered to the
125
- * (attacker-controlled) sender as a bounce, so the default reason is a fixed,
126
- * generic string and the real error is logged server-side. Override to log,
127
- * forward, or swallow but never pass internal error text to `setReject`.
128
- */
173
+ * Called when `parse`, `verify`, or `dispatch` fails but with two DIFFERENT
174
+ * contracts, because only the first two decide the message's fate here:
175
+ *
176
+ * - `parse` / `verify` this hook DECIDES the outcome. The default
177
+ * ({@link rejectOnError}) rejects via `message.setReject`; supplying your own
178
+ * replaces that, so the message is accepted unless you reject it yourself.
179
+ * - `dispatch` — this hook is OBSERVABILITY ONLY. It is called for the side
180
+ * effect (log, alert, forward) and the outcome is then decided by `retain`
181
+ * (accept) or a generic reject, regardless of what it does; the built-in
182
+ * default is deliberately NOT applied there. A `setReject` from inside it
183
+ * still takes effect, which would bounce a message `retain` went on to
184
+ * accept — almost certainly not what you want. A throw from the hook itself
185
+ * is logged and swallowed so it cannot mask the original dispatch error.
186
+ *
187
+ * SECURITY: a reject reason is delivered to the (attacker-controlled) sender
188
+ * as a bounce, so the default reason is a fixed, generic string and the real
189
+ * error is logged server-side. Never pass internal error text to `setReject`.
190
+ */
129
191
  onError?: (error: unknown, context: InboundDispatchContext<TEnv>) => Promise<void> | void;
130
192
  /** Parses raw bytes into an {@link InboundEmail} (e.g. `parseInboundEmail`). */
131
193
  parse: (raw: RawInboundEmail) => Promise<InboundEmail>;
132
194
  /**
133
- * Opt-in sender-authentication gate run before `dispatch`. SECURITY: inbound
134
- * `from` is spoofable and dispatch is privileged supply this (gating on
135
- * `email.authentication`) when an inbound function makes any trust decision.
136
- */
195
+ * Opt-in: take durable ownership of a message whose `dispatch` failed, so a
196
+ * transient fault (a shard 502, a briefly-absent admin token) is retried
197
+ * instead of bounced. Omit it and a dispatch failure bounces, as it always
198
+ * has. See {@link InboundRetain}.
199
+ */
200
+ retain?: InboundRetain<TEnv>;
201
+ /**
202
+ * Opt-in sender-authentication gate run before `dispatch`. SECURITY: inbound
203
+ * `from` is spoofable and dispatch is privileged — supply this (gating on
204
+ * `email.authentication`) when an inbound function makes any trust decision.
205
+ */
137
206
  verify?: InboundVerify<TEnv>;
138
207
  }
139
208
  /** The `email(message, env, ctx)` callback the factory returns. */
140
209
  type InboundEmailHandler<TEnv = Record<string, unknown>> = (message: ForwardableEmailMessageLike, env: TEnv, context: unknown) => Promise<void>;
141
210
  /**
142
- * Build the `email(message, env, ctx)` handler. It (a) reads `message.raw`,
143
- * (b) parses it via `parse`, (c) runs the optional `verify` gate, then
144
- * (d) calls `dispatch(parsed, { message, env, ctx })`. Any throw (or a falsy
145
- * `verify`) routes through `onError` (default: a generic `message.setReject`).
146
- */
211
+ * Build the `email(message, env, ctx)` handler. It (a) reads `message.raw`,
212
+ * (b) parses it via `parse`, (c) runs the optional `verify` gate, then
213
+ * (d) calls `dispatch(parsed, { message, env, ctx })`.
214
+ *
215
+ * The two failure classes are routed differently:
216
+ *
217
+ * - `parse` / `verify` (including a falsy `verify`) → `onError` (default: a
218
+ * generic `message.setReject`). A malformed or unauthenticated message fails
219
+ * the same way on every redelivery, so bouncing it is the honest answer.
220
+ * - `dispatch` (or its transport) → a custom `onError` is called for
221
+ * observability, then the message is handed to `retain` if one is configured
222
+ * (SMTP ACCEPTs — the retry is now owned elsewhere) and otherwise bounced with
223
+ * the same generic reason. A `retain` that throws bounces too.
224
+ *
225
+ * WHY THE RETRY IS ABSORBED IN-WORKER RATHER THAN SIGNALLED OVER SMTP — there is
226
+ * no transient-reject API and no inbound redelivery to appeal to:
227
+ *
228
+ * - `setReject` is documented as a PERMANENT SMTP error
229
+ * (https://developers.cloudflare.com/email-routing/email-workers/runtime-api/),
230
+ * with no "try later" variant.
231
+ * - Cloudflare does not document what an uncaught throw from `email()` does. The
232
+ * full Email Routing and Email Service docs corpora
233
+ * (`developers.cloudflare.com/email-routing/llms-full.txt`,
234
+ * `.../email-service/llms-full.txt`) say nothing about an unhandled exception,
235
+ * and describe NO redelivery mechanism for inbound Email Workers at all. The
236
+ * documented lifecycle
237
+ * (https://developers.cloudflare.com/email-service/concepts/email-lifecycle/)
238
+ * lists exactly three worker outcomes — `forward()`, `reply()`, `setReject()` —
239
+ * with no branch for "the worker threw". (Its 4xx-retry prose is about OUTBOUND
240
+ * delivery to the destination MTA, not about invoking the worker.)
241
+ * - The behaviour reported in practice is a PERMANENT in-session rejection:
242
+ * `521 5.3.0 Upstream error`, i.e. the same permanence as `setReject` but with
243
+ * an opaque reason instead of ours. See
244
+ * https://community.cloudflare.com/t/is-it-possible-to-return-a-transient-failure-from-an-email-worker/599938
245
+ * ("If the email function raises an exception, a permanent failure is returned
246
+ * to the client after the DATA command") — a question Cloudflare never answered
247
+ * — and https://community.cloudflare.com/t/email-worker-upstream-error/457228.
248
+ * Email Routing also states it forwards upstream SMTP errors back to the sender
249
+ * in-session rather than generating a bounce later
250
+ * (https://developers.cloudflare.com/email-service/reference/postmaster/#smtp-errors).
251
+ *
252
+ * So every SMTP-visible outcome is permanent, and the only way not to lose a
253
+ * legitimate message to a two-second shard 502 is to accept it and take durable
254
+ * ownership: that is `retain`. It stays opt-in — with no `retain`, a dispatch
255
+ * failure bounces exactly as before.
256
+ *
257
+ * A dispatch that KNOWS one of its own failures is permanent should call
258
+ * `context.message.setReject(...)` and return normally rather than throw, so it
259
+ * bounces without being handed to `retain` (see `@lunora/agent`'s inbound
260
+ * handler for both cases).
261
+ */
147
262
  declare const createInboundEmailHandler: <TEnv = Record<string, unknown>>(options: InboundEmailHandlerOptions<TEnv>) => InboundEmailHandler<TEnv>;
148
263
  /** The `RpcEnvelope` shape the runtime's `/_lunora/rpc` path consumes. */
149
264
  interface RpcEnvelope {
@@ -154,23 +269,23 @@ interface RpcEnvelope {
154
269
  /** Options for {@link dispatchToLunoraFunction}. */
155
270
  interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
156
271
  /**
157
- * Admin bearer authorizing the shard RPC. Defaults to reading
158
- * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
159
- */
272
+ * Admin bearer authorizing the shard RPC. Defaults to reading
273
+ * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
274
+ */
160
275
  adminToken?: string;
161
276
  /** `functionPath` of the target mutation/action (e.g. `"inbound:onEmail"`). */
162
277
  functionPath: string;
163
278
  /**
164
- * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Pass the
165
- * same value as the worker's `jurisdiction` so inbound mail routes to the
166
- * jurisdiction-pinned shard. Omit for the un-pinned global namespace.
167
- */
279
+ * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Pass the
280
+ * same value as the worker's `jurisdiction` so inbound mail routes to the
281
+ * jurisdiction-pinned shard. Omit for the un-pinned global namespace.
282
+ */
168
283
  jurisdiction?: DurableObjectJurisdiction;
169
284
  /**
170
- * Map the parsed message into the function's args. Defaults to passing the
171
- * whole {@link InboundEmail} with binary attachment `content` base64-encoded
172
- * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
173
- */
285
+ * Map the parsed message into the function's args. Defaults to passing the
286
+ * whole {@link InboundEmail} with binary attachment `content` base64-encoded
287
+ * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
288
+ */
174
289
  resolveArgs?: (email: InboundEmail, context: InboundDispatchContext<TEnv>) => unknown;
175
290
  /** The `SHARD` Durable Object namespace. */
176
291
  shard: ShardNamespaceLike;
@@ -178,16 +293,146 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
178
293
  shardKey?: string;
179
294
  }
180
295
  /**
181
- * Build a {@link InboundDispatch} that posts an {@link RpcEnvelope} to the root
182
- * shard stub — the same admin-RPC-over-shard path the dev capture sink uses
183
- * (`from-env.ts`) — routing the parsed message into a named Lunora
184
- * mutation/action. Throws on a non-2xx RPC or a missing admin token so the
185
- * handler's `onError` (default `setReject`) bounces the message.
186
- *
187
- * SECURITY: the RPC carries the admin bearer, so the target function runs with
188
- * RLS bypassed over fully attacker-controlled, spoofable input — see the module
189
- * docstring. Verify the sender (`verify` hook / `email.authentication`) before
190
- * making any trust decision in the target function.
191
- */
296
+ * Build a {@link InboundDispatch} that posts an {@link RpcEnvelope} to the root
297
+ * shard stub — the same admin-RPC-over-shard path the dev capture sink uses
298
+ * (`from-env.ts`) — routing the parsed message into a named Lunora
299
+ * mutation/action. Throws on a non-2xx RPC or a missing admin token both
300
+ * transient in principle, so the handler reports it through a custom `onError`
301
+ * and then hands the message to `retain` if one is configured, bouncing only
302
+ * when there is nowhere durable to put it (see {@link createInboundEmailHandler}).
303
+ *
304
+ * SECURITY: the RPC carries the admin bearer, so the target function runs with
305
+ * RLS bypassed over fully attacker-controlled, spoofable input — see the module
306
+ * docstring. Verify the sender (`verify` hook / `email.authentication`) before
307
+ * making any trust decision in the target function.
308
+ */
192
309
  declare const dispatchToLunoraFunction: <TEnv extends Record<string, unknown> = Record<string, unknown>>(options: DispatchToLunoraFunctionOptions<TEnv>) => InboundDispatch<TEnv>;
193
- export { type DispatchToLunoraFunctionOptions, type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication, type InboundDispatch, type InboundDispatchContext, type InboundEmail, type InboundEmailHandler, type InboundEmailHandlerOptions, type InboundVerify, type RawInboundEmail, type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
310
+ export {
311
+ /**
312
+ * `@lunora/mail/inbound` — inbound Email Routing support.
313
+ *
314
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
315
+ * mutation/action:
316
+ *
317
+ * ```ts
318
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
319
+ *
320
+ * export const email = createInboundEmailHandler({
321
+ * parse: parseInboundEmail,
322
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
323
+ * });
324
+ * ```
325
+ */
326
+ type DispatchToLunoraFunctionOptions,
327
+ /**
328
+ * `@lunora/mail/inbound` — inbound Email Routing support.
329
+ *
330
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
331
+ * mutation/action:
332
+ *
333
+ * ```ts
334
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
335
+ *
336
+ * export const email = createInboundEmailHandler({
337
+ * parse: parseInboundEmail,
338
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
339
+ * });
340
+ * ```
341
+ */
342
+ type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication,
343
+ /**
344
+ * `@lunora/mail/inbound` — inbound Email Routing support.
345
+ *
346
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
347
+ * mutation/action:
348
+ *
349
+ * ```ts
350
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
351
+ *
352
+ * export const email = createInboundEmailHandler({
353
+ * parse: parseInboundEmail,
354
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
355
+ * });
356
+ * ```
357
+ */
358
+ type InboundDispatch,
359
+ /**
360
+ * `@lunora/mail/inbound` — inbound Email Routing support.
361
+ *
362
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
363
+ * mutation/action:
364
+ *
365
+ * ```ts
366
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
367
+ *
368
+ * export const email = createInboundEmailHandler({
369
+ * parse: parseInboundEmail,
370
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
371
+ * });
372
+ * ```
373
+ */
374
+ type InboundDispatchContext, type InboundEmail,
375
+ /**
376
+ * `@lunora/mail/inbound` — inbound Email Routing support.
377
+ *
378
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
379
+ * mutation/action:
380
+ *
381
+ * ```ts
382
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
383
+ *
384
+ * export const email = createInboundEmailHandler({
385
+ * parse: parseInboundEmail,
386
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
387
+ * });
388
+ * ```
389
+ */
390
+ type InboundEmailHandler,
391
+ /**
392
+ * `@lunora/mail/inbound` — inbound Email Routing support.
393
+ *
394
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
395
+ * mutation/action:
396
+ *
397
+ * ```ts
398
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
399
+ *
400
+ * export const email = createInboundEmailHandler({
401
+ * parse: parseInboundEmail,
402
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
403
+ * });
404
+ * ```
405
+ */
406
+ type InboundEmailHandlerOptions,
407
+ /**
408
+ * `@lunora/mail/inbound` — inbound Email Routing support.
409
+ *
410
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
411
+ * mutation/action:
412
+ *
413
+ * ```ts
414
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
415
+ *
416
+ * export const email = createInboundEmailHandler({
417
+ * parse: parseInboundEmail,
418
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
419
+ * });
420
+ * ```
421
+ */
422
+ type InboundVerify, type RawInboundEmail,
423
+ /**
424
+ * `@lunora/mail/inbound` — inbound Email Routing support.
425
+ *
426
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
427
+ * mutation/action:
428
+ *
429
+ * ```ts
430
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
431
+ *
432
+ * export const email = createInboundEmailHandler({
433
+ * parse: parseInboundEmail,
434
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
435
+ * });
436
+ * ```
437
+ */
438
+ type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };