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

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.
@@ -1,3 +1,5 @@
1
+ import { D as DurableObjectJurisdiction, S as ShardNamespaceLike } from "../packem_shared/shard.d-CL2Lmliv.mjs";
2
+ export type { a as ShardStubLike } from "../packem_shared/shard.d-CL2Lmliv.mjs";
1
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. */
@@ -68,28 +70,6 @@ interface InboundEmail {
68
70
  */
69
71
  declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
70
72
  /**
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
- }
92
- /**
93
73
  * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
94
74
  * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
95
75
  * members the handler touches are modelled, so the host can pass the real
@@ -181,6 +161,12 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
181
161
  /** `functionPath` of the target mutation/action (e.g. `"inbound:onEmail"`). */
182
162
  functionPath: string;
183
163
  /**
164
+ * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Pass the
165
+ * same value as the worker's `jurisdiction` so inbound mail routes to the
166
+ * jurisdiction-pinned shard. Omit for the un-pinned global namespace.
167
+ */
168
+ jurisdiction?: DurableObjectJurisdiction;
169
+ /**
184
170
  * Map the parsed message into the function's args. Defaults to passing the
185
171
  * whole {@link InboundEmail} with binary attachment `content` base64-encoded
186
172
  * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
@@ -204,4 +190,4 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
204
190
  * making any trust decision in the target function.
205
191
  */
206
192
  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 };
