@lunora/mail 1.0.0-alpha.101 → 1.0.0-alpha.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -56,7 +56,11 @@ declare const createMailer: (options: LunoraMailOptions) => Mailer;
56
56
  type MailEnv = Record<string, unknown>;
57
57
  /** Options for {@link createMailerFromEnv}. */
58
58
  interface FromEnvOptions {
59
- /** RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare default transport). */
59
+ /**
60
+ * RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare
61
+ * default transport). Used only when `env.SEND_EMAIL` is set, so it can be
62
+ * passed unconditionally.
63
+ */
60
64
  cloudflareSend?: CloudflareSend;
61
65
  /**
62
66
  * Pin the captured-mail inbox shard to a Cloudflare data-residency
@@ -91,9 +95,13 @@ declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction
91
95
  /**
92
96
  * Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
93
97
  * captured into the studio's Mail inbox; otherwise it delivers via the supplied
94
- * `cloudflareSend` (the `SEND_EMAIL` binding) or, failing that, `RESEND_API_KEY`.
95
- * Throws when neither a capture context nor a real transport is available, so a
96
- * misconfigured production deploy fails loudly instead of silently dropping mail.
98
+ * `cloudflareSend` (used only when the `SEND_EMAIL` binding is present) or,
99
+ * failing that, `RESEND_API_KEY`. With no real transport `send()` rejects, so a
100
+ * misconfigured production deploy fails loudly instead of silently dropping mail
101
+ * — while `queue()` still works for a producer-only worker.
102
+ *
103
+ * A queue consumer should build its mailer here too: `consumeQueuedSend` goes
104
+ * through `send()`, so in dev queued mail is captured at consume time.
97
105
  *
98
106
  * `MAIL_FROM` is required (the default sender).
99
107
  */
@@ -148,7 +156,8 @@ declare const toQueuedPayload: (options: SendOptions) => QueuedSend;
148
156
  * ```ts
149
157
  * export default {
150
158
  * queue: async (batch, env) => {
151
- * const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
159
+ * // createMailerFromEnv, so dev captures the message here instead of sending it.
160
+ * const mailer = createMailerFromEnv(env);
152
161
  * for (const message of batch.messages) {
153
162
  * const { idempotencyKey } = message.body;
154
163
  * if (await env.SENT.get(idempotencyKey)) { message.ack(); continue; }
@@ -167,6 +176,10 @@ declare const consumeQueuedSend: (mailer: Mailer, payload: unknown) => Promise<{
167
176
  * Render a React element to an HTML/text pair suitable for inlining into a
168
177
  * provider payload. Wraps `@react-email/render` so we can swap to
169
178
  * `@visulima/email`'s react-email template engine without touching callers.
179
+ *
180
+ * The renderer is imported lazily so its top-level evaluation (prettier and
181
+ * html-to-text, in its workerd build) is deferred until the first React render
182
+ * instead of running on cold start. Bundlers may still include the code.
170
183
  */
171
184
  declare const renderEmail: (element: ReactElement) => Promise<{
172
185
  html: string;
package/dist/index.d.ts CHANGED
@@ -56,7 +56,11 @@ declare const createMailer: (options: LunoraMailOptions) => Mailer;
56
56
  type MailEnv = Record<string, unknown>;
57
57
  /** Options for {@link createMailerFromEnv}. */
58
58
  interface FromEnvOptions {
59
- /** RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare default transport). */
59
+ /**
60
+ * RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare
61
+ * default transport). Used only when `env.SEND_EMAIL` is set, so it can be
62
+ * passed unconditionally.
63
+ */
60
64
  cloudflareSend?: CloudflareSend;
61
65
  /**
62
66
  * Pin the captured-mail inbox shard to a Cloudflare data-residency
@@ -91,9 +95,13 @@ declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction
91
95
  /**
92
96
  * Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
93
97
  * captured into the studio's Mail inbox; otherwise it delivers via the supplied
94
- * `cloudflareSend` (the `SEND_EMAIL` binding) or, failing that, `RESEND_API_KEY`.
95
- * Throws when neither a capture context nor a real transport is available, so a
96
- * misconfigured production deploy fails loudly instead of silently dropping mail.
98
+ * `cloudflareSend` (used only when the `SEND_EMAIL` binding is present) or,
99
+ * failing that, `RESEND_API_KEY`. With no real transport `send()` rejects, so a
100
+ * misconfigured production deploy fails loudly instead of silently dropping mail
101
+ * — while `queue()` still works for a producer-only worker.
102
+ *
103
+ * A queue consumer should build its mailer here too: `consumeQueuedSend` goes
104
+ * through `send()`, so in dev queued mail is captured at consume time.
97
105
  *
98
106
  * `MAIL_FROM` is required (the default sender).
99
107
  */
@@ -148,7 +156,8 @@ declare const toQueuedPayload: (options: SendOptions) => QueuedSend;
148
156
  * ```ts
149
157
  * export default {
150
158
  * queue: async (batch, env) => {
151
- * const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
159
+ * // createMailerFromEnv, so dev captures the message here instead of sending it.
160
+ * const mailer = createMailerFromEnv(env);
152
161
  * for (const message of batch.messages) {
153
162
  * const { idempotencyKey } = message.body;
154
163
  * if (await env.SENT.get(idempotencyKey)) { message.ack(); continue; }
@@ -167,6 +176,10 @@ declare const consumeQueuedSend: (mailer: Mailer, payload: unknown) => Promise<{
167
176
  * Render a React element to an HTML/text pair suitable for inlining into a
168
177
  * provider payload. Wraps `@react-email/render` so we can swap to
169
178
  * `@visulima/email`'s react-email template engine without touching callers.
179
+ *
180
+ * The renderer is imported lazily so its top-level evaluation (prettier and
181
+ * html-to-text, in its workerd build) is deferred until the first React render
182
+ * instead of running on cold start. Bundlers may still include the code.
170
183
  */
171
184
  declare const renderEmail: (element: ReactElement) => Promise<{
172
185
  html: string;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-DZhMUmaI.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-bWoNSnK_.mjs";import{default as u}from"./packem_shared/createMailer-TVxz0_5S.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-WtmvYpT-.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-BMjRlHmy.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-DZhMUmaI.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-bWoNSnK_.mjs";import{default as u}from"./packem_shared/createMailer-ImyDcZuj.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-Bw5lqHql.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-CRfALnCU.mjs";import{default as i}from"./packem_shared/renderEmail-CkxYrU2s.mjs";import{default as M}from"./packem_shared/createResendTransport-BMjRlHmy.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};
@@ -0,0 +1 @@
1
+ import{createCaptureTransport as d}from"./createCaptureTransport-DZhMUmaI.mjs";import"./createCloudflareTransport-bWoNSnK_.mjs";import s from"./createMailer-ImyDcZuj.mjs";import{LunoraError as l}from"@lunora/errors";import{D as E,a as _,p}from"./shard-C5t-b7FX.mjs";import"./consumeQueuedSend-CRfALnCU.mjs";import"./renderEmail-CkxYrU2s.mjs";import"./createResendTransport-BMjRlHmy.mjs";const N="__lunora_admin__:recordMail",f=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,m=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],A=(r,e)=>{const o=r[e];if(typeof o!="string"||o==="")throw new l("INTERNAL",`@lunora/mail: missing env var \`${e}\` — set it in .dev.vars (and \`wrangler secret put ${e}\` for secrets).`);return o},R=r=>{const e=r.LUNORA_MAIL_CAPTURE;if(typeof e=="string"){const t=e.toLowerCase();if(t==="1"||t==="true")return!0;if(t==="0"||t==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${e}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}const o=m.map(t=>r[t]).filter(t=>typeof t=="string"&&t!=="");return o.length>0&&o.every(t=>f.test(t))};let u=!1;const g=(r,e=E,o)=>({record:async t=>{const i=r.SHARD,a=typeof r.LUNORA_ADMIN_TOKEN=="string"?r.LUNORA_ADMIN_TOKEN:void 0;if(i===void 0||a===void 0)return u||(u=!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"};const c=_(i,o);try{return{id:(await p(c,{adminToken:a,envelope:{args:t,functionPath:N},label:"@lunora/mail: recording captured mail",shardKey:e})).result?.id??"captured"}}catch(n){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",n),{id:"uncaptured"}}}}),y=(r,e={})=>{const o=A(r,"MAIL_FROM");if(R(r))return s({from:o,queue:e.queue,transport:d(g(r,e.rootShard,e.jurisdiction))});const t=r.SEND_EMAIL===void 0?void 0:e.cloudflareSend,i=typeof r.RESEND_API_KEY=="string"&&r.RESEND_API_KEY!==""?r.RESEND_API_KEY:void 0;return s({apiKey:i,cloudflareSend:t,from:o,queue:e.queue})};export{g as createCaptureSink,y as createMailerFromEnv,R as shouldCaptureMail};
@@ -0,0 +1 @@
1
+ import{LunoraError as c}from"@lunora/errors";import{a as l,c as s}from"./address-oKy_d0aG.mjs";import{isCaptureTransport as m}from"./createCaptureTransport-DZhMUmaI.mjs";import{createCloudflareTransport as i}from"./createCloudflareTransport-bWoNSnK_.mjs";import{toQueuedPayload as y}from"./consumeQueuedSend-CRfALnCU.mjs";import b from"./renderEmail-CkxYrU2s.mjs";import h from"./createResendTransport-BMjRlHmy.mjs";const w=()=>{if(typeof crypto<"u"){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto.getRandomValues=="function"){const r=crypto.getRandomValues(new Uint8Array(16));return Array.from(r,n=>n.toString(16).padStart(2,"0")).join("")}}throw new Error("randomSessionId: no Web Crypto available — a session id needs crypto.randomUUID or crypto.getRandomValues")},p=r=>{if(r.cloudflareSend)return i({from:r.from,send:r.cloudflareSend});if(r.apiKey)return h(r.apiKey,r.from);if(r.queue)return{send:()=>Promise.reject(new c("INTERNAL","@lunora/mail: no transport configured — this mailer can only queue(); pass `transport`, `cloudflareSend` (a SEND_EMAIL binding) or `apiKey` (Resend) to send()"))};throw new c("INTERNAL","@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend), or a `queue` for a producer-only mailer")},j=r=>{if(!r.from)throw new c("INTERNAL","@lunora/mail: `from` is required");const n=r.transport??p(r),o=async e=>{let{html:a}=e,{text:u}=e;if(e.react){const t=await b(e.react);a=a??t.html,u=u??t.text}if(l("subject",e.subject),e.headers)for(const[t,f]of Object.entries(e.headers))l(`header name "${t}"`,t),l(`header "${t}" value`,f);const d=e.from??r.from;return s({bcc:e.bcc,cc:e.cc,from:d,replyTo:e.replyTo,to:e.to}),{bcc:e.bcc?.length?e.bcc:void 0,cc:e.cc?.length?e.cc:void 0,from:d,headers:e.headers,html:a,replyTo:e.replyTo,subject:e.subject,text:u,to:e.to}};return{queue:async e=>{if(!r.queue){if(m(n)){const d=await o(e);return await n.send(d),{queued:!0}}throw new c("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const a=await o(e),u=e.idempotencyKey??w();return await r.queue.send(y({...a,idempotencyKey:u})),{queued:!0}},send:async e=>{const a=await o(e);return n.send(a)}}};export{j as default};
@@ -0,0 +1 @@
1
+ const n=async t=>{const{render:a}=await import("@react-email/render"),[r,e]=await Promise.all([a(t,{pretty:!1}),a(t,{plainText:!0})]);return{html:r,text:e}};export{n as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mail",
3
- "version": "1.0.0-alpha.101",
3
+ "version": "1.0.0-alpha.103",
4
4
  "description": "Email for Lunora: Resend adapter, TSX templates, and queue-backed sends",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -54,9 +54,9 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@lunora/errors": "1.0.0-alpha.51",
57
+ "@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
58
58
  "@react-email/render": "2.1.0",
59
- "@visulima/email": "3.0.26",
59
+ "@visulima/email": "3.0.28",
60
60
  "postal-mime": "3.0.1"
61
61
  },
62
62
  "peerDependencies": {
@@ -1 +0,0 @@
1
- import{createCaptureTransport as c}from"./createCaptureTransport-DZhMUmaI.mjs";import"./createCloudflareTransport-bWoNSnK_.mjs";import n from"./createMailer-TVxz0_5S.mjs";import{LunoraError as d}from"@lunora/errors";import{D as E,a as f,p}from"./shard-C5t-b7FX.mjs";import"./consumeQueuedSend-CRfALnCU.mjs";import"./renderEmail-BOUnr6i3.mjs";import"./createResendTransport-BMjRlHmy.mjs";const N="__lunora_admin__:recordMail",_=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,m=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],A=(t,r)=>{const o=t[r];if(typeof o!="string"||o==="")throw new d("INTERNAL",`@lunora/mail: missing env var \`${r}\` — set it in .dev.vars (and \`wrangler secret put ${r}\` for secrets).`);return o},R=t=>{const r=t.LUNORA_MAIL_CAPTURE;if(typeof r=="string"){const e=r.toLowerCase();if(e==="1"||e==="true")return!0;if(e==="0"||e==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${r}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}const o=m.map(e=>t[e]).filter(e=>typeof e=="string"&&e!=="");return o.length>0&&o.every(e=>_.test(e))};let s=!1;const g=(t,r=E,o)=>({record:async e=>{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 s||(s=!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"};const l=f(i,o);try{return{id:(await p(l,{adminToken:a,envelope:{args:e,functionPath:N},label:"@lunora/mail: recording captured mail",shardKey:r})).result?.id??"captured"}}catch(u){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",u),{id:"uncaptured"}}}}),D=(t,r={})=>{const o=A(t,"MAIL_FROM");if(R(t))return n({from:o,queue:r.queue,transport:c(g(t,r.rootShard,r.jurisdiction))});if(r.cloudflareSend)return n({cloudflareSend:r.cloudflareSend,from:o,queue:r.queue});const e=typeof t.RESEND_API_KEY=="string"?t.RESEND_API_KEY:void 0;if(e!==void 0&&e!=="")return n({apiKey:e,from:o,queue:r.queue});throw new d("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{g as createCaptureSink,D as createMailerFromEnv,R as shouldCaptureMail};
@@ -1 +0,0 @@
1
- import{LunoraError as l}from"@lunora/errors";import{a as o,c as m}from"./address-oKy_d0aG.mjs";import{isCaptureTransport as s}from"./createCaptureTransport-DZhMUmaI.mjs";import{createCloudflareTransport as y}from"./createCloudflareTransport-bWoNSnK_.mjs";import{toQueuedPayload as i}from"./consumeQueuedSend-CRfALnCU.mjs";import b from"./renderEmail-BOUnr6i3.mjs";import h from"./createResendTransport-BMjRlHmy.mjs";const w=()=>{if(typeof crypto<"u"){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto.getRandomValues=="function"){const r=crypto.getRandomValues(new Uint8Array(16));return Array.from(r,c=>c.toString(16).padStart(2,"0")).join("")}}throw new Error("randomSessionId: no Web Crypto available — a session id needs crypto.randomUUID or crypto.getRandomValues")},p=r=>{if(r.cloudflareSend)return y({from:r.from,send:r.cloudflareSend});if(r.apiKey)return h(r.apiKey,r.from);throw new l("INTERNAL","@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)")},A=r=>{if(!r.from)throw new l("INTERNAL","@lunora/mail: `from` is required");const c=r.transport??p(r),n=async e=>{let{html:a}=e,{text:d}=e;if(e.react){const t=await b(e.react);a=a??t.html,d=d??t.text}if(o("subject",e.subject),e.headers)for(const[t,f]of Object.entries(e.headers))o(`header name "${t}"`,t),o(`header "${t}" value`,f);const u=e.from??r.from;return m({bcc:e.bcc,cc:e.cc,from:u,replyTo:e.replyTo,to:e.to}),{bcc:e.bcc?.length?e.bcc:void 0,cc:e.cc?.length?e.cc:void 0,from:u,headers:e.headers,html:a,replyTo:e.replyTo,subject:e.subject,text:d,to:e.to}};return{queue:async e=>{if(!r.queue){if(s(c)){const u=await n(e);return await c.send(u),{queued:!0}}throw new l("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const a=await n(e),d=e.idempotencyKey??w();return await r.queue.send(i({...a,idempotencyKey:d})),{queued:!0}},send:async e=>{const a=await n(e);return c.send(a)}}};export{A as default};
@@ -1 +0,0 @@
1
- import{render as r}from"@react-email/render";const n=async t=>{const[e,a]=await Promise.all([r(t,{pretty:!1}),r(t,{plainText:!0})]);return{html:e,text:a}};export{n as default};