@lunora/mail 1.0.0-alpha.8 → 1.0.0-alpha.81

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 (42) hide show
  1. package/dist/inbound/index.d.mts +322 -78
  2. package/dist/inbound/index.d.ts +322 -78
  3. package/dist/inbound/index.mjs +1 -2
  4. package/dist/index.d.mts +122 -94
  5. package/dist/index.d.ts +122 -94
  6. package/dist/index.mjs +1 -7
  7. package/dist/packem_shared/address-oKy_d0aG.mjs +3 -0
  8. package/dist/packem_shared/authenticatesFrom-BOX-Paur.mjs +1 -0
  9. package/dist/packem_shared/capture-transport.d-BD67fxbB.d.mts +130 -0
  10. package/dist/packem_shared/capture-transport.d-BD67fxbB.d.ts +130 -0
  11. package/dist/packem_shared/consumeQueuedSend-CRfALnCU.mjs +1 -0
  12. package/dist/packem_shared/createCaptureSink-DI9tmsLn.mjs +1 -0
  13. package/dist/packem_shared/createCaptureTransport-CKc6NpZR.mjs +1 -0
  14. package/dist/packem_shared/createCloudflareTransport-bWoNSnK_.mjs +1 -0
  15. package/dist/packem_shared/createInboundEmailHandler-Bh10YPyJ.mjs +1 -0
  16. package/dist/packem_shared/createMailer-aRrzRLwc.mjs +1 -0
  17. package/dist/packem_shared/createResendTransport-BMjRlHmy.mjs +1 -0
  18. package/dist/packem_shared/provider-transport-BnA39QbI.mjs +1 -0
  19. package/dist/packem_shared/renderEmail-BOUnr6i3.mjs +1 -0
  20. package/dist/packem_shared/shard-CRhAKK93.mjs +1 -0
  21. package/dist/packem_shared/shard.d-DVADjmEJ.d.mts +29 -0
  22. package/dist/packem_shared/shard.d-DVADjmEJ.d.ts +29 -0
  23. package/dist/testing.d.mts +9 -9
  24. package/dist/testing.d.ts +9 -9
  25. package/dist/testing.mjs +1 -63
  26. package/package.json +5 -5
  27. package/dist/packem_shared/address-vSUAVU2T.mjs +0 -64
  28. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.mts +0 -117
  29. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.ts +0 -117
  30. package/dist/packem_shared/consumeQueuedSend-B9hTDOZ6.mjs +0 -77
  31. package/dist/packem_shared/createCaptureSink-Di-5AJUs.mjs +0 -65
  32. package/dist/packem_shared/createCaptureTransport-Crz_8822.mjs +0 -11
  33. package/dist/packem_shared/createCloudflareTransport-DXh1nUNi.mjs +0 -28
  34. package/dist/packem_shared/createInboundEmailHandler-BW_s0-Vc.mjs +0 -84
  35. package/dist/packem_shared/createMailer-B4Z_Cgiu.mjs +0 -81
  36. package/dist/packem_shared/createResendTransport-CMQWjZoi.mjs +0 -16
  37. package/dist/packem_shared/parseInboundEmail-6lafQBT1.mjs +0 -72
  38. package/dist/packem_shared/provider-transport-C0xHi3oy.mjs +0 -48
  39. package/dist/packem_shared/renderEmail-hyS1bpVP.mjs +0 -8
  40. package/dist/packem_shared/shard-CJ-TvmfT.mjs +0 -13
  41. package/dist/packem_shared/shard.d-CL2Lmliv.d.mts +0 -39
  42. package/dist/packem_shared/shard.d-CL2Lmliv.d.ts +0 -39
@@ -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. */
@@ -15,34 +15,79 @@ interface InboundAttachment {
15
15
  /** Declared MIME type (e.g. `image/png`). */
16
16
  mimeType: string;
17
17
  }
