@lunora/mail 1.0.0-alpha.61 → 1.0.0-alpha.63

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.
@@ -193,8 +193,11 @@ type InboundDispatch<TEnv = Record<string, unknown>> = (email: InboundEmail, con
193
193
  * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
194
194
  * the parsed message. Return `false` (or throw) to reject the message before it
195
195
  * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
196
- * `email.authentication`, an allow-list, etc. Returning `true`/`undefined`
197
- * proceeds.
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.
198
201
  */
199
202
  type InboundVerify<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<boolean | void> | boolean | void;
200
203
  /**
@@ -254,58 +257,6 @@ interface InboundEmailHandlerOptions<TEnv = Record<string, unknown>> {
254
257
  }
255
258
  /** The `email(message, env, ctx)` callback the factory returns. */
256
259
  type InboundEmailHandler<TEnv = Record<string, unknown>> = (message: ForwardableEmailMessageLike, env: TEnv, context: unknown) => Promise<void>;
257
- /**
258
- * Build the `email(message, env, ctx)` handler. It (a) reads `message.raw`,
259
- * (b) parses it via `parse`, (c) runs the optional `verify` gate, then
260
- * (d) calls `dispatch(parsed, { message, env, ctx })`.
261
- *
262
- * The two failure classes are routed differently:
263
- *
264
- * - `parse` / `verify` (including a falsy `verify`) → `onError` (default: a
265
- * generic `message.setReject`). A malformed or unauthenticated message fails
266
- * the same way on every redelivery, so bouncing it is the honest answer.
267
- * - `dispatch` (or its transport) → a custom `onError` is called for
268
- * observability, then the message is handed to `retain` if one is configured
269
- * (SMTP ACCEPTs — the retry is now owned elsewhere) and otherwise bounced with
270
- * the same generic reason. A `retain` that throws bounces too.
271
- *
272
- * WHY THE RETRY IS ABSORBED IN-WORKER RATHER THAN SIGNALLED OVER SMTP — there is
273
- * no transient-reject API and no inbound redelivery to appeal to:
274
- *
275
- * - `setReject` is documented as a PERMANENT SMTP error
276
- * (https://developers.cloudflare.com/email-routing/email-workers/runtime-api/),
277
- * with no "try later" variant.
278
- * - Cloudflare does not document what an uncaught throw from `email()` does. The
279
- * full Email Routing and Email Service docs corpora
280
- * (`developers.cloudflare.com/email-routing/llms-full.txt`,
281
- * `.../email-service/llms-full.txt`) say nothing about an unhandled exception,
282
- * and describe NO redelivery mechanism for inbound Email Workers at all. The
283
- * documented lifecycle
284
- * (https://developers.cloudflare.com/email-service/concepts/email-lifecycle/)
285
- * lists exactly three worker outcomes — `forward()`, `reply()`, `setReject()` —
286
- * with no branch for "the worker threw". (Its 4xx-retry prose is about OUTBOUND
287
- * delivery to the destination MTA, not about invoking the worker.)
288
- * - The behaviour reported in practice is a PERMANENT in-session rejection:
289
- * `521 5.3.0 Upstream error`, i.e. the same permanence as `setReject` but with
290
- * an opaque reason instead of ours. See
291
- * https://community.cloudflare.com/t/is-it-possible-to-return-a-transient-failure-from-an-email-worker/599938
292
- * ("If the email function raises an exception, a permanent failure is returned
293
- * to the client after the DATA command") — a question Cloudflare never answered
294
- * — and https://community.cloudflare.com/t/email-worker-upstream-error/457228.
295
- * Email Routing also states it forwards upstream SMTP errors back to the sender
296
- * in-session rather than generating a bounce later
297
- * (https://developers.cloudflare.com/email-service/reference/postmaster/#smtp-errors).
298
- *
299
- * So every SMTP-visible outcome is permanent, and the only way not to lose a
300
- * legitimate message to a two-second shard 502 is to accept it and take durable
301
- * ownership: that is `retain`. It stays opt-in — with no `retain`, a dispatch
302
- * failure bounces exactly as before.
303
- *
304
- * A dispatch that KNOWS one of its own failures is permanent should call
305
- * `context.message.setReject(...)` and return normally rather than throw, so it
306
- * bounces without being handed to `retain` (see `@lunora/agent`'s inbound
307
- * handler for both cases).
308
- */
309
260
  declare const createInboundEmailHandler: <TEnv = Record<string, unknown>>(options: InboundEmailHandlerOptions<TEnv>) => InboundEmailHandler<TEnv>;
310
261
  /** The `RpcEnvelope` shape the runtime's `/_lunora/rpc` path consumes. */
311
262
  interface RpcEnvelope {
@@ -193,8 +193,11 @@ type InboundDispatch<TEnv = Record<string, unknown>> = (email: InboundEmail, con
193
193
  * Opt-in sender-verification gate. Runs after `parse` and before `dispatch` with
194
194
  * the parsed message. Return `false` (or throw) to reject the message before it
195
195
  * reaches the privileged dispatch — use it to enforce DKIM/SPF/DMARC via
196
- * `email.authentication`, an allow-list, etc. Returning `true`/`undefined`
197
- * proceeds.
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.
198
201
  */
199
202
  type InboundVerify<TEnv = Record<string, unknown>> = (email: InboundEmail, context: InboundDispatchContext<TEnv>) => Promise<boolean | void> | boolean | void;
200
203
  /**
@@ -254,58 +257,6 @@ interface InboundEmailHandlerOptions<TEnv = Record<string, unknown>> {
254
257
  }
255
258
  /** The `email(message, env, ctx)` callback the factory returns. */
256
259
  type InboundEmailHandler<TEnv = Record<string, unknown>> = (message: ForwardableEmailMessageLike, env: TEnv, context: unknown) => Promise<void>;
257
- /**
258
- * Build the `email(message, env, ctx)` handler. It (a) reads `message.raw`,
259
- * (b) parses it via `parse`, (c) runs the optional `verify` gate, then
260
- * (d) calls `dispatch(parsed, { message, env, ctx })`.
261
- *
262
- * The two failure classes are routed differently:
263
- *
264
- * - `parse` / `verify` (including a falsy `verify`) → `onError` (default: a
265
- * generic `message.setReject`). A malformed or unauthenticated message fails
266
- * the same way on every redelivery, so bouncing it is the honest answer.
267
- * - `dispatch` (or its transport) → a custom `onError` is called for
268
- * observability, then the message is handed to `retain` if one is configured
269
- * (SMTP ACCEPTs — the retry is now owned elsewhere) and otherwise bounced with
270
- * the same generic reason. A `retain` that throws bounces too.
271
- *
272
- * WHY THE RETRY IS ABSORBED IN-WORKER RATHER THAN SIGNALLED OVER SMTP — there is
273
- * no transient-reject API and no inbound redelivery to appeal to:
274
- *
275
- * - `setReject` is documented as a PERMANENT SMTP error
276
- * (https://developers.cloudflare.com/email-routing/email-workers/runtime-api/),
277
- * with no "try later" variant.
278
- * - Cloudflare does not document what an uncaught throw from `email()` does. The
279
- * full Email Routing and Email Service docs corpora
280
- * (`developers.cloudflare.com/email-routing/llms-full.txt`,
281
- * `.../email-service/llms-full.txt`) say nothing about an unhandled exception,
282
- * and describe NO redelivery mechanism for inbound Email Workers at all. The
283
- * documented lifecycle
284
- * (https://developers.cloudflare.com/email-service/concepts/email-lifecycle/)
285
- * lists exactly three worker outcomes — `forward()`, `reply()`, `setReject()` —
286
- * with no branch for "the worker threw". (Its 4xx-retry prose is about OUTBOUND
287
- * delivery to the destination MTA, not about invoking the worker.)
288
- * - The behaviour reported in practice is a PERMANENT in-session rejection:
289
- * `521 5.3.0 Upstream error`, i.e. the same permanence as `setReject` but with
290
- * an opaque reason instead of ours. See
291
- * https://community.cloudflare.com/t/is-it-possible-to-return-a-transient-failure-from-an-email-worker/599938
292
- * ("If the email function raises an exception, a permanent failure is returned
293
- * to the client after the DATA command") — a question Cloudflare never answered
294
- * — and https://community.cloudflare.com/t/email-worker-upstream-error/457228.
295
- * Email Routing also states it forwards upstream SMTP errors back to the sender
296
- * in-session rather than generating a bounce later
297
- * (https://developers.cloudflare.com/email-service/reference/postmaster/#smtp-errors).
298
- *
299
- * So every SMTP-visible outcome is permanent, and the only way not to lose a
300
- * legitimate message to a two-second shard 502 is to accept it and take durable
301
- * ownership: that is `retain`. It stays opt-in — with no `retain`, a dispatch
302
- * failure bounces exactly as before.
303
- *
304
- * A dispatch that KNOWS one of its own failures is permanent should call
305
- * `context.message.setReject(...)` and return normally rather than throw, so it
306
- * bounces without being handed to `retain` (see `@lunora/agent`'s inbound
307
- * handler for both cases).
308
- */
309
260
  declare const createInboundEmailHandler: <TEnv = Record<string, unknown>>(options: InboundEmailHandlerOptions<TEnv>) => InboundEmailHandler<TEnv>;
310
261
  /** The `RpcEnvelope` shape the runtime's `/_lunora/rpc` path consumes. */
311
262
  interface RpcEnvelope {
@@ -1 +1 @@
1
- import{createInboundEmailHandler as n,dispatchToLunoraFunction as r}from"../packem_shared/createInboundEmailHandler-vXSNL5mZ.mjs";import{authenticatesFrom as t,parseInboundEmail as i}from"../packem_shared/authenticatesFrom-xpKAFMRN.mjs";export{t as authenticatesFrom,n as createInboundEmailHandler,r as dispatchToLunoraFunction,i as parseInboundEmail};
1
+ import{createInboundEmailHandler as n,dispatchToLunoraFunction as r}from"../packem_shared/createInboundEmailHandler-Bh10YPyJ.mjs";import{authenticatesFrom as t,parseInboundEmail as i}from"../packem_shared/authenticatesFrom-xpKAFMRN.mjs";export{t as authenticatesFrom,n as createInboundEmailHandler,r as dispatchToLunoraFunction,i as parseInboundEmail};
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-8CDAv51Z.mjs";
2
- export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-8CDAv51Z.mjs";
1
+ import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-BD67fxbB.mjs";
2
+ export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-BD67fxbB.mjs";
3
3
  import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-DVADjmEJ.mjs";
4
4
  import { ReactElement } from 'react';
5
5
  /**
@@ -80,7 +80,7 @@ declare const shouldCaptureMail: (env: MailEnv) => boolean;
80
80
  * root-shard inbox via the reserved `recordMail` admin RPC — the same
81
81
  * worker→root-shard path the runtime uses for auth events. Best-effort: without
82
82
  * the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
83
- * never fails for lack of somewhere to record.
83
+ * never fails for lack of somewhere to record — but it says so first (see below).
84
84
  */
85
85
  declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction?: DurableObjectJurisdiction) => MailboxSink;
86
86
  /**
@@ -122,12 +122,33 @@ declare const toQueuedPayload: (options: SendOptions) => QueuedSend;
122
122
  * to a configured `Mailer.send()`. Use this inside your Worker's `queue()`
123
123
  * handler.
124
124
  *
125
+ * DEDUPE IS THE CONSUMER'S JOB, and this helper does not do it. The payload's
126
+ * `idempotencyKey` was minted once at enqueue time so it survives redelivery, but
127
+ * nothing downstream reads it: no transport forwards it to the provider (Resend
128
+ * dedupes on an `Idempotency-Key` REQUEST header, which the provider client offers
129
+ * no hook for — its `headers` field becomes message headers in the body). A
130
+ * consumer that only acks is correct for the failure the ack covers and sends a
131
+ * duplicate for the one it does not: the provider accepted the message and the
132
+ * worker died before acking.
133
+ *
134
+ * The recipe below NARROWS that window; it does not close it, and nothing on
135
+ * this path can. The mark is written after the provider accepted the message, so
136
+ * a crash in between still redelivers and resends, and a KV read is eventually
137
+ * consistent — a redelivery seconds later can miss a mark that was written.
138
+ * Delivery is at-least-once end to end. An app that must not double-send needs a
139
+ * strongly-consistent mark taken BEFORE the send (a Durable Object or D1 row
140
+ * claimed by this key, released on failure), and even then the send-then-crash
141
+ * window is only ever traded for a send-that-may-not-have-happened one.
142
+ *
125
143
  * ```ts
126
144
  * export default {
127
145
  * queue: async (batch, env) => {
128
146
  * const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
129
147
  * for (const message of batch.messages) {
148
+ * const { idempotencyKey } = message.body;
149
+ * if (await env.SENT.get(idempotencyKey)) { message.ack(); continue; }
130
150
  * await consumeQueuedSend(mailer, message.body);
151
+ * await env.SENT.put(idempotencyKey, "1", { expirationTtl: 86_400 });
131
152
  * message.ack();
132
153
  * }
133
154
  * },
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-8CDAv51Z.js";
2
- export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-8CDAv51Z.js";
1
+ import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-BD67fxbB.js";
2
+ export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-BD67fxbB.js";
3
3
  import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-DVADjmEJ.js";
4
4
  import { ReactElement } from 'react';
5
5
  /**
@@ -80,7 +80,7 @@ declare const shouldCaptureMail: (env: MailEnv) => boolean;
80
80
  * root-shard inbox via the reserved `recordMail` admin RPC — the same
81
81
  * worker→root-shard path the runtime uses for auth events. Best-effort: without
82
82
  * the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
83
- * never fails for lack of somewhere to record.
83
+ * never fails for lack of somewhere to record — but it says so first (see below).
84
84
  */
85
85
  declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction?: DurableObjectJurisdiction) => MailboxSink;
86
86
  /**
@@ -122,12 +122,33 @@ declare const toQueuedPayload: (options: SendOptions) => QueuedSend;
122
122
  * to a configured `Mailer.send()`. Use this inside your Worker's `queue()`
123
123
  * handler.
124
124
  *
125
+ * DEDUPE IS THE CONSUMER'S JOB, and this helper does not do it. The payload's
126
+ * `idempotencyKey` was minted once at enqueue time so it survives redelivery, but
127
+ * nothing downstream reads it: no transport forwards it to the provider (Resend
128
+ * dedupes on an `Idempotency-Key` REQUEST header, which the provider client offers
129
+ * no hook for — its `headers` field becomes message headers in the body). A
130
+ * consumer that only acks is correct for the failure the ack covers and sends a
131
+ * duplicate for the one it does not: the provider accepted the message and the
132
+ * worker died before acking.
133
+ *
134
+ * The recipe below NARROWS that window; it does not close it, and nothing on
135
+ * this path can. The mark is written after the provider accepted the message, so
136
+ * a crash in between still redelivers and resends, and a KV read is eventually
137
+ * consistent — a redelivery seconds later can miss a mark that was written.
138
+ * Delivery is at-least-once end to end. An app that must not double-send needs a
139
+ * strongly-consistent mark taken BEFORE the send (a Durable Object or D1 row
140
+ * claimed by this key, released on failure), and even then the send-then-crash
141
+ * window is only ever traded for a send-that-may-not-have-happened one.
142
+ *
125
143
  * ```ts
126
144
  * export default {
127
145
  * queue: async (batch, env) => {
128
146
  * const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
129
147
  * for (const message of batch.messages) {
148
+ * const { idempotencyKey } = message.body;
149
+ * if (await env.SENT.get(idempotencyKey)) { message.ack(); continue; }
130
150
  * await consumeQueuedSend(mailer, message.body);
151
+ * await env.SENT.put(idempotencyKey, "1", { expirationTtl: 86_400 });
131
152
  * message.ack();
132
153
  * }
133
154
  * },
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-DpfCxn16.mjs";import{default as u}from"./packem_shared/createMailer-CjQmT7Ln.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-wbDNGuA-.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-CRfALnCU.mjs";import{default as i}from"./packem_shared/renderEmail-BOUnr6i3.mjs";import{default as M}from"./packem_shared/createResendTransport-DDkOitck.mjs";export{s as consumeQueuedSend,f as createCaptureSink,a as createCaptureTransport,t as createCloudflareTransport,u as createMailer,l as createMailerFromEnv,M as createResendTransport,i as renderEmail,m as shouldCaptureMail,c as toQueuedPayload};
1
+ import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-DpfCxn16.mjs";import{default as u}from"./packem_shared/createMailer-CjQmT7Ln.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-CeV0DSsT.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-CRfALnCU.mjs";import{default as i}from"./packem_shared/renderEmail-BOUnr6i3.mjs";import{default as M}from"./packem_shared/createResendTransport-DDkOitck.mjs";export{s as consumeQueuedSend,f as createCaptureSink,a as createCaptureTransport,t as createCloudflareTransport,u as createMailer,l as createMailerFromEnv,M as createResendTransport,i as renderEmail,m as shouldCaptureMail,c as toQueuedPayload};
@@ -39,8 +39,12 @@ interface SendOptions {
39
39
  * redelivery. Only meaningful for `mailer.queue()` — `mailer.send()` ignores it.
40
40
  * When omitted, `queue()` generates one at enqueue time so it survives redelivery
41
41
  * (a key minted in the consumer would change on every retry, defeating the point).
42
- * Not forwarded to the mail provider a consumer wanting exactly-once delivery
43
- * must dedupe against its own store using this key.
42
+ * Not forwarded to the mail provider, and no transport can: Resend dedupes on an
43
+ * `Idempotency-Key` REQUEST header, and the provider client exposes no hook for
44
+ * one (its own `headers` field becomes message headers in the JSON body). So a
45
+ * consumer that wants to collapse redeliveries MUST dedupe against its own store
46
+ * using this key — see `consumeQueuedSend` for the shape, and for why that is a
47
+ * narrowed at-least-once rather than exactly-once.
44
48
  */
45
49
  idempotencyKey?: string;
46
50
  react?: ReactElement;
@@ -39,8 +39,12 @@ interface SendOptions {
39
39
  * redelivery. Only meaningful for `mailer.queue()` — `mailer.send()` ignores it.
40
40
  * When omitted, `queue()` generates one at enqueue time so it survives redelivery
41
41
  * (a key minted in the consumer would change on every retry, defeating the point).
42
- * Not forwarded to the mail provider a consumer wanting exactly-once delivery
43
- * must dedupe against its own store using this key.
42
+ * Not forwarded to the mail provider, and no transport can: Resend dedupes on an
43
+ * `Idempotency-Key` REQUEST header, and the provider client exposes no hook for
44
+ * one (its own `headers` field becomes message headers in the JSON body). So a
45
+ * consumer that wants to collapse redeliveries MUST dedupe against its own store
46
+ * using this key — see `consumeQueuedSend` for the shape, and for why that is a
47
+ * narrowed at-least-once rather than exactly-once.
44
48
  */
45
49
  idempotencyKey?: string;
46
50
  react?: ReactElement;
@@ -0,0 +1 @@
1
+ import{createCaptureTransport as l}from"./createCaptureTransport-CKc6NpZR.mjs";import"./createCloudflareTransport-DpfCxn16.mjs";import n from"./createMailer-CjQmT7Ln.mjs";import{LunoraError as u}from"@lunora/errors";import{D as c,p as E}from"./shard-CRhAKK93.mjs";import"./consumeQueuedSend-CRfALnCU.mjs";import"./renderEmail-BOUnr6i3.mjs";import"./createResendTransport-DDkOitck.mjs";const p="__lunora_admin__:recordMail",N=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,_=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],f=(e,t)=>{const r=e[t];if(typeof r!="string"||r==="")throw new u("INTERNAL",`@lunora/mail: missing env var \`${t}\` — set it in .dev.vars (and \`wrangler secret put ${t}\` for secrets).`);return r},m=e=>{const t=e.LUNORA_MAIL_CAPTURE;if(typeof t=="string"){const r=t.toLowerCase();if(r==="1"||r==="true")return!0;if(r==="0"||r==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${t}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}return _.some(r=>{const o=e[r];return typeof o=="string"&&N.test(o)})};let d=!1;const A=(e,t=c,r)=>({record:async o=>{const i=e.SHARD,a=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(i===void 0||a===void 0)return d||(d=!0,console.warn("@lunora/mail: capturing mail but there is nowhere to record it — the `SHARD` binding and/or `LUNORA_ADMIN_TOKEN` is missing, so every captured message is discarded. Set both to see mail in the studio inbox, or set LUNORA_MAIL_CAPTURE=0 to deliver for real.")),{id:"uncaptured"};try{return{id:(await E(i,{adminToken:a,envelope:{args:o,functionPath:p},jurisdiction:r,label:"@lunora/mail: recording captured mail",shardKey:t})).result?.id??"captured"}}catch(s){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",s),{id:"uncaptured"}}}}),h=(e,t={})=>{const r=f(e,"MAIL_FROM");if(m(e))return n({from:r,transport:l(A(e,t.rootShard,t.jurisdiction))});if(t.cloudflareSend)return n({cloudflareSend:t.cloudflareSend,from:r});const o=typeof e.RESEND_API_KEY=="string"?e.RESEND_API_KEY:void 0;if(o!==void 0&&o!=="")return n({apiKey:o,from:r});throw new u("INTERNAL","@lunora/mail: no transport configured — provide `cloudflareSend` (a SEND_EMAIL binding) or RESEND_API_KEY, or run in a dev environment to capture.")};export{A as createCaptureSink,h as createMailerFromEnv,m as shouldCaptureMail};
@@ -0,0 +1 @@
1
+ import{LunoraError as d}from"@lunora/errors";import{D as h,p as l}from"./shard-CRhAKK93.mjs";const f=r=>{let n="";for(let e=0;e<r.length;e+=32768)n+=String.fromCharCode(...r.subarray(e,e+32768));return btoa(n)},E="message could not be processed",u=(r,n)=>{console.error("@lunora/mail/inbound: dropping message —",r),n.message.setReject(E)},m=r=>r===!0||r===void 0,A=r=>{const n=r.onError??u;return async(t,e,c)=>{const a={ctx:c,env:e,message:t};let i;try{if(i=await r.parse(t.raw),r.verify){const o=await r.verify(i,a);if(!m(o))throw new d("INTERNAL","@lunora/mail/inbound: sender verification rejected the message")}}catch(o){await n(o,a);return}try{await r.dispatch(i,a)}catch(o){if(r.onError)try{await r.onError(o,a)}catch(s){console.error("@lunora/mail/inbound: onError threw while reporting a dispatch failure —",s)}if(r.retain)try{await r.retain(i,a,o);return}catch(s){console.error("@lunora/mail/inbound: retain failed to take ownership of the message —",s)}u(o,a)}}},y=r=>r.attachments.length===0?r:{...r,attachments:r.attachments.map(n=>{const{content:t}=n;if(typeof t=="string")return n;const e=t instanceof Uint8Array?t:new Uint8Array(t);return{...n,content:f(e),encoding:"base64"}})},N=r=>{const n=r.shardKey??h,t=r.resolveArgs??(e=>y(e));return async(e,c)=>{const a=r.adminToken??(typeof c.env.LUNORA_ADMIN_TOKEN=="string"?c.env.LUNORA_ADMIN_TOKEN:void 0);if(a===void 0||a==="")throw new d("INTERNAL","@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");const i={args:t(e,c),functionPath:r.functionPath,shardKey:n};await l(r.shard,{adminToken:a,envelope:i,jurisdiction:r.jurisdiction,label:`@lunora/mail/inbound: dispatch to \`${r.functionPath}\``,shardKey:n})}};export{A as createInboundEmailHandler,N as dispatchToLunoraFunction};
@@ -1,4 +1,4 @@
1
- import { C as CapturedMail } from "./packem_shared/capture-transport.d-8CDAv51Z.mjs";
1
+ import { C as CapturedMail } from "./packem_shared/capture-transport.d-BD67fxbB.mjs";
2
2
  import 'react';
3
3
  /** Minimal `fetch` projection so a test can inject a stub. */
4
4
  type FetchLike = (input: string, init?: {
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CapturedMail } from "./packem_shared/capture-transport.d-8CDAv51Z.js";
1
+ import { C as CapturedMail } from "./packem_shared/capture-transport.d-BD67fxbB.js";
2
2
  import 'react';
3
3
  /** Minimal `fetch` projection so a test can inject a stub. */
4
4
  type FetchLike = (input: string, init?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mail",
3
- "version": "1.0.0-alpha.61",
3
+ "version": "1.0.0-alpha.63",
4
4
  "description": "Email for Lunora: Resend adapter, TSX templates, and queue-backed sends",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -54,7 +54,7 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@lunora/errors": "1.0.0-alpha.30",
57
+ "@lunora/errors": "1.0.0-alpha.31",
58
58
  "@react-email/render": "2.1.0",
59
59
  "@visulima/email": "2.1.8",
60
60
  "postal-mime": "2.7.6"
@@ -1 +0,0 @@
1
- import{createCaptureTransport as d}from"./createCaptureTransport-CKc6NpZR.mjs";import"./createCloudflareTransport-DpfCxn16.mjs";import n from"./createMailer-CjQmT7Ln.mjs";import{LunoraError as c}from"@lunora/errors";import{D as s,p as l}from"./shard-CRhAKK93.mjs";import"./consumeQueuedSend-CRfALnCU.mjs";import"./renderEmail-BOUnr6i3.mjs";import"./createResendTransport-DDkOitck.mjs";const E="__lunora_admin__:recordMail",p=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,f=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],_=(t,e)=>{const r=t[e];if(typeof r!="string"||r==="")throw new c("INTERNAL",`@lunora/mail: missing env var \`${e}\` — set it in .dev.vars (and \`wrangler secret put ${e}\` for secrets).`);return r},N=t=>{const e=t.LUNORA_MAIL_CAPTURE;if(typeof e=="string"){const r=e.toLowerCase();if(r==="1"||r==="true")return!0;if(r==="0"||r==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${e}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}return f.some(r=>{const o=t[r];return typeof o=="string"&&p.test(o)})},m=(t,e=s,r)=>({record:async o=>{const i=t.SHARD,a=typeof t.LUNORA_ADMIN_TOKEN=="string"?t.LUNORA_ADMIN_TOKEN:void 0;if(i===void 0||a===void 0)return{id:"uncaptured"};try{return{id:(await l(i,{adminToken:a,envelope:{args:o,functionPath:E},jurisdiction:r,label:"@lunora/mail: recording captured mail",shardKey:e})).result?.id??"captured"}}catch(u){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",u),{id:"uncaptured"}}}}),L=(t,e={})=>{const r=_(t,"MAIL_FROM");if(N(t))return n({from:r,transport:d(m(t,e.rootShard,e.jurisdiction))});if(e.cloudflareSend)return n({cloudflareSend:e.cloudflareSend,from:r});const o=typeof t.RESEND_API_KEY=="string"?t.RESEND_API_KEY:void 0;if(o!==void 0&&o!=="")return n({apiKey:o,from:r});throw new c("INTERNAL","@lunora/mail: no transport configured — provide `cloudflareSend` (a SEND_EMAIL binding) or RESEND_API_KEY, or run in a dev environment to capture.")};export{m as createCaptureSink,L as createMailerFromEnv,N as shouldCaptureMail};
@@ -1 +0,0 @@
1
- import{LunoraError as d}from"@lunora/errors";import{D as h,p as l}from"./shard-CRhAKK93.mjs";const f=r=>{let n="";for(let e=0;e<r.length;e+=32768)n+=String.fromCharCode(...r.subarray(e,e+32768));return btoa(n)},E="message could not be processed",u=(r,n)=>{console.error("@lunora/mail/inbound: dropping message —",r),n.message.setReject(E)},y=r=>{const n=r.onError??u;return async(t,e,c)=>{const a={ctx:c,env:e,message:t};let o;try{if(o=await r.parse(t.raw),r.verify&&await r.verify(o,a)===!1)throw new d("INTERNAL","@lunora/mail/inbound: sender verification rejected the message")}catch(i){await n(i,a);return}try{await r.dispatch(o,a)}catch(i){if(r.onError)try{await r.onError(i,a)}catch(s){console.error("@lunora/mail/inbound: onError threw while reporting a dispatch failure —",s)}if(r.retain)try{await r.retain(o,a,i);return}catch(s){console.error("@lunora/mail/inbound: retain failed to take ownership of the message —",s)}u(i,a)}}},m=r=>r.attachments.length===0?r:{...r,attachments:r.attachments.map(n=>{const{content:t}=n;if(typeof t=="string")return n;const e=t instanceof Uint8Array?t:new Uint8Array(t);return{...n,content:f(e),encoding:"base64"}})},A=r=>{const n=r.shardKey??h,t=r.resolveArgs??(e=>m(e));return async(e,c)=>{const a=r.adminToken??(typeof c.env.LUNORA_ADMIN_TOKEN=="string"?c.env.LUNORA_ADMIN_TOKEN:void 0);if(a===void 0||a==="")throw new d("INTERNAL","@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");const o={args:t(e,c),functionPath:r.functionPath,shardKey:n};await l(r.shard,{adminToken:a,envelope:o,jurisdiction:r.jurisdiction,label:`@lunora/mail/inbound: dispatch to \`${r.functionPath}\``,shardKey:n})}};export{y as createInboundEmailHandler,A as dispatchToLunoraFunction};