193
+ export { type DispatchToLunoraFunctionOptions, type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication, type InboundDispatch, type InboundDispatchContext, type InboundEmail, type InboundEmailHandler, type InboundEmailHandlerOptions, type InboundVerify, type RawInboundEmail, type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
@@ -1,3 +1,5 @@
1
+ import { D as DurableObjectJurisdiction, S as ShardNamespaceLike } from "../packem_shared/shard.d-CL2Lmliv.js";
2
+ export type { a as ShardStubLike } from "../packem_shared/shard.d-CL2Lmliv.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. */
@@ -68,28 +70,6 @@ interface InboundEmail {
68
70
  */
69
71
  declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
70
72
  /**
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
- }
92
- /**
93
73
  * Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
94
74
  * against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
95
75
  * members the handler touches are modelled, so the host can pass the real
@@ -181,6 +161,12 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
181
161
  /** `functionPath` of the target mutation/action (e.g. `"inbound:onEmail"`). */
182
162
  functionPath: string;
183
163
  /**
164
+ * Pin inbound dispatch to a Cloudflare data-residency jurisdiction. Pass the
165
+ * same value as the worker's `jurisdiction` so inbound mail routes to the
166
+ * jurisdiction-pinned shard. Omit for the un-pinned global namespace.
167
+ */
168
+ jurisdiction?: DurableObjectJurisdiction;
169
+ /**
184
170
  * Map the parsed message into the function's args. Defaults to passing the
185
171
  * whole {@link InboundEmail} with binary attachment `content` base64-encoded
186
172
  * (see {@link toJsonSafeEmail}) so it survives the JSON-serialised RPC body.
@@ -204,4 +190,4 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
204
190
  * making any trust decision in the target function.
205
191
  */
206
192
  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 };
193
+ export { type DispatchToLunoraFunctionOptions, type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication, type InboundDispatch, type InboundDispatchContext, type InboundEmail, type InboundEmailHandler, type InboundEmailHandlerOptions, type InboundVerify, type RawInboundEmail, type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
@@ -1,2 +1,2 @@
1
- export { createInboundEmailHandler, dispatchToLunoraFunction } from '../packem_shared/createInboundEmailHandler-Cd8dyzB7.mjs';
2
- export { parseInboundEmail } from '../packem_shared/parseInboundEmail-Bw9u_1oc.mjs';
1
+ export { createInboundEmailHandler, dispatchToLunoraFunction } from '../packem_shared/createInboundEmailHandler-BW_s0-Vc.mjs';
2
+ export { parseInboundEmail } from '../packem_shared/parseInboundEmail-6lafQBT1.mjs';
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-ChnhdPO2.mjs";
2
2
  export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-ChnhdPO2.mjs";
3
+ import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-CL2Lmliv.mjs";
3
4
  import { ReactElement } from 'react';
4
5
  /**
5
6
  * Sends a raw RFC 822 message through a Worker's Email send binding. The Workers
@@ -33,6 +34,23 @@ interface CloudflareTransportOptions {
33
34
  * verified-address constraint never bites the dev loop.
34
35
  */
35
36
  declare const createCloudflareTransport: (options: CloudflareTransportOptions) => MailTransport;
37
+ /**
38
+ * Create a mailer bound to a transport.
39
+ *
40
+ * SECURITY — recipient policy and HTML content are the caller's responsibility.
41
+ * The mailer fully blocks header/CRLF/comma injection in addresses
42
+ * (`assertSafeAddresses` / `assertSafeHeaderValue`), but it does NOT decide WHO
43
+ * you may send to or WHAT HTML you render.
44
+ *
45
+ * Open relay: derive `to`/`cc`/`bcc` from server-trusted state, never from raw
46
+ * request input, and prefer a fixed/allowlisted `from` — sending to an arbitrary
47
+ * user-supplied address turns your deployment into a spam relay.
48
+ *
49
+ * Template XSS / content injection: treat template HTML like any other HTML sink
50
+ * — never interpolate untrusted data into raw markup (or a
51
+ * `dangerouslySetInnerHTML`-style template) without escaping. The mailer sends
52
+ * whatever HTML you hand it verbatim.
53
+ */
36
54
  declare const createMailer: (options: LunoraMailOptions) => Mailer;
37
55
  /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
38
56
  type MailEnv = Record<string, unknown>;
@@ -40,6 +58,12 @@ type MailEnv = Record<string, unknown>;
40
58
  interface FromEnvOptions {
41
59
  /** RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare default transport). */
42
60
  cloudflareSend?: CloudflareSend;
61
+ /**
62
+ * Pin the captured-mail inbox shard to a Cloudflare data-residency
63
+ * jurisdiction. Pass the same value as the worker's `jurisdiction` so the
64
+ * dev inbox co-resides with app data. Omit for the un-pinned global namespace.
65
+ */
66
+ jurisdiction?: DurableObjectJurisdiction;
43
67
  /** Shard the captured-mail inbox lives on; override if your worker sets a custom `defaultShardKey`. */
44
68
  rootShard?: string;
45
69
  }
@@ -58,7 +82,7 @@ declare const shouldCaptureMail: (env: MailEnv) => boolean;
58
82
  * the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
59
83
  * never fails for lack of somewhere to record.
60
84
  */
61
- declare const createCaptureSink: (env: MailEnv, rootShard?: string) => MailboxSink;
85
+ declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction?: DurableObjectJurisdiction) => MailboxSink;
62
86
  /**
63
87
  * Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
64
88
  * captured into the studio's Mail inbox; otherwise it delivers via the supplied
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-ChnhdPO2.js";
2
2
  export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-ChnhdPO2.js";
3
+ import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-CL2Lmliv.js";
3
4
  import { ReactElement } from 'react';
4
5
  /**
5
6
  * Sends a raw RFC 822 message through a Worker's Email send binding. The Workers
@@ -33,6 +34,23 @@ interface CloudflareTransportOptions {
33
34
  * verified-address constraint never bites the dev loop.
34
35
  */
35
36
  declare const createCloudflareTransport: (options: CloudflareTransportOptions) => MailTransport;
37
+ /**
38
+ * Create a mailer bound to a transport.
39
+ *
40
+ * SECURITY — recipient policy and HTML content are the caller's responsibility.
41
+ * The mailer fully blocks header/CRLF/comma injection in addresses
42
+ * (`assertSafeAddresses` / `assertSafeHeaderValue`), but it does NOT decide WHO
43
+ * you may send to or WHAT HTML you render.
44
+ *
45
+ * Open relay: derive `to`/`cc`/`bcc` from server-trusted state, never from raw
46
+ * request input, and prefer a fixed/allowlisted `from` — sending to an arbitrary
47
+ * user-supplied address turns your deployment into a spam relay.
48
+ *
49
+ * Template XSS / content injection: treat template HTML like any other HTML sink
50
+ * — never interpolate untrusted data into raw markup (or a
51
+ * `dangerouslySetInnerHTML`-style template) without escaping. The mailer sends
52
+ * whatever HTML you hand it verbatim.
53
+ */
36
54
  declare const createMailer: (options: LunoraMailOptions) => Mailer;
37
55
  /** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
38
56
  type MailEnv = Record<string, unknown>;
@@ -40,6 +58,12 @@ type MailEnv = Record<string, unknown>;
40
58
  interface FromEnvOptions {
41
59
  /** RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare default transport). */
42
60
  cloudflareSend?: CloudflareSend;
61
+ /**
62
+ * Pin the captured-mail inbox shard to a Cloudflare data-residency
63
+ * jurisdiction. Pass the same value as the worker's `jurisdiction` so the
64
+ * dev inbox co-resides with app data. Omit for the un-pinned global namespace.
65
+ */
66
+ jurisdiction?: DurableObjectJurisdiction;
43
67
  /** Shard the captured-mail inbox lives on; override if your worker sets a custom `defaultShardKey`. */
44
68
  rootShard?: string;
45
69
  }
@@ -58,7 +82,7 @@ declare const shouldCaptureMail: (env: MailEnv) => boolean;
58
82
  * the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
59
83
  * never fails for lack of somewhere to record.
60
84
  */
61
- declare const createCaptureSink: (env: MailEnv, rootShard?: string) => MailboxSink;
85
+ declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction?: DurableObjectJurisdiction) => MailboxSink;
62
86
  /**
63
87
  * Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
64
88
  * captured into the studio's Mail inbox; otherwise it delivers via the supplied
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  export { createCaptureTransport } from './packem_shared/createCaptureTransport-Crz_8822.mjs';
2
- export { createCloudflareTransport } from './packem_shared/createCloudflareTransport-yHOVEsZv.mjs';
3
- export { default as createMailer } from './packem_shared/createMailer-CMVVTp0F.mjs';
4
- export { createCaptureSink, createMailerFromEnv, shouldCaptureMail } from './packem_shared/createCaptureSink-CNhqLMZ1.mjs';
5
- export { consumeQueuedSend, toQueuedPayload } from './packem_shared/toQueuedPayload-BEKOdaxU.mjs';
2
+ export { createCloudflareTransport } from './packem_shared/createCloudflareTransport-DXh1nUNi.mjs';
3
+ export { default as createMailer } from './packem_shared/createMailer-B4Z_Cgiu.mjs';
4
+ export { createCaptureSink, createMailerFromEnv, shouldCaptureMail } from './packem_shared/createCaptureSink-Di-5AJUs.mjs';
5
+ export { consumeQueuedSend, toQueuedPayload } from './packem_shared/consumeQueuedSend-B9hTDOZ6.mjs';
6
6
  export { default as renderEmail } from './packem_shared/renderEmail-hyS1bpVP.mjs';
7
- export { default as createResendTransport } from './packem_shared/createResendTransport-oNIorpzv.mjs';
7
+ export { default as createResendTransport } from './packem_shared/createResendTransport-CMQWjZoi.mjs';
@@ -1,22 +1,24 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const MAX_EMAIL_LENGTH = 320;
2
4
  const MAX_NAME_LENGTH = 256;
3
5
  const ADDRESS_PATTERN = /^([^<]*)<([^>]*)>\s*$/;
4
6
  const assertSafeAddressField = (field, value) => {
5
7
  if (value.includes("\r") || value.includes("\n") || value.includes(",")) {
6
- throw new Error(`@lunora/mail: address ${field} must not contain CR, LF, or comma`);
8
+ throw new LunoraError("INTERNAL", `@lunora/mail: address ${field} must not contain CR, LF, or comma`);
7
9
  }
8
10
  };
9
11
  const assertSafeHeaderValue = (label, value) => {
10
12
  if (value.includes("\r") || value.includes("\n")) {
11
- throw new Error(`@lunora/mail: ${label} must not contain CR or LF`);
13
+ throw new LunoraError("INTERNAL", `@lunora/mail: ${label} must not contain CR or LF`);
12
14
  }
13
15
  };
14
16
  const toBracketedAddress = (name, email) => {
15
17
  if (name.length > MAX_NAME_LENGTH) {
16
- throw new Error(`@lunora/mail: address name must be <= ${String(MAX_NAME_LENGTH)} characters`);
18
+ throw new LunoraError("INTERNAL", `@lunora/mail: address name must be <= ${String(MAX_NAME_LENGTH)} characters`);
17
19
  }
18
20
  if (email.length > MAX_EMAIL_LENGTH) {
19
- throw new Error(`@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
21
+ throw new LunoraError("INTERNAL", `@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
20
22
  }
21
23
  if (name) {
22
24
  assertSafeAddressField("name", name);
@@ -27,7 +29,7 @@ const toBracketedAddress = (name, email) => {
27
29
  const toBareAddress = (input) => {
28
30
  const email = input.trim();
29
31
  if (email.length > MAX_EMAIL_LENGTH) {
30
- throw new Error(`@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
32
+ throw new LunoraError("INTERNAL", `@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
31
33
  }
32
34
  assertSafeAddressField("email", email);
33
35
  return { email };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const toQueuedPayload = (options) => {
2
4
  return {
3
5
  bcc: options.bcc,
@@ -13,7 +15,7 @@ const toQueuedPayload = (options) => {
13
15
  };
14
16
  const consumeQueuedSend = async (mailer, payload) => {
15
17
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
16
- throw new Error("@lunora/mail: queue message body must be an object");
18
+ throw new LunoraError("INTERNAL", "@lunora/mail: queue message body must be an object");
17
19
  }
18
20
  const candidate = payload;
19
21
  if (typeof candidate.subject !== "string") {
@@ -22,7 +24,7 @@ const consumeQueuedSend = async (mailer, payload) => {
22
24
  const recipientIsString = typeof candidate.to === "string";
23
25
  const recipientIsStringArray = Array.isArray(candidate.to) && candidate.to.every((value) => typeof value === "string");
24
26
  if (!recipientIsString && !recipientIsStringArray) {
25
- throw new Error("@lunora/mail: queue message `to` must be a string or string[]");
27
+ throw new LunoraError("INTERNAL", "@lunora/mail: queue message `to` must be a string or string[]");
26
28
  }
27
29
  const assertOptionalString = (field, value) => {
28
30
  if (value === void 0) {
@@ -1,5 +1,7 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { createCaptureTransport } from './createCaptureTransport-Crz_8822.mjs';
2
- import createMailer from './createMailer-CMVVTp0F.mjs';
3
+ import createMailer from './createMailer-B4Z_Cgiu.mjs';
4
+ import { a as applyJurisdiction } from './shard-CJ-TvmfT.mjs';
3
5
 
4
6
  const RECORD_MAIL_OP = "__lunora_admin__:recordMail";
5
7
  const DEFAULT_ROOT_SHARD = "__root__";
@@ -8,7 +10,7 @@ const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "NODE_ENV", "WORKER_ENV"];
8
10
  const requireStringEnv = (env, name) => {
9
11
  const value = env[name];
10
12
  if (typeof value !== "string" || value === "") {
11
- throw new Error(`@lunora/mail: missing env var \`${name}\` — set it in .dev.vars (and \`wrangler secret put ${name}\` for secrets).`);
13
+ throw new LunoraError("INTERNAL", `@lunora/mail: missing env var \`${name}\` — set it in .dev.vars (and \`wrangler secret put ${name}\` for secrets).`);
12
14
  }
13
15
  return value;
14
16
  };
@@ -22,14 +24,15 @@ const shouldCaptureMail = (env) => {
22
24
  return typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value);
23
25
  });
24
26
  };
25
- const createCaptureSink = (env, rootShard = DEFAULT_ROOT_SHARD) => {
27
+ const createCaptureSink = (env, rootShard = DEFAULT_ROOT_SHARD, jurisdiction) => {
26
28
  return {
27
29
  record: async (mail) => {
28
- const namespace = env["SHARD"];
30
+ const binding = env["SHARD"];
29
31
  const adminToken = typeof env["LUNORA_ADMIN_TOKEN"] === "string" ? env["LUNORA_ADMIN_TOKEN"] : void 0;
30
- if (namespace === void 0 || adminToken === void 0) {
32
+ if (binding === void 0 || adminToken === void 0) {
31
33
  return { id: "uncaptured" };
32
34
  }
35
+ const namespace = applyJurisdiction(binding, jurisdiction);
33
36
  const stub = namespace.get(namespace.idFromName(rootShard));
34
37
  const response = await stub.fetch("https://shard.internal/rpc", {
35
38
  body: JSON.stringify({ args: mail, functionPath: RECORD_MAIL_OP }),
@@ -44,7 +47,7 @@ const createCaptureSink = (env, rootShard = DEFAULT_ROOT_SHARD) => {
44
47
  const createMailerFromEnv = (env, options = {}) => {
45
48
  const from = requireStringEnv(env, "MAIL_FROM");
46
49
  if (shouldCaptureMail(env)) {
47
- return createMailer({ from, transport: createCaptureTransport(createCaptureSink(env, options.rootShard)) });
50
+ return createMailer({ from, transport: createCaptureTransport(createCaptureSink(env, options.rootShard, options.jurisdiction)) });
48
51
  }
49
52
  if (options.cloudflareSend) {
50
53
  return createMailer({ cloudflareSend: options.cloudflareSend, from });
@@ -53,7 +56,8 @@ const createMailerFromEnv = (env, options = {}) => {
53
56
  if (apiKey !== void 0 && apiKey !== "") {
54
57
  return createMailer({ apiKey, from });
55
58
  }
56
- throw new Error(
59
+ throw new LunoraError(
60
+ "INTERNAL",
57
61
  "@lunora/mail: no transport configured — provide `cloudflareSend` (a SEND_EMAIL binding) or RESEND_API_KEY, or run in a dev environment to capture."
58
62
  );
59
63
  };
@@ -1,5 +1,6 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { cloudflareEmailProvider } from '@visulima/email/providers/cloudflare-email';
2
- import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C5CVbjRF.mjs';
3
+ import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C0xHi3oy.mjs';
3
4
 
4
5
  const createCloudflareTransport = (options) => {
5
6
  const provider = cloudflareEmailProvider({ send: options.send });
@@ -9,11 +10,12 @@ const createCloudflareTransport = (options) => {
9
10
  const hasCc = payload.cc !== void 0 && payload.cc.length > 0;
10
11
  const hasBcc = payload.bcc !== void 0 && payload.bcc.length > 0;
11
12
  if (hasCc || hasBcc) {
12
- throw new Error("@lunora/mail: Cloudflare Email Workers does not support cc/bcc — fan out one send per recipient instead");
13
+ throw new LunoraError("INTERNAL", "@lunora/mail: Cloudflare Email Workers does not support cc/bcc — fan out one send per recipient instead");
13
14
  }
14
15
  const { first, list } = requireRecipients(payload.to);
15
16
  if (list.length > 1) {
16
- throw new Error(
17
+ throw new LunoraError(
18
+ "INTERNAL",
17
19
  `@lunora/mail: Cloudflare Email Workers is single-recipient but received ${String(list.length)} \`to\` addresses — fan out one send per recipient instead`
18
20
  );
19
21
  }
@@ -1,3 +1,6 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { a as applyJurisdiction } from './shard-CJ-TvmfT.mjs';
3
+
1
4
  const GENERIC_REJECT_REASON = "message could not be processed";
2
5
  const rejectOnError = (error, context) => {
3
6
  console.error("@lunora/mail/inbound: dropping message —", error);
@@ -12,7 +15,7 @@ const createInboundEmailHandler = (options) => {
12
15
  if (options.verify) {
13
16
  const verified = await options.verify(parsed, context);
14
17
  if (verified === false) {
15
- throw new Error("@lunora/mail/inbound: sender verification rejected the message");
18
+ throw new LunoraError("INTERNAL", "@lunora/mail/inbound: sender verification rejected the message");
16
19
  }
17
20
  }
18
21
  await options.dispatch(parsed, context);
@@ -51,27 +54,28 @@ const dispatchToLunoraFunction = (options) => {
51
54
  return async (email, context) => {
52
55
  const adminToken = options.adminToken ?? (typeof context.env["LUNORA_ADMIN_TOKEN"] === "string" ? context.env["LUNORA_ADMIN_TOKEN"] : void 0);
53
56
  if (adminToken === void 0 || adminToken === "") {
54
- throw new Error("@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");
57
+ throw new LunoraError("INTERNAL", "@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");
55
58
  }
56
59
  const envelope = {
57
60
  args: resolveArgs(email, context),
58
61
  functionPath: options.functionPath,
59
62
  shardKey
60
63
  };
61
- const stub = options.shard.get(options.shard.idFromName(shardKey));
64
+ const namespace = applyJurisdiction(options.shard, options.jurisdiction);
65
+ const stub = namespace.get(namespace.idFromName(shardKey));
62
66
  const response = await stub.fetch("https://shard.internal/rpc", {
63
67
  body: JSON.stringify(envelope),
64
68
  headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
65
69
  method: "POST"
66
70
  });
67
71
  if (response.ok === false) {
68
- throw new Error(`@lunora/mail/inbound: dispatch to \`${options.functionPath}\` failed (HTTP ${String(response.status ?? "?")}).`);
72
+ throw new LunoraError("INTERNAL", `@lunora/mail/inbound: dispatch to \`${options.functionPath}\` failed (HTTP ${String(response.status ?? "?")}).`);
69
73
  }
70
74
  const body = await response.json();
71
75
  if (typeof body === "object" && body !== null && "error" in body) {
72
76
  const { error } = body;
73
77
  if (error !== void 0 && error !== null) {
74
- throw new Error(`@lunora/mail/inbound: dispatch to \`${options.functionPath}\` returned an error: ${JSON.stringify(error)}`);
78
+ throw new LunoraError("INTERNAL", `@lunora/mail/inbound: dispatch to \`${options.functionPath}\` returned an error: ${JSON.stringify(error)}`);
75
79
  }
76
80
  }
77
81
  };
@@ -1,9 +1,10 @@
1
- import { a as assertSafeHeaderValue, b as assertSafeAddresses } from './address-fkXxLKza.mjs';
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { a as assertSafeHeaderValue, b as assertSafeAddresses } from './address-vSUAVU2T.mjs';
2
3
  import { isCaptureTransport } from './createCaptureTransport-Crz_8822.mjs';
3
- import { createCloudflareTransport } from './createCloudflareTransport-yHOVEsZv.mjs';
4
- import { toQueuedPayload } from './toQueuedPayload-BEKOdaxU.mjs';
4
+ import { createCloudflareTransport } from './createCloudflareTransport-DXh1nUNi.mjs';
5
+ import { toQueuedPayload } from './consumeQueuedSend-B9hTDOZ6.mjs';
5
6
  import renderEmail from './renderEmail-hyS1bpVP.mjs';
6
- import createResendTransport from './createResendTransport-oNIorpzv.mjs';
7
+ import createResendTransport from './createResendTransport-CMQWjZoi.mjs';
7
8
 
8
9
  const buildDefaultTransport = (options) => {
9
10
  if (options.cloudflareSend) {
@@ -12,11 +13,14 @@ const buildDefaultTransport = (options) => {
12
13
  if (options.apiKey) {
13
14
  return createResendTransport(options.apiKey, options.from);
14
15
  }
15
- throw new Error("@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)");
16
+ throw new LunoraError(
17
+ "INTERNAL",
18
+ "@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)"
19
+ );
16
20
  };
17
21
  const createMailer = (options) => {
18
22
  if (!options.from) {
19
- throw new Error("@lunora/mail: `from` is required");
23
+ throw new LunoraError("INTERNAL", "@lunora/mail: `from` is required");
20
24
  }
21
25
  const transport = options.transport ?? buildDefaultTransport(options);
22
26
  const buildPayload = async (options_) => {
@@ -65,7 +69,7 @@ const createMailer = (options) => {
65
69
  await transport.send(captured);
66
70
  return { queued: true };
67
71
  }
68
- throw new Error("@lunora/mail: `queue` binding is required for mailer.queue()");
72
+ throw new LunoraError("INTERNAL", "@lunora/mail: `queue` binding is required for mailer.queue()");
69
73
  }
70
74
  const payload = await buildPayload(options_);
71
75
  await options.queue.send(toQueuedPayload(payload));
@@ -1,5 +1,5 @@
1
1
  import { resendProvider } from '@visulima/email/providers/resend';
2
- import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C5CVbjRF.mjs';
2
+ import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C0xHi3oy.mjs';
3
3
 
4
4
  const createResendTransport = (apiKey, defaultFrom) => {
5
5
  const provider = resendProvider({ apiKey });
@@ -1,5 +1,5 @@
1
1
  import PostalMime from 'postal-mime';
2
- import { a as assertSafeHeaderValue } from './address-fkXxLKza.mjs';
2
+ import { a as assertSafeHeaderValue } from './address-vSUAVU2T.mjs';
3
3
 
4
4
  const safe = (label, value) => {
5
5
  if (value === void 0) {
@@ -1,4 +1,5 @@
1
- import { t as toAddressList, c as toAddress } from './address-fkXxLKza.mjs';
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { t as toAddressList, c as toAddress } from './address-vSUAVU2T.mjs';
2
3
 
3
4
  const reasonOf = (rawError) => {
4
5
  if (rawError instanceof Error) {
@@ -19,7 +20,7 @@ const requireRecipients = (to) => {
19
20
  const list = toAddressList(to);
20
21
  const [first] = list ?? [];
21
22
  if (!list || first === void 0) {
22
- throw new Error("@lunora/mail: at least one recipient is required");
23
+ throw new LunoraError("INTERNAL", "@lunora/mail: at least one recipient is required");
23
24
  }
24
25
  return { first, list };
25
26
  };
@@ -39,7 +40,7 @@ const toProviderEmail = (payload, defaultFrom, to) => {
39
40
  const interpretSendResult = (result) => {
40
41
  if (!result.success || !result.data) {
41
42
  console.error(`@lunora/mail: send failed: ${reasonOf(result.error)}`);
42
- throw new Error("@lunora/mail: send failed");
43
+ throw new LunoraError("INTERNAL", "@lunora/mail: send failed");
43
44
  }
44
45
  return { id: result.data.messageId };
45
46
  };
@@ -0,0 +1,13 @@
1
+ const applyJurisdiction = (namespace, jurisdiction) => {
2
+ if (jurisdiction === void 0) {
3
+ return namespace;
4
+ }
5
+ if (typeof namespace.jurisdiction !== "function") {
6
+ throw new TypeError(
7
+ `@lunora/mail: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
8
+ );
9
+ }
10
+ return namespace.jurisdiction(jurisdiction);
11
+ };
12
+
13
+ export { applyJurisdiction as a };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Structural projections of the `SHARD` Durable Object namespace + one shard
3
+ * stub, shared by the inbound dispatcher. Mirrors the shapes the outbound dev
4
+ * capture sink uses (`packages/mail/src/from-env.ts`) so inbound dispatch routes
5
+ * a parsed message into a Lunora function over the exact same admin-RPC-over-shard
6
+ * path — without importing any Cloudflare types into `@lunora/mail`.
7
+ */
8
+ /** Structural projection of one shard stub — only `fetch` returning something with `.json()`. */
9
+ interface ShardStubLike {
10
+ fetch: (input: string, init?: {
11
+ body?: string;
12
+ headers?: Record<string, string>;
13
+ method?: string;
14
+ }) => Promise<{
15
+ json: () => Promise<unknown>;
16
+ }>;
17
+ }
18
+ /**
19
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
20
+ * Cloudflare adds values over time.
21
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
22
+ */
23
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
24
+ /** Structural projection of the `SHARD` Durable Object namespace. */
25
+ interface ShardNamespaceLike {
26
+ get: (id: unknown) => ShardStubLike;
27
+ idFromName: (name: string) => unknown;
28
+ /**
29
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
30
+ * workers-types releases (and test doubles) may not expose it.
31
+ */
32
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
33
+ }
34
+ /**
35
+ * Return a jurisdiction-restricted view of `namespace`, or `namespace`
36
+ * unchanged when no jurisdiction is configured. Fail-closed when the binding
37
+ * lacks `.jurisdiction()` so a residency constraint is never silently dropped.
38
+ */
39
+ export { DurableObjectJurisdiction as D, ShardNamespaceLike as S, ShardStubLike as a };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Structural projections of the `SHARD` Durable Object namespace + one shard
3
+ * stub, shared by the inbound dispatcher. Mirrors the shapes the outbound dev
4
+ * capture sink uses (`packages/mail/src/from-env.ts`) so inbound dispatch routes
5
+ * a parsed message into a Lunora function over the exact same admin-RPC-over-shard
6
+ * path — without importing any Cloudflare types into `@lunora/mail`.
7
+ */
8
+ /** Structural projection of one shard stub — only `fetch` returning something with `.json()`. */
9
+ interface ShardStubLike {
10
+ fetch: (input: string, init?: {
11
+ body?: string;
12
+ headers?: Record<string, string>;
13
+ method?: string;
14
+ }) => Promise<{
15
+ json: () => Promise<unknown>;
16
+ }>;
17
+ }
18
+ /**
19
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
20
+ * Cloudflare adds values over time.
21
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
22
+ */
23
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
24
+ /** Structural projection of the `SHARD` Durable Object namespace. */
25
+ interface ShardNamespaceLike {
26
+ get: (id: unknown) => ShardStubLike;
27
+ idFromName: (name: string) => unknown;
28
+ /**
29
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
30
+ * workers-types releases (and test doubles) may not expose it.
31
+ */
32
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
33
+ }
34
+ /**
35
+ * Return a jurisdiction-restricted view of `namespace`, or `namespace`
36
+ * unchanged when no jurisdiction is configured. Fail-closed when the binding
37
+ * lacks `.jurisdiction()` so a residency constraint is never silently dropped.
38
+ */
39
+ export { DurableObjectJurisdiction as D, ShardNamespaceLike as S, ShardStubLike as a };
package/dist/testing.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const GET_CAPTURED_MAIL_OP = "__lunora_admin__:getCapturedMail";
2
4
  const DEFAULT_RPC_PATH = "/_lunora/rpc";