18
+ /** One reported `method=result [ptype.property=value]` clause of an `Authentication-Results` header. */
19
+ interface InboundAuthResult {
20
+ /**
21
+ * The identifier the result is about, lowercased — DKIM's signing domain
22
+ * (`header.d=`), SPF's envelope `MAIL FROM` domain (`smtp.mailfrom=`, local
23
+ * part dropped) or DMARC's `header.from=`. `null` when the clause reported
24
+ * none, in which case it cannot be aligned and vouches for nothing.
25
+ */
26
+ domain: string | null;
27
+ /** The result token, lowercased (`"pass"`, `"fail"`, `"none"`, …). */
28
+ result: string;
29
+ }
18
30
  /**
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
- */
31
+ * Sender-authentication verdicts pulled from the `Authentication-Results` header
32
+ * the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
33
+ *
34
+ * SECURITY: Cloudflare Email Routing authenticates only the *recipient* domain,
35
+ * **not** the sender. The envelope `from` and message content are trivially
36
+ * spoofable, so a downstream handler MUST NOT make trust/authorization decisions
37
+ * on `email.from` alone — gate on these verdicts (or your own policy) instead.
38
+ * Verdicts are best-effort: when the receiving MX did not stamp an
39
+ * `Authentication-Results` header, every list is empty ("unknown").
40
+ *
41
+ * Each method holds a LIST, because RFC 8601 lets one header report the same
42
+ * method more than once and real mail does. An ESP-relayed message carries two
43
+ * DKIM signatures — the relay's and the author domain's — and the MX stamps a
44
+ * clause per signature in whatever order it verified them. Keeping only the
45
+ * first threw the aligned one away whenever it was not the one that happened to
46
+ * come first, and the message was rejected as unauthenticated. A consumer
47
+ * therefore asks "does ANY reported clause pass and align?", not "did the first
48
+ * one?".
49
+ *
50
+ * SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
51
+ * authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
52
+ * (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
53
+ * dkim=pass header.d=evil.example` is routine for a message whose `From` says
54
+ * `ceo@victim.example`. Each verdict therefore carries the identifier it is
55
+ * about ({@link InboundAuthResult.domain}): an SPF or DKIM pass only vouches for
56
+ * `from` when that domain equals the `From` address's domain (RFC 7489 strict
57
+ * alignment). Only a DMARC pass already checked alignment for you. A pass with
58
+ * no identifier reported cannot be aligned and must be treated as
59
+ * unauthenticated.
60
+ *
61
+ * SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
62
+ * header in document order. The receiving MX prepends its own genuine header per
63
+ * RFC 8601, so the topmost occurrence is the trustworthy one; any lower
64
+ * occurrences (which an untrusted sender can inject into the raw message) are
65
+ * ignored. As defense-in-depth a consumer may additionally verify the topmost
66
+ * header's `authserv-id` matches its receiving MX (e.g. Cloudflare) — that needs
67
+ * config this runtime-agnostic parser does not carry, so it is left to the host.
68
+ */
29
69
  interface InboundAuthentication {
30
- /** DKIM verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
31
- dkim: string | null;
32
- /** DMARC verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
33
- dmarc: string | null;
34
- /** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
35
- spf: string | null;
70
+ /** Every DKIM clause the header reported, in header order; empty when the method was not reported. */
71
+ dkim: InboundAuthResult[];
72
+ /** Every DMARC clause the header reported, in header order; empty when the method was not reported. */
73
+ dmarc: InboundAuthResult[];
74
+ /** Every SPF clause the header reported, in header order; empty when the method was not reported. */
75
+ spf: InboundAuthResult[];
36
76
  }
37
77
  /** Normalised, transport-agnostic view of a received message. */
38
78
  interface InboundEmail {
39
79
  /** Decoded attachments (empty array when none). */
40
80
  attachments: InboundAttachment[];
41
81
  /**
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
- */
82
+ * Sender-authentication verdicts (DKIM/SPF/DMARC) and the domain each is
83
+ * about, parsed from the receiving MX's **first/topmost**
84
+ * `Authentication-Results` header. SECURITY: see
85
+ * {@link InboundAuthentication} — `from` is spoofable, and an SPF/DKIM pass
86
+ * vouches for it only when that clause's `domain` equals the `From` domain (a
87
+ * DMARC pass checked that already). Reading the raw `headers["authentication-results"]`
88
+ * map instead exposes last-wins (a lower, potentially attacker-injected)
89
+ * value — trust `authentication`, not the raw map.
90
+ */
46
91
  authentication: InboundAuthentication;
47
92
  /** Sender mailbox (`from`), CR/LF-checked. Empty string when the message omitted it. SECURITY: spoofable — do not trust for authorization. */
48
93
  from: string;
@@ -64,17 +109,48 @@ interface InboundEmail {
64
109
  to: string[];
65
110
  }
66
111
  /**
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
- */
112
+ * Parse a raw RFC 822 message into a normalised {@link InboundEmail}. Accepts the
113
+ * shapes a Cloudflare Email Worker can hand off — `ReadableStream`, `ArrayBuffer`,
114
+ * `Uint8Array`, or a decoded string.
115
+ */
71
116
  declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
72
117
  /**
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
- */
118
+ * THE inbound sender-authentication gate: does the receiving MX vouch for this
119
+ * message's `From` domain? Pass it as the `verify` hook of
120
+ * `createInboundEmailHandler` (or call it from your own) — it is one exported
121
+ * helper precisely so the insecure variants cannot be hand-rolled again.
122
+ *
123
+ * True when ANY reported DMARC, SPF or DKIM clause both **passes** and names a
124
+ * `domain` equal to the `From` address's domain. False otherwise — including for
125
+ * an empty verdict list (the MX stamped no `Authentication-Results` header at
126
+ * all, which is "unknown", not "fine") and for a `From` with no single mailbox
127
+ * to align against.
128
+ *
129
+ * SECURITY — the two halves are each load-bearing.
130
+ *
131
+ * **Alignment.** A bare `pass` proves nothing about `From`. SPF authenticates the
132
+ * envelope `MAIL FROM` domain and DKIM the signing `d=`, both attacker-chosen, so
133
+ * `spf=pass`+`dkim=pass` for `evil.example` is routine on a message whose `From`
134
+ * says `ceo@victim.example`. Only a clause whose own `domain` equals the `From`
135
+ * domain vouches for it. Alignment is STRICT (RFC 7489): there is no
136
+ * public-suffix list here, so `mail.example.com` does not vouch for
137
+ * `example.com`. A pass reporting no domain (`null`) cannot be aligned and is
138
+ * rejected. A DMARC pass already checked alignment at the MX.
139
+ *
140
+ * **Every clause, not the first.** One header legitimately reports a method more
141
+ * than once (an ESP-relayed message is DKIM-signed by both the relay and the
142
+ * author domain), and the aligned clause is not reliably the first, so reading
143
+ * only the first bounced fully authenticated mail. "Any clause passes AND aligns"
144
+ * stays strictly narrower than a bare pass: a clause vouching for some other
145
+ * domain contributes nothing.
146
+ */
147
+ declare const authenticatesFrom: (email: InboundEmail) => boolean;
148
+ /**
149
+ * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
150
+ * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
151
+ * members the handler touches are modelled, so the host can pass the real
152
+ * runtime object without `@lunora/mail` importing `cloudflare:email`.
153
+ */
78
154
  interface ForwardableEmailMessageLike {
79
155
  /** Forward this message to a verified destination address. */
80
156
  forward: (rcptTo: string, headers?: Headers) => Promise<unknown>;
@@ -90,7 +166,14 @@ interface ForwardableEmailMessageLike {
90
166
  raw: string;
91
167
  to: string;
92
168
  }) => Promise<unknown>;
93
- /** Reject the message with a permanent SMTP error (Cloudflare bounces/retries). */
169
+ /**
170
+ * Reject the message with a PERMANENT SMTP error — Cloudflare returns it to
171
+ * the connecting client with this reason, and it is never redelivered.
172
+ * Documented at https://developers.cloudflare.com/email-routing/email-workers/runtime-api/
173
+ * ("Reject this email message by returning a permanent SMTP error back to
174
+ * the connecting client, including the given reason") and mirrored in
175
+ * workerd's own `types/defines/email.d.ts`.
176
+ */
94
177
  setReject: (reason: string) => void;
95
178
  /** Envelope `To`. */
96
179
  readonly to: string;
@@ -107,43 +190,73 @@ interface InboundDispatchContext<TEnv = Record<string, unknown>> {
107
190
  /** Routes a parsed message into a Lunora function (or anywhere). */
108
191
  type InboundDispatch<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<void>;
109
192
  /**
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
- */
193
+ * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
194
+ * the parsed message. Return `false` (or throw) to reject the message before it
195
+ * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
196
+ * `email.authentication`, an allow-list, etc.
197
+ *
198
+ * `true` and `undefined` are the ONLY answers that proceed — `undefined` so a
199
+ * `(): void` hook that rejects by throwing type-checks. Anything else is read as a
200
+ * rejection: this is the gate whose failure mode would otherwise grant.
201
+ */
116
202
  type InboundVerify<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<boolean | void> | boolean | void;
203
+ /**
204
+ * Opt-in durable sink for a failed `dispatch`. Hand the parsed message to
205
+ * something that owns the retry — a queue producer, a Durable Object, an alarm
206
+ * — and the handler ACCEPTS the SMTP session instead of bouncing, because the
207
+ * message is now owned rather than lost. Returning normally means "I have it";
208
+ * throwing means the hand-off itself failed and the message bounces with the
209
+ * generic reason (see {@link createInboundEmailHandler}).
210
+ *
211
+ * `error` is the dispatch failure, for classification/logging by the sink.
212
+ *
213
+ * NOTE: binary attachment `content` is an `ArrayBuffer`/`Uint8Array`, which
214
+ * survives structured clone (Cloudflare Queues, DO storage) but is corrupted by
215
+ * `JSON.stringify` — encode it yourself if the sink is JSON-bodied.
216
+ */
217
+ type InboundRetain<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>, error: unknown) => Promise<void> | void;
117
218
  /** Options for {@link createInboundEmailHandler}. */
118
219
  interface InboundEmailHandlerOptions<TEnv = Record<string, unknown>> {
119
220
  /** Routes the parsed message onward (e.g. {@link dispatchToLunoraFunction}). */
120
221
  dispatch: InboundDispatch<TEnv>;
121
222
  /**
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
- */
223
+ * Called when `parse`, `verify`, or `dispatch` fails — but with two DIFFERENT
224
+ * contracts, because only the first two decide the message's fate here:
225
+ *
226
+ * - `parse` / `verify` — this hook DECIDES the outcome. The default
227
+ * ({@link rejectOnError}) rejects via `message.setReject`; supplying your own
228
+ * replaces that, so the message is accepted unless you reject it yourself.
229
+ * - `dispatch` — this hook is OBSERVABILITY ONLY. It is called for the side
230
+ * effect (log, alert, forward) and the outcome is then decided by `retain`
231
+ * (accept) or a generic reject, regardless of what it does; the built-in
232
+ * default is deliberately NOT applied there. A `setReject` from inside it
233
+ * still takes effect, which would bounce a message `retain` went on to
234
+ * accept — almost certainly not what you want. A throw from the hook itself
235
+ * is logged and swallowed so it cannot mask the original dispatch error.
236
+ *
237
+ * SECURITY: a reject reason is delivered to the (attacker-controlled) sender
238
+ * as a bounce, so the default reason is a fixed, generic string and the real
239
+ * error is logged server-side. Never pass internal error text to `setReject`.
240
+ */
129
241
  onError?: (error: unknown, context: InboundDispatchContext<TEnv>) => Promise<void> | void;
130
242
  /** Parses raw bytes into an {@link InboundEmail} (e.g. `parseInboundEmail`). */
131
243
  parse: (raw: RawInboundEmail) => Promise<InboundEmail>;
132
244
  /**
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
- */
245
+ * Opt-in: take durable ownership of a message whose `dispatch` failed, so a
246
+ * transient fault (a shard 502, a briefly-absent admin token) is retried
247
+ * instead of bounced. Omit it and a dispatch failure bounces, as it always
248
+ * has. See {@link InboundRetain}.
249
+ */
250
+ retain?: InboundRetain<TEnv>;
251
+ /**
252
+ * Opt-in sender-authentication gate run before `dispatch`. SECURITY: inbound
253
+ * `from` is spoofable and dispatch is privileged — supply this (gating on
254
+ * `email.authentication`) when an inbound function makes any trust decision.
255
+ */
137
256
  verify?: InboundVerify<TEnv>;
138
257
  }
139
258
  /** The `email(message, env, ctx)` callback the factory returns. */
140
259
  type InboundEmailHandler<TEnv = Record<string, unknown>> = (message: ForwardableEmailMessageLike, env: TEnv, context: unknown) => Promise<void>;
141
- /**
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
- */
147
260
  declare const createInboundEmailHandler: <TEnv = Record<string, unknown>>(options: InboundEmailHandlerOptions<TEnv>) => InboundEmailHandler<TEnv>;
148
261
  /** The `RpcEnvelope` shape the runtime's `/_lunora/rpc` path consumes. */
149
262
  interface RpcEnvelope {
@@ -154,23 +267,23 @@ interface RpcEnvelope {
154
267
  /** Options for {@link dispatchToLunoraFunction}. */
155
268
  interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
156
269
  /**
157
- * Admin bearer authorizing the shard RPC. Defaults to reading
158
- * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
159
- */
270
+ * Admin bearer authorizing the shard RPC. Defaults to reading
271
+ * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
272
+ */
160
273
  adminToken?: string;
161
274
  /** `functionPath` of the target mutation/action (e.g. `"inbound:onEmail"`). */
162
275
  functionPath: string;
163
276
  /**
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
- */
277
+ * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Pass the
278
+ * same value as the worker's `jurisdiction` so inbound mail routes to the
279
+ * jurisdiction-pinned shard. Omit for the un-pinned global namespace.
280
+ */
168
281
  jurisdiction?: DurableObjectJurisdiction;
169
282
  /**
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
- */
283
+ * Map the parsed message into the function's args. Defaults to passing the
284
+ * whole {@link InboundEmail} with binary attachment `content` base64-encoded
285
+ * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
286
+ */
174
287
  resolveArgs?: (email: InboundEmail, context: InboundDispatchContext<TEnv>) => unknown;
175
288
  /** The `SHARD` Durable Object namespace. */
176
289
  shard: ShardNamespaceLike;
@@ -178,16 +291,147 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
178
291
  shardKey?: string;
179
292
  }
180
293
  /**
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
- */
294
+ * Build a {@link InboundDispatch} that posts an {@link RpcEnvelope} to the root
295
+ * shard stub — the same admin-RPC-over-shard path the dev capture sink uses
296
+ * (`from-env.ts`) — routing the parsed message into a named Lunora
297
+ * mutation/action. Throws on a non-2xx RPC or a missing admin token — both
298
+ * transient in principle, so the handler reports it through a custom `onError`
299
+ * and then hands the message to `retain` if one is configured, bouncing only
300
+ * when there is nowhere durable to put it (see {@link createInboundEmailHandler}).
301
+ *
302
+ * SECURITY: the RPC is marked a trusted system dispatch, so `functionPath` may
303
+ * (and should) name an `internalMutation`/`internalAction` — a public `mutation`
304
+ * target is callable by any browser client with a forged message. The input is
305
+ * fully attacker-controlled and spoofable; verify the sender (`verify` hook /
306
+ * `email.authentication`) before making any trust decision in the target.
307
+ */
192
308
  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 };
309
+ export {
310
+ /**
311
+ * `@lunora/mail/inbound` — inbound Email Routing support.
312
+ *
313
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
314
+ * mutation/action:
315
+ *
316
+ * ```ts
317
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
318
+ *
319
+ * export const email = createInboundEmailHandler({
320
+ * parse: parseInboundEmail,
321
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
322
+ * });
323
+ * ```
324
+ */
325
+ type DispatchToLunoraFunctionOptions,
326
+ /**
327
+ * `@lunora/mail/inbound` — inbound Email Routing support.
328
+ *
329
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
330
+ * mutation/action:
331
+ *
332
+ * ```ts
333
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
334
+ *
335
+ * export const email = createInboundEmailHandler({
336
+ * parse: parseInboundEmail,
337
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
338
+ * });
339
+ * ```
340
+ */
341
+ type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthResult, type InboundAuthentication,
342
+ /**
343
+ * `@lunora/mail/inbound` — inbound Email Routing support.
344
+ *
345
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
346
+ * mutation/action:
347
+ *
348
+ * ```ts
349
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
350
+ *
351
+ * export const email = createInboundEmailHandler({
352
+ * parse: parseInboundEmail,
353
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
354
+ * });
355
+ * ```
356
+ */
357
+ type InboundDispatch,
358
+ /**
359
+ * `@lunora/mail/inbound` — inbound Email Routing support.
360
+ *
361
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
362
+ * mutation/action:
363
+ *
364
+ * ```ts
365
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
366
+ *
367
+ * export const email = createInboundEmailHandler({
368
+ * parse: parseInboundEmail,
369
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
370
+ * });
371
+ * ```
372
+ */
373
+ type InboundDispatchContext, type InboundEmail,
374
+ /**
375
+ * `@lunora/mail/inbound` — inbound Email Routing support.
376
+ *
377
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
378
+ * mutation/action:
379
+ *
380
+ * ```ts
381
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
382
+ *
383
+ * export const email = createInboundEmailHandler({
384
+ * parse: parseInboundEmail,
385
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
386
+ * });
387
+ * ```
388
+ */
389
+ type InboundEmailHandler,
390
+ /**
391
+ * `@lunora/mail/inbound` — inbound Email Routing support.
392
+ *
393
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
394
+ * mutation/action:
395
+ *
396
+ * ```ts
397
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
398
+ *
399
+ * export const email = createInboundEmailHandler({
400
+ * parse: parseInboundEmail,
401
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
402
+ * });
403
+ * ```
404
+ */
405
+ type InboundEmailHandlerOptions,
406
+ /**
407
+ * `@lunora/mail/inbound` — inbound Email Routing support.
408
+ *
409
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
410
+ * mutation/action:
411
+ *
412
+ * ```ts
413
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
414
+ *
415
+ * export const email = createInboundEmailHandler({
416
+ * parse: parseInboundEmail,
417
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
418
+ * });
419
+ * ```
420
+ */
421
+ type InboundVerify, type RawInboundEmail,
422
+ /**
423
+ * `@lunora/mail/inbound` — inbound Email Routing support.
424
+ *
425
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
426
+ * mutation/action:
427
+ *
428
+ * ```ts
429
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
430
+ *
431
+ * export const email = createInboundEmailHandler({
432
+ * parse: parseInboundEmail,
433
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
434
+ * });
435
+ * ```
436
+ */
437
+ type RpcEnvelope, type ShardNamespaceLike, authenticatesFrom, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };