@lunora/mail 1.0.0-alpha.1 → 1.0.0-alpha.100

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 (41) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/inbound/index.d.mts +325 -93
  4. package/dist/inbound/index.d.ts +325 -93
  5. package/dist/inbound/index.mjs +1 -2
  6. package/dist/index.d.mts +132 -75
  7. package/dist/index.d.ts +132 -75
  8. package/dist/index.mjs +1 -7
  9. package/dist/packem_shared/address-oKy_d0aG.mjs +3 -0
  10. package/dist/packem_shared/authenticatesFrom-BOX-Paur.mjs +1 -0
  11. package/dist/packem_shared/capture-transport.d-BD67fxbB.d.mts +130 -0
  12. package/dist/packem_shared/capture-transport.d-BD67fxbB.d.ts +130 -0
  13. package/dist/packem_shared/consumeQueuedSend-CRfALnCU.mjs +1 -0
  14. package/dist/packem_shared/createCaptureSink-WtmvYpT-.mjs +1 -0
  15. package/dist/packem_shared/createCaptureTransport-DZhMUmaI.mjs +1 -0
  16. package/dist/packem_shared/createCloudflareTransport-bWoNSnK_.mjs +1 -0
  17. package/dist/packem_shared/createInboundEmailHandler-QnZHQZpF.mjs +1 -0
  18. package/dist/packem_shared/createMailer-TVxz0_5S.mjs +1 -0
  19. package/dist/packem_shared/createResendTransport-BMjRlHmy.mjs +1 -0
  20. package/dist/packem_shared/provider-transport-BnA39QbI.mjs +1 -0
  21. package/dist/packem_shared/renderEmail-BOUnr6i3.mjs +1 -0
  22. package/dist/packem_shared/shard-C5t-b7FX.mjs +1 -0
  23. package/dist/packem_shared/shard.d-DVADjmEJ.d.mts +29 -0
  24. package/dist/packem_shared/shard.d-DVADjmEJ.d.ts +29 -0
  25. package/dist/testing.d.mts +9 -9
  26. package/dist/testing.d.ts +9 -9
  27. package/dist/testing.mjs +1 -57
  28. package/package.json +6 -5
  29. package/dist/packem_shared/address-fkXxLKza.mjs +0 -62
  30. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.mts +0 -117
  31. package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.ts +0 -117
  32. package/dist/packem_shared/createCaptureSink-CNhqLMZ1.mjs +0 -61
  33. package/dist/packem_shared/createCaptureTransport-Crz_8822.mjs +0 -11
  34. package/dist/packem_shared/createCloudflareTransport-yHOVEsZv.mjs +0 -26
  35. package/dist/packem_shared/createInboundEmailHandler-Cd8dyzB7.mjs +0 -80
  36. package/dist/packem_shared/createMailer-CMVVTp0F.mjs +0 -77
  37. package/dist/packem_shared/createResendTransport-oNIorpzv.mjs +0 -16
  38. package/dist/packem_shared/parseInboundEmail-Bw9u_1oc.mjs +0 -72
  39. package/dist/packem_shared/provider-transport-C5CVbjRF.mjs +0 -47
  40. package/dist/packem_shared/renderEmail-hyS1bpVP.mjs +0 -8
  41. package/dist/packem_shared/toQueuedPayload-BEKOdaxU.mjs +0 -75
@@ -1,3 +1,5 @@
1
+ import { D as DurableObjectJurisdiction, S as ShardNamespaceLike } from "../packem_shared/shard.d-DVADjmEJ.js";
2
+ export type { a as ShardStubLike } from "../packem_shared/shard.d-DVADjmEJ.js";
1
3
  /** A raw RFC 822 message as accepted by the parser. */
2
4
  type RawInboundEmail = ArrayBuffer | ReadableStream<Uint8Array> | string | Uint8Array;
3
5
  /** One parsed attachment. `content` is preserved as the parser decoded it. */
@@ -13,34 +15,79 @@ interface InboundAttachment {
13
15
  /** Declared MIME type (e.g. `image/png`). */
14
16
  mimeType: string;
15
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
+ }
16
30
  /**
17
- * Sender-authentication verdicts pulled from the `Authentication-Results` header
18
- * the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
19
- *
20
- * SECURITY: Cloudflare Email Routing authenticates only the *recipient* domain,
21
- * **not** the sender. The envelope `from` and message content are trivially
22
- * spoofable, so a downstream handler MUST NOT make trust/authorization decisions
23
- * on `email.from` alone — gate on these verdicts (or your own policy) instead.
24
- * Verdicts are best-effort: when the receiving MX did not stamp an
25
- * `Authentication-Results` header, every field is `null` ("unknown").
26
- */
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
+ */
27
69
  interface InboundAuthentication {
28
- /** DKIM verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
29
- dkim: string | null;
30
- /** DMARC verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
31
- dmarc: string | null;
32
- /** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
33
- 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[];
34
76
  }
35
77
  /** Normalised, transport-agnostic view of a received message. */
36
78
  interface InboundEmail {
37
79
  /** Decoded attachments (empty array when none). */
38
80
  attachments: InboundAttachment[];
39
81
  /**
40
- * Sender-authentication verdicts (DKIM/SPF/DMARC) parsed from the receiving
41
- * MX's `Authentication-Results` header. SECURITY: see {@link InboundAuthentication}
42
- * — `from` is spoofable; gate trust on these verdicts, not on `from`.
43
- */
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
+ */
44
91
  authentication: InboundAuthentication;
45
92
  /** Sender mailbox (`from`), CR/LF-checked. Empty string when the message omitted it. SECURITY: spoofable — do not trust for authorization. */
46
93
  from: string;
@@ -62,39 +109,48 @@ interface InboundEmail {
62
109
  to: string[];
63
110
  }
64
111
  /**
65
- * Parse a raw RFC 822 message into a normalised {@link InboundEmail}. Accepts the
66
- * shapes a Cloudflare Email Worker can hand off — `ReadableStream`, `ArrayBuffer`,
67
- * `Uint8Array`, or a decoded string.
68
- */
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
+ */
69
116
  declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
70
117
  /**
71
- * Structural projections of the `SHARD` Durable Object namespace + one shard
72
- * stub, shared by the inbound dispatcher. Mirrors the shapes the outbound dev
73
- * capture sink uses (`packages/mail/src/from-env.ts`) so inbound dispatch routes
74
- * a parsed message into a Lunora function over the exact same admin-RPC-over-shard
75
- * path — without importing any Cloudflare types into `@lunora/mail`.
76
- */
77
- /** Structural projection of one shard stub — only `fetch` returning something with `.json()`. */
78
- interface ShardStubLike {
79
- fetch: (input: string, init?: {
80
- body?: string;
81
- headers?: Record<string, string>;
82
- method?: string;
83
- }) => Promise<{
84
- json: () => Promise<unknown>;
85
- }>;
86
- }
87
- /** Structural projection of the `SHARD` Durable Object namespace. */
88
- interface ShardNamespaceLike {
89
- get: (id: unknown) => ShardStubLike;
90
- idFromName: (name: string) => unknown;
91
- }
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;
92
148
  /**
93
- * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
94
- * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
95
- * members the handler touches are modelled, so the host can pass the real
96
- * runtime object without `@lunora/mail` importing `cloudflare:email`.
97
- */
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
+ */
98
154
  interface ForwardableEmailMessageLike {
99
155
  /** Forward this message to a verified destination address. */
100
156
  forward: (rcptTo: string, headers?: Headers) => Promise<unknown>;
@@ -110,7 +166,14 @@ interface ForwardableEmailMessageLike {
110
166
  raw: string;
111
167
  to: string;
112
168
  }) => Promise<unknown>;
113
- /** 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
+ */
114
177
  setReject: (reason: string) => void;
115
178
  /** Envelope `To`. */
116
179
  readonly to: string;
@@ -127,43 +190,73 @@ interface InboundDispatchContext<TEnv = Record<string, unknown>> {
127
190
  /** Routes a parsed message into a Lunora function (or anywhere). */
128
191
  type InboundDispatch<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<void>;
129
192
  /**
130
- * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
131
- * the parsed message. Return `false` (or throw) to reject the message before it
132
- * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
133
- * `email.authentication`, an allow-list, etc. Returning `true`/`undefined`
134
- * proceeds.
135
- */
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
+ */
136
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;
137
218
  /** Options for {@link createInboundEmailHandler}. */
138
219
  interface InboundEmailHandlerOptions<TEnv = Record<string, unknown>> {
139
220
  /** Routes the parsed message onward (e.g. {@link dispatchToLunoraFunction}). */
140
221
  dispatch: InboundDispatch<TEnv>;
141
222
  /**
142
- * Called when `parse`/`verify`/`dispatch` throws. The default rejects the
143
- * message via `message.setReject` so Cloudflare bounces/retries rather than
144
- * silently dropping it. SECURITY: the reject reason is delivered to the
145
- * (attacker-controlled) sender as a bounce, so the default reason is a fixed,
146
- * generic string and the real error is logged server-side. Override to log,
147
- * forward, or swallow — but never pass internal error text to `setReject`.
148
- */
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
+ */
149
241
  onError?: (error: unknown, context: InboundDispatchContext<TEnv>) => Promise<void> | void;
150
242
  /** Parses raw bytes into an {@link InboundEmail} (e.g. `parseInboundEmail`). */
151
243
  parse: (raw: RawInboundEmail) => Promise<InboundEmail>;
152
244
  /**
153
- * Opt-in sender-authentication gate run before `dispatch`. SECURITY: inbound
154
- * `from` is spoofable and dispatch is privileged — supply this (gating on
155
- * `email.authentication`) when an inbound function makes any trust decision.
156
- */
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
+ */
157
256
  verify?: InboundVerify<TEnv>;
158
257
  }
159
258
  /** The `email(message, env, ctx)` callback the factory returns. */
160
259
  type InboundEmailHandler<TEnv = Record<string, unknown>> = (message: ForwardableEmailMessageLike, env: TEnv, context: unknown) => Promise<void>;
161
- /**
162
- * Build the `email(message, env, ctx)` handler. It (a) reads `message.raw`,
163
- * (b) parses it via `parse`, (c) runs the optional `verify` gate, then
164
- * (d) calls `dispatch(parsed, { message, env, ctx })`. Any throw (or a falsy
165
- * `verify`) routes through `onError` (default: a generic `message.setReject`).
166
- */
167
260
  declare const createInboundEmailHandler: <TEnv = Record<string, unknown>>(options: InboundEmailHandlerOptions<TEnv>) => InboundEmailHandler<TEnv>;
168
261
  /** The `RpcEnvelope` shape the runtime's `/_lunora/rpc` path consumes. */
169
262
  interface RpcEnvelope {
@@ -174,17 +267,25 @@ interface RpcEnvelope {
174
267
  /** Options for {@link dispatchToLunoraFunction}. */
175
268
  interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
176
269
  /**
177
- * Admin bearer authorizing the shard RPC. Defaults to reading
178
- * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
179
- */
270
+ * Admin bearer authorizing the shard RPC. Defaults to reading
271
+ * `env.LUNORA_ADMIN_TOKEN` at dispatch time.
272
+ */
180
273
  adminToken?: string;
181
274
  /** `functionPath` of the target mutation/action (e.g. `"inbound:onEmail"`). */
182
275
  functionPath: string;
183
276
  /**
184
- * Map the parsed message into the function's args. Defaults to passing the
185
- * whole {@link InboundEmail} with binary attachment `content` base64-encoded
186
- * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
187
- */
277
+ * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Defaults
278
+ * to the jurisdiction the schema declares (`.jurisdiction("eu")`), so a
279
+ * generated app needs nothing here; set it only for a hand-written worker, to
280
+ * the same value as its `jurisdiction`. A value that contradicts the schema's
281
+ * throws.
282
+ */
283
+ jurisdiction?: DurableObjectJurisdiction;
284
+ /**
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
+ */
188
289
  resolveArgs?: (email: InboundEmail, context: InboundDispatchContext<TEnv>) => unknown;
189
290
  /** The `SHARD` Durable Object namespace. */
190
291
  shard: ShardNamespaceLike;
@@ -192,16 +293,147 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
192
293
  shardKey?: string;
193
294
  }
194
295
  /**
195
- * Build a {@link InboundDispatch} that posts an {@link RpcEnvelope} to the root
196
- * shard stub — the same admin-RPC-over-shard path the dev capture sink uses
197
- * (`from-env.ts`) — routing the parsed message into a named Lunora
198
- * mutation/action. Throws on a non-2xx RPC or a missing admin token so the
199
- * handler's `onError` (default `setReject`) bounces the message.
200
- *
201
- * SECURITY: the RPC carries the admin bearer, so the target function runs with
202
- * RLS bypassed over fully attacker-controlled, spoofable input — see the module
203
- * docstring. Verify the sender (`verify` hook / `email.authentication`) before
204
- * making any trust decision in the target function.
205
- */
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 is marked a trusted system dispatch, so `functionPath` may
305
+ * (and should) name an `internalMutation`/`internalAction` — a public `mutation`
306
+ * target is callable by any browser client with a forged message. The input is
307
+ * fully attacker-controlled and spoofable; verify the sender (`verify` hook /
308
+ * `email.authentication`) before making any trust decision in the target.
309
+ */
206
310
  declare const dispatchToLunoraFunction: <TEnv extends Record<string, unknown> = Record<string, unknown>>(options: DispatchToLunoraFunctionOptions<TEnv>) => InboundDispatch<TEnv>;
207
- 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, type ShardStubLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
311
+ export {
312
+ /**
313
+ * `@lunora/mail/inbound` — inbound Email Routing support.
314
+ *
315
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
316
+ * mutation/action:
317
+ *
318
+ * ```ts
319
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
320
+ *
321
+ * export const email = createInboundEmailHandler({
322
+ * parse: parseInboundEmail,
323
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
324
+ * });
325
+ * ```
326
+ */
327
+ type DispatchToLunoraFunctionOptions,
328
+ /**
329
+ * `@lunora/mail/inbound` — inbound Email Routing support.
330
+ *
331
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
332
+ * mutation/action:
333
+ *
334
+ * ```ts
335
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
336
+ *
337
+ * export const email = createInboundEmailHandler({
338
+ * parse: parseInboundEmail,
339
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
340
+ * });
341
+ * ```
342
+ */
343
+ type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthResult, type InboundAuthentication,
344
+ /**
345
+ * `@lunora/mail/inbound` — inbound Email Routing support.
346
+ *
347
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
348
+ * mutation/action:
349
+ *
350
+ * ```ts
351
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
352
+ *
353
+ * export const email = createInboundEmailHandler({
354
+ * parse: parseInboundEmail,
355
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
356
+ * });
357
+ * ```
358
+ */
359
+ type InboundDispatch,
360
+ /**
361
+ * `@lunora/mail/inbound` — inbound Email Routing support.
362
+ *
363
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
364
+ * mutation/action:
365
+ *
366
+ * ```ts
367
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
368
+ *
369
+ * export const email = createInboundEmailHandler({
370
+ * parse: parseInboundEmail,
371
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
372
+ * });
373
+ * ```
374
+ */
375
+ type InboundDispatchContext, type InboundEmail,
376
+ /**
377
+ * `@lunora/mail/inbound` — inbound Email Routing support.
378
+ *
379
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
380
+ * mutation/action:
381
+ *
382
+ * ```ts
383
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
384
+ *
385
+ * export const email = createInboundEmailHandler({
386
+ * parse: parseInboundEmail,
387
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
388
+ * });
389
+ * ```
390
+ */
391
+ type InboundEmailHandler,
392
+ /**
393
+ * `@lunora/mail/inbound` — inbound Email Routing support.
394
+ *
395
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
396
+ * mutation/action:
397
+ *
398
+ * ```ts
399
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
400
+ *
401
+ * export const email = createInboundEmailHandler({
402
+ * parse: parseInboundEmail,
403
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
404
+ * });
405
+ * ```
406
+ */
407
+ type InboundEmailHandlerOptions,
408
+ /**
409
+ * `@lunora/mail/inbound` — inbound Email Routing support.
410
+ *
411
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
412
+ * mutation/action:
413
+ *
414
+ * ```ts
415
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
416
+ *
417
+ * export const email = createInboundEmailHandler({
418
+ * parse: parseInboundEmail,
419
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
420
+ * });
421
+ * ```
422
+ */
423
+ type InboundVerify, type RawInboundEmail,
424
+ /**
425
+ * `@lunora/mail/inbound` — inbound Email Routing support.
426
+ *
427
+ * Wire a Cloudflare Email Worker entry to route received mail into a Lunora
428
+ * mutation/action:
429
+ *
430
+ * ```ts
431
+ * import { createInboundEmailHandler, parseInboundEmail, dispatchToLunoraFunction } from "@lunora/mail/inbound";
432
+ *
433
+ * export const email = createInboundEmailHandler({
434
+ * parse: parseInboundEmail,
435
+ * dispatch: dispatchToLunoraFunction({ shard: env.SHARD, functionPath: "inbound:onEmail" }),
436
+ * });
437
+ * ```
438
+ */
439
+ type RpcEnvelope, type ShardNamespaceLike, authenticatesFrom, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
@@ -1,2 +1 @@
1
- export { createInboundEmailHandler, dispatchToLunoraFunction } from '../packem_shared/createInboundEmailHandler-Cd8dyzB7.mjs';
2
- export { parseInboundEmail } from '../packem_shared/parseInboundEmail-Bw9u_1oc.mjs';
1
+ import{createInboundEmailHandler as n,dispatchToLunoraFunction as r}from"../packem_shared/createInboundEmailHandler-QnZHQZpF.mjs";import{authenticatesFrom as t,parseInboundEmail as i}from"../packem_shared/authenticatesFrom-BOX-Paur.mjs";export{t as authenticatesFrom,n as createInboundEmailHandler,r as dispatchToLunoraFunction,i as parseInboundEmail};