@lunora/mail 1.0.0-alpha.100 → 1.0.0-alpha.102
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 +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/createCaptureSink-Bw5lqHql.mjs +1 -0
- package/dist/packem_shared/createMailer-ImyDcZuj.mjs +1 -0
- package/dist/packem_shared/renderEmail-CkxYrU2s.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/createCaptureSink-WtmvYpT-.mjs +0 -1
- package/dist/packem_shared/createMailer-TVxz0_5S.mjs +0 -1
- package/dist/packem_shared/renderEmail-BOUnr6i3.mjs +0 -1
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
|
-
/**
|
|
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,
|
|
95
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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,
|
|
95
|
-
*
|
|
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
|
-
*
|
|
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-
|
|
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.
|
|
3
|
+
"version": "1.0.0-alpha.102",
|
|
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.
|
|
57
|
+
"@lunora/errors": "1.0.0-alpha.51",
|
|
58
58
|
"@react-email/render": "2.1.0",
|
|
59
|
-
"@visulima/email": "3.0.
|
|
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};
|