3
5
  const TRAILING_SLASH = /\/$/;
@@ -14,7 +16,7 @@ const listCapturedMail = async (options) => {
14
16
  method: "POST"
15
17
  });
16
18
  if (!response.ok) {
17
- throw new Error(`@lunora/mail/testing: getCapturedMail failed (HTTP ${String(response.status)})`);
19
+ throw new LunoraError("INTERNAL", `@lunora/mail/testing: getCapturedMail failed (HTTP ${String(response.status)})`);
18
20
  }
19
21
  const body = await response.json();
20
22
  return body.result?.entries ?? [];
@@ -32,7 +34,8 @@ const waitForMail = async (options) => {
32
34
  return match;
33
35
  }
34
36
  if (Date.now() >= deadline) {
35
- throw new Error(
37
+ throw new LunoraError(
38
+ "INTERNAL",
36
39
  `@lunora/mail/testing: no mail to "${options.to}"${options.subjectMatch === void 0 ? "" : ` matching "${options.subjectMatch}"`} within ${String(timeoutMs)}ms`
37
40
  );
38
41
  }
@@ -51,7 +54,10 @@ const extractLink = (mail, options = {}) => {
51
54
  return link;
52
55
  }
53
56
  }
54
- throw new Error(`@lunora/mail/testing: no link${options.match === void 0 ? "" : ` containing "${options.match}"`} found in the captured message`);
57
+ throw new LunoraError(
58
+ "INTERNAL",
59
+ `@lunora/mail/testing: no link${options.match === void 0 ? "" : ` containing "${options.match}"`} found in the captured message`
60
+ );
55
61
  };
56
62
 
57
63
  export { extractLink, listCapturedMail, waitForMail };