@every-app/sdk 0.2.0 → 0.3.0-rc.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.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -3
  3. package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
  4. package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
  5. package/dist/cloudflare/catalogSettings.generated.js +174 -0
  6. package/dist/cloudflare/index.d.ts +23 -0
  7. package/dist/cloudflare/index.d.ts.map +1 -0
  8. package/dist/cloudflare/index.js +24 -0
  9. package/dist/cloudflare/sentinel.d.ts +127 -0
  10. package/dist/cloudflare/sentinel.d.ts.map +1 -0
  11. package/dist/cloudflare/sentinel.js +85 -0
  12. package/dist/cloudflare/worker.d.ts +805 -0
  13. package/dist/cloudflare/worker.d.ts.map +1 -0
  14. package/dist/cloudflare/worker.js +997 -0
  15. package/dist/dev/index.d.ts +8 -0
  16. package/dist/dev/index.d.ts.map +1 -0
  17. package/dist/dev/index.js +7 -0
  18. package/dist/dev/keypair.d.ts +26 -0
  19. package/dist/dev/keypair.d.ts.map +1 -0
  20. package/dist/dev/keypair.js +62 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +5 -0
  24. package/dist/internal/aiDefault.d.ts +204 -0
  25. package/dist/internal/aiDefault.d.ts.map +1 -0
  26. package/dist/internal/aiDefault.js +334 -0
  27. package/dist/internal/aiProviders.d.ts +72 -0
  28. package/dist/internal/aiProviders.d.ts.map +1 -0
  29. package/dist/internal/aiProviders.js +233 -0
  30. package/dist/internal/appSettings.d.ts +315 -0
  31. package/dist/internal/appSettings.d.ts.map +1 -0
  32. package/dist/internal/appSettings.js +549 -0
  33. package/dist/internal/audit.d.ts +108 -0
  34. package/dist/internal/audit.d.ts.map +1 -0
  35. package/dist/internal/audit.js +235 -0
  36. package/dist/internal/capabilities.d.ts +3174 -0
  37. package/dist/internal/capabilities.d.ts.map +1 -0
  38. package/dist/internal/capabilities.js +1110 -0
  39. package/dist/internal/catalog/commerce.d.ts +394 -0
  40. package/dist/internal/catalog/commerce.d.ts.map +1 -0
  41. package/dist/internal/catalog/commerce.js +477 -0
  42. package/dist/internal/catalog/comms.d.ts +375 -0
  43. package/dist/internal/catalog/comms.d.ts.map +1 -0
  44. package/dist/internal/catalog/comms.js +520 -0
  45. package/dist/internal/catalog/index.d.ts +1973 -0
  46. package/dist/internal/catalog/index.d.ts.map +1 -0
  47. package/dist/internal/catalog/index.js +19 -0
  48. package/dist/internal/catalog/inference.d.ts +393 -0
  49. package/dist/internal/catalog/inference.d.ts.map +1 -0
  50. package/dist/internal/catalog/inference.js +460 -0
  51. package/dist/internal/catalog/infra.d.ts +423 -0
  52. package/dist/internal/catalog/infra.d.ts.map +1 -0
  53. package/dist/internal/catalog/infra.js +543 -0
  54. package/dist/internal/catalog/search.d.ts +400 -0
  55. package/dist/internal/catalog/search.d.ts.map +1 -0
  56. package/dist/internal/catalog/search.js +463 -0
  57. package/dist/internal/devProviders.d.ts +49 -0
  58. package/dist/internal/devProviders.d.ts.map +1 -0
  59. package/dist/internal/devProviders.js +80 -0
  60. package/dist/internal/email.d.ts +280 -0
  61. package/dist/internal/email.d.ts.map +1 -0
  62. package/dist/internal/email.js +613 -0
  63. package/dist/internal/events.d.ts +193 -0
  64. package/dist/internal/events.d.ts.map +1 -0
  65. package/dist/internal/events.js +367 -0
  66. package/dist/internal/gatewaySettings.d.ts +83 -0
  67. package/dist/internal/gatewaySettings.d.ts.map +1 -0
  68. package/dist/internal/gatewaySettings.js +215 -0
  69. package/dist/internal/identity.d.ts +57 -5
  70. package/dist/internal/identity.d.ts.map +1 -1
  71. package/dist/internal/identity.js +99 -22
  72. package/dist/internal/index.d.ts +22 -1
  73. package/dist/internal/index.d.ts.map +1 -1
  74. package/dist/internal/index.js +22 -1
  75. package/dist/internal/members.d.ts +36 -0
  76. package/dist/internal/members.d.ts.map +1 -0
  77. package/dist/internal/members.js +7 -0
  78. package/dist/internal/oauthClient.d.ts +61 -0
  79. package/dist/internal/oauthClient.d.ts.map +1 -0
  80. package/dist/internal/oauthClient.js +133 -0
  81. package/dist/internal/oauthPresets.d.ts +33 -0
  82. package/dist/internal/oauthPresets.d.ts.map +1 -0
  83. package/dist/internal/oauthPresets.js +1064 -0
  84. package/dist/internal/oauthScopes.d.ts +172 -0
  85. package/dist/internal/oauthScopes.d.ts.map +1 -0
  86. package/dist/internal/oauthScopes.js +510 -0
  87. package/dist/internal/oauthSpec.d.ts +102 -0
  88. package/dist/internal/oauthSpec.d.ts.map +1 -0
  89. package/dist/internal/oauthSpec.js +789 -0
  90. package/dist/internal/organizationName.d.ts +18 -0
  91. package/dist/internal/organizationName.d.ts.map +1 -0
  92. package/dist/internal/organizationName.js +24 -0
  93. package/dist/internal/protocol.d.ts +150 -3
  94. package/dist/internal/protocol.d.ts.map +1 -1
  95. package/dist/internal/protocol.js +163 -2
  96. package/dist/internal/providerApps.d.ts +75 -0
  97. package/dist/internal/providerApps.d.ts.map +1 -0
  98. package/dist/internal/providerApps.js +59 -0
  99. package/dist/internal/providerRegistry.d.ts +218 -0
  100. package/dist/internal/providerRegistry.d.ts.map +1 -0
  101. package/dist/internal/providerRegistry.js +432 -0
  102. package/dist/internal/providerSpec.d.ts +66 -0
  103. package/dist/internal/providerSpec.d.ts.map +1 -0
  104. package/dist/internal/providerSpec.js +1154 -0
  105. package/dist/internal/remoteMcp.d.ts +188 -0
  106. package/dist/internal/remoteMcp.d.ts.map +1 -0
  107. package/dist/internal/remoteMcp.js +543 -0
  108. package/dist/internal/signInProviders.d.ts +104 -0
  109. package/dist/internal/signInProviders.d.ts.map +1 -0
  110. package/dist/internal/signInProviders.js +245 -0
  111. package/dist/internal/tools.d.ts +115 -0
  112. package/dist/internal/tools.d.ts.map +1 -0
  113. package/dist/internal/tools.js +80 -0
  114. package/dist/manifest.d.ts +238 -0
  115. package/dist/manifest.d.ts.map +1 -0
  116. package/dist/manifest.js +30 -0
  117. package/dist/server/ai.d.ts +111 -0
  118. package/dist/server/ai.d.ts.map +1 -0
  119. package/dist/server/ai.js +145 -0
  120. package/dist/server/aiBinding.d.ts +68 -0
  121. package/dist/server/aiBinding.d.ts.map +1 -0
  122. package/dist/server/aiBinding.js +294 -0
  123. package/dist/server/attribution.d.ts +43 -0
  124. package/dist/server/attribution.d.ts.map +1 -0
  125. package/dist/server/attribution.js +75 -0
  126. package/dist/server/authorizeUrl.d.ts +43 -0
  127. package/dist/server/authorizeUrl.d.ts.map +1 -0
  128. package/dist/server/authorizeUrl.js +106 -0
  129. package/dist/server/connectUrl.d.ts +21 -0
  130. package/dist/server/connectUrl.d.ts.map +1 -0
  131. package/dist/server/connectUrl.js +37 -0
  132. package/dist/server/context.d.ts +68 -0
  133. package/dist/server/context.d.ts.map +1 -0
  134. package/dist/server/context.js +117 -0
  135. package/dist/server/emailBinding.d.ts +72 -0
  136. package/dist/server/emailBinding.d.ts.map +1 -0
  137. package/dist/server/emailBinding.js +124 -0
  138. package/dist/server/emit.d.ts +63 -0
  139. package/dist/server/emit.d.ts.map +1 -0
  140. package/dist/server/emit.js +101 -0
  141. package/dist/server/env.d.ts +44 -0
  142. package/dist/server/env.d.ts.map +1 -0
  143. package/dist/server/env.js +1 -0
  144. package/dist/server/everyApp.d.ts +65 -19
  145. package/dist/server/everyApp.d.ts.map +1 -1
  146. package/dist/server/everyApp.js +195 -30
  147. package/dist/server/fromMcpTools.d.ts +89 -0
  148. package/dist/server/fromMcpTools.d.ts.map +1 -0
  149. package/dist/server/fromMcpTools.js +192 -0
  150. package/dist/server/gatewayFetch.d.ts +54 -14
  151. package/dist/server/gatewayFetch.d.ts.map +1 -1
  152. package/dist/server/gatewayFetch.js +201 -37
  153. package/dist/server/gatewayUrl.d.ts +13 -0
  154. package/dist/server/gatewayUrl.d.ts.map +1 -0
  155. package/dist/server/gatewayUrl.js +34 -0
  156. package/dist/server/index.d.ts +23 -3
  157. package/dist/server/index.d.ts.map +1 -1
  158. package/dist/server/index.js +19 -2
  159. package/dist/server/integrationFetch.d.ts +162 -0
  160. package/dist/server/integrationFetch.d.ts.map +1 -0
  161. package/dist/server/integrationFetch.js +688 -0
  162. package/dist/server/listMembers.d.ts +32 -0
  163. package/dist/server/listMembers.d.ts.map +1 -0
  164. package/dist/server/listMembers.js +71 -0
  165. package/dist/server/nodeServer.d.ts +48 -0
  166. package/dist/server/nodeServer.d.ts.map +1 -0
  167. package/dist/server/nodeServer.js +45 -0
  168. package/dist/server/refusal.d.ts +55 -0
  169. package/dist/server/refusal.d.ts.map +1 -0
  170. package/dist/server/refusal.js +193 -0
  171. package/dist/server/sendEmail.d.ts +39 -0
  172. package/dist/server/sendEmail.d.ts.map +1 -0
  173. package/dist/server/sendEmail.js +114 -0
  174. package/dist/server/tools.d.ts +184 -0
  175. package/dist/server/tools.d.ts.map +1 -0
  176. package/dist/server/tools.js +347 -0
  177. package/dist/server/toolsEndpoint.d.ts +32 -0
  178. package/dist/server/toolsEndpoint.d.ts.map +1 -0
  179. package/dist/server/toolsEndpoint.js +237 -0
  180. package/dist/tanstack-start/index.d.ts +9 -0
  181. package/dist/tanstack-start/index.d.ts.map +1 -0
  182. package/dist/tanstack-start/index.js +29 -0
  183. package/dist/testing/index.d.ts +93 -0
  184. package/dist/testing/index.d.ts.map +1 -0
  185. package/dist/testing/index.js +156 -0
  186. package/package.json +42 -8
  187. package/dist/server/mcp.d.ts +0 -66
  188. package/dist/server/mcp.d.ts.map +0 -1
  189. package/dist/server/mcp.js +0 -147
@@ -0,0 +1,613 @@
1
+ /**
2
+ * The `email` capability's wire contract (docs/capabilities.md "Gateway
3
+ * services"): what an app sends to `POST /api/integrations/email/send`,
4
+ * how the request is checked, which senders and inbound addresses an
5
+ * installation may hold (`everyapp email set`), which provider a gateway
6
+ * sends with (Cloudflare Email Sending or Resend), and how the provider's
7
+ * answers map onto the gateway's refusals.
8
+ *
9
+ * The gateway enforces these, the CLI's `email set` checks the lists with
10
+ * the same validator, and the SDK's local-dev fallback applies the same
11
+ * checks and the same REST body, so a refusal in `everyapp dev` is the one
12
+ * production gives. Pure functions only: no env, no I/O.
13
+ */
14
+ /**
15
+ * The providers a gateway sends mail with, in the order it prefers them
16
+ * (docs/runbooks/email-sending-setup.md, "Choosing a provider"). One serves
17
+ * the whole gateway: its own mail and every app's.
18
+ */
19
+ export const EMAIL_PROVIDER_IDS = ["cloudflare", "resend"];
20
+ /** Each provider's name, as messages give it. */
21
+ export const EMAIL_PROVIDER_LABELS = {
22
+ cloudflare: "Cloudflare Email Sending",
23
+ resend: "Resend",
24
+ };
25
+ /**
26
+ * Which provider sends. EMAIL_PROVIDER (trimmed, any case), when set, names
27
+ * it, and it must be set up. Without it: the first provider, in
28
+ * {@link EMAIL_PROVIDER_IDS} order, the operator gave settings to, else one
29
+ * a binding alone sets up. The EMAIL binding is declared on every gateway
30
+ * Worker, so it can't be what makes Cloudflare win over a RESEND_API_KEY
31
+ * the operator set.
32
+ */
33
+ export function chooseEmailProvider(emailProvider, setup) {
34
+ const requested = typeof emailProvider === "string" ? emailProvider.trim() : "";
35
+ if (requested) {
36
+ const provider = EMAIL_PROVIDER_IDS.find((id) => id === requested.toLowerCase());
37
+ if (!provider)
38
+ return { ok: false, error: "unknown_provider", requested };
39
+ return setup(provider)
40
+ ? { ok: true, provider }
41
+ : { ok: false, error: "not_configured", requested: provider };
42
+ }
43
+ const provider = EMAIL_PROVIDER_IDS.find((id) => setup(id) === "settings") ??
44
+ EMAIL_PROVIDER_IDS.find((id) => setup(id) !== null);
45
+ return provider
46
+ ? { ok: true, provider }
47
+ : { ok: false, error: "not_configured", requested: null };
48
+ }
49
+ /** Cloudflare Email Sending's limits the gateway checks before sending, and the lists an installation keeps on the gateway. */
50
+ export const EMAIL_LIMITS = {
51
+ /** The request body, read with a counting stream. */
52
+ bodyBytes: 5 * 1024 * 1024,
53
+ recipients: 50,
54
+ subjectChars: 998,
55
+ headers: 20,
56
+ headerValueBytes: 2048,
57
+ headerNameBytes: 100,
58
+ headersTotalBytes: 16 * 1024,
59
+ attachments: 32,
60
+ /** Sender patterns an installation may hold (`everyapp email set --sender`). */
61
+ senders: 20,
62
+ /** Inbound addresses an installation may hold (Email Routing allows 200 rules a domain). */
63
+ inbound: 50,
64
+ /** Email Routing's limit on a rule's address (its matcher's value). */
65
+ inboundAddressChars: 90,
66
+ };
67
+ /**
68
+ * A sender: `*@<domain>` (any address on exactly that domain) or
69
+ * `<local>@<domain>`, in lowercase. Set per installed app with
70
+ * `everyapp email set --sender`; the gateway reads them from its registry.
71
+ */
72
+ export const EMAIL_SENDER_PATTERN = /^(\*|[a-z0-9._+-]+)@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z][a-z0-9-]*[a-z0-9]$/;
73
+ /**
74
+ * An inbound address Email Routing delivers to the app's Worker: `*@<domain>`,
75
+ * the catch-all of a zone's apex, or one address, in lowercase. The CLI finds
76
+ * the zone; a catch-all on a subdomain is refused there.
77
+ */
78
+ export const EMAIL_INBOUND_PATTERN = /^(\*|[a-z0-9._+-]+)@([a-z0-9-]+\.)+[a-z]{2,}$/;
79
+ /** The command that sets an installation's senders and inbound addresses, for messages. */
80
+ export const EMAIL_SET_COMMAND = "everyapp email set";
81
+ /**
82
+ * Why `values` can't be an installation's senders or inbound addresses
83
+ * (`everyapp email set`), one sentence each; empty when they can. The
84
+ * rules the manifest's `capabilities.email.senders` and `email.inbound`
85
+ * had: the patterns above, no duplicates, at most EMAIL_LIMITS.senders or
86
+ * EMAIL_LIMITS.inbound entries, an inbound address at most
87
+ * EMAIL_LIMITS.inboundAddressChars long.
88
+ */
89
+ export function emailListProblems(kind, values) {
90
+ const problems = [];
91
+ const limit = kind === "senders" ? EMAIL_LIMITS.senders : EMAIL_LIMITS.inbound;
92
+ if (values.length > limit) {
93
+ problems.push(`At most ${limit} ${kind === "senders" ? "senders" : "inbound addresses"}, not ${values.length}.`);
94
+ }
95
+ for (const value of values) {
96
+ if (kind === "inbound" && value.length > EMAIL_LIMITS.inboundAddressChars) {
97
+ problems.push(`${value} is longer than Email Routing's ${EMAIL_LIMITS.inboundAddressChars} characters.`);
98
+ }
99
+ else if (!(kind === "senders" ? EMAIL_SENDER_PATTERN : EMAIL_INBOUND_PATTERN).test(value)) {
100
+ problems.push(kind === "senders"
101
+ ? `"${value}" isn't a sender: a sender is "*@<domain>" or "<local>@<domain>", in lowercase.`
102
+ : `"${value}" isn't an inbound address: an inbound address is "*@<domain>" or "<local>@<domain>", in lowercase.`);
103
+ }
104
+ }
105
+ for (const twice of new Set(values.filter((value, index) => values.indexOf(value) !== index))) {
106
+ problems.push(`${twice} is listed twice.`);
107
+ }
108
+ return problems;
109
+ }
110
+ /** A list as the row stores it: JSON, or null for none. */
111
+ export function storedEmailList(values) {
112
+ return values.length === 0 ? null : JSON.stringify(values);
113
+ }
114
+ /**
115
+ * The 403 `email_sender_not_allowed` sentence. `where` says what sets the
116
+ * senders: the gateway's registry row, or `.dev.vars` in local dev.
117
+ */
118
+ export function emailSenderRefusal(address, where) {
119
+ const domain = `*@${emailDomain(address)}`;
120
+ return where === "gateway"
121
+ ? `This app may not send as ${address}: list it, or ${domain}, among the app's senders with \`${EMAIL_SET_COMMAND} --sender\` in the app's directory.`
122
+ : `This app may not send as ${address}: in dev, list it, or ${domain}, in EVERYAPP_EMAIL_SENDERS in .dev.vars; deployed, \`${EMAIL_SET_COMMAND} --sender\` sets the app's senders.`;
123
+ }
124
+ /** An address the provider will take: printable ASCII, one `@`, a dotted domain. */
125
+ const ADDRESS = /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/;
126
+ const MAX_ADDRESS_LENGTH = 254;
127
+ const MAX_NAME_LENGTH = 256;
128
+ const MAX_FILENAME_LENGTH = 255;
129
+ const LINE_BREAK = /[\r\n]/;
130
+ const CONTROL = /[\u0000-\u001f\u007f]/;
131
+ const HEADER_NAME = /^[A-Za-z0-9-]+$/;
132
+ const MIME_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
133
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
134
+ /**
135
+ * Headers the message's own fields set, or the transport does: an app may
136
+ * not set them as headers (`From` would get past the sender rule).
137
+ */
138
+ const RESERVED_HEADERS = new Set([
139
+ "from",
140
+ "sender",
141
+ "to",
142
+ "cc",
143
+ "bcc",
144
+ "reply-to",
145
+ "subject",
146
+ "return-path",
147
+ "date",
148
+ "mime-version",
149
+ "content-type",
150
+ "content-transfer-encoding",
151
+ "dkim-signature",
152
+ ]);
153
+ const MESSAGE_FIELDS = new Set([
154
+ "to",
155
+ "cc",
156
+ "bcc",
157
+ "subject",
158
+ "text",
159
+ "html",
160
+ "from",
161
+ "replyTo",
162
+ "headers",
163
+ "attachments",
164
+ ]);
165
+ /**
166
+ * Check a send request's JSON body against what Email Sending takes, so an
167
+ * app hears "bcc has 51 recipients" from the gateway rather than a bare
168
+ * provider error. Unknown fields are refused (a `reply_to` typo would
169
+ * otherwise vanish).
170
+ */
171
+ export function parseSendEmailMessage(input) {
172
+ try {
173
+ return { ok: true, message: checkMessage(input) };
174
+ }
175
+ catch (error) {
176
+ if (error instanceof EmailMessageProblem) {
177
+ return { ok: false, problem: error.message };
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+ class EmailMessageProblem extends Error {
183
+ }
184
+ function problem(message) {
185
+ throw new EmailMessageProblem(message);
186
+ }
187
+ function checkMessage(input) {
188
+ if (!isRecord(input))
189
+ problem("The body is a JSON object.");
190
+ for (const field of Object.keys(input)) {
191
+ if (!MESSAGE_FIELDS.has(field)) {
192
+ problem(`Unknown field "${field}"; a message takes ${[...MESSAGE_FIELDS].join(", ")}.`);
193
+ }
194
+ }
195
+ const to = recipients(input["to"], "to");
196
+ const cc = recipients(input["cc"] ?? [], "cc");
197
+ const bcc = recipients(input["bcc"] ?? [], "bcc");
198
+ if (to.length === 0)
199
+ problem("to names at least one recipient.");
200
+ const count = to.length + cc.length + bcc.length;
201
+ if (count > EMAIL_LIMITS.recipients) {
202
+ problem(`A message goes to at most ${EMAIL_LIMITS.recipients} recipients across to, cc and bcc (this one: ${count}).`);
203
+ }
204
+ const subject = input["subject"];
205
+ if (typeof subject !== "string")
206
+ problem("subject is a string.");
207
+ if (LINE_BREAK.test(subject))
208
+ problem("subject is one line.");
209
+ if (subject.length > EMAIL_LIMITS.subjectChars) {
210
+ problem(`subject is at most ${EMAIL_LIMITS.subjectChars} characters.`);
211
+ }
212
+ const text = optionalString(input["text"], "text");
213
+ const html = optionalString(input["html"], "html");
214
+ if (!text && !html)
215
+ problem("A message has a text or an html body.");
216
+ const message = { to, cc, bcc, subject };
217
+ if (text)
218
+ message.text = text;
219
+ if (html)
220
+ message.html = html;
221
+ if (input["from"] !== undefined) {
222
+ message.from = addressInput(input["from"], "from");
223
+ }
224
+ if (input["replyTo"] !== undefined) {
225
+ message.replyTo = addressInput(input["replyTo"], "replyTo");
226
+ }
227
+ if (input["headers"] !== undefined) {
228
+ message.headers = headers(input["headers"]);
229
+ }
230
+ if (input["attachments"] !== undefined) {
231
+ message.attachments = attachments(input["attachments"]);
232
+ }
233
+ return message;
234
+ }
235
+ function recipients(value, field) {
236
+ const list = typeof value === "string" ? [value] : value;
237
+ if (!Array.isArray(list)) {
238
+ problem(`${field} is an address or a list of addresses.`);
239
+ }
240
+ return list.map((entry, index) => {
241
+ if (typeof entry !== "string") {
242
+ problem(`${field}[${index}] is an address string.`);
243
+ }
244
+ return address(entry, `${field}[${index}]`);
245
+ });
246
+ }
247
+ function address(value, field) {
248
+ const trimmed = value.trim();
249
+ if (trimmed.length > MAX_ADDRESS_LENGTH || !ADDRESS.test(trimmed)) {
250
+ problem(`${field} isn't an email address: "${value.slice(0, 80)}".`);
251
+ }
252
+ return trimmed;
253
+ }
254
+ function addressInput(value, field) {
255
+ if (typeof value === "string")
256
+ return { address: address(value, field) };
257
+ if (!isRecord(value)) {
258
+ problem(`${field} is an address, or { address, name }.`);
259
+ }
260
+ for (const key of Object.keys(value)) {
261
+ if (key !== "address" && key !== "name") {
262
+ problem(`${field} takes address and name, not "${key}".`);
263
+ }
264
+ }
265
+ if (typeof value["address"] !== "string") {
266
+ problem(`${field}.address is an address string.`);
267
+ }
268
+ const result = {
269
+ address: address(value["address"], `${field}.address`),
270
+ };
271
+ const name = value["name"];
272
+ if (name !== undefined && name !== null) {
273
+ if (typeof name !== "string")
274
+ problem(`${field}.name is a string.`);
275
+ if (CONTROL.test(name) || name.length > MAX_NAME_LENGTH) {
276
+ problem(`${field}.name is one line of at most ${MAX_NAME_LENGTH} characters.`);
277
+ }
278
+ if (name.trim())
279
+ result.name = name.trim();
280
+ }
281
+ return result;
282
+ }
283
+ function headers(value) {
284
+ if (!isRecord(value))
285
+ problem("headers is an object of names to values.");
286
+ const entries = Object.entries(value);
287
+ if (entries.length > EMAIL_LIMITS.headers) {
288
+ problem(`A message sets at most ${EMAIL_LIMITS.headers} headers.`);
289
+ }
290
+ let total = 0;
291
+ const result = {};
292
+ for (const [name, headerValue] of entries) {
293
+ if (!HEADER_NAME.test(name) ||
294
+ byteLength(name) > EMAIL_LIMITS.headerNameBytes) {
295
+ problem(`"${name.slice(0, 80)}" isn't a header name.`);
296
+ }
297
+ if (RESERVED_HEADERS.has(name.toLowerCase())) {
298
+ problem(`The ${name} header comes from the message's own fields or the transport; it can't be set as a header.`);
299
+ }
300
+ if (typeof headerValue !== "string") {
301
+ problem(`The ${name} header's value is a string.`);
302
+ }
303
+ if (CONTROL.test(headerValue)) {
304
+ problem(`The ${name} header's value is one line.`);
305
+ }
306
+ const bytes = byteLength(headerValue);
307
+ if (bytes > EMAIL_LIMITS.headerValueBytes) {
308
+ problem(`The ${name} header's value is at most ${EMAIL_LIMITS.headerValueBytes} bytes.`);
309
+ }
310
+ total += byteLength(name) + bytes;
311
+ result[name] = headerValue;
312
+ }
313
+ if (total > EMAIL_LIMITS.headersTotalBytes) {
314
+ problem(`A message's headers are at most ${EMAIL_LIMITS.headersTotalBytes / 1024} KB together.`);
315
+ }
316
+ return result;
317
+ }
318
+ function attachments(value) {
319
+ if (!Array.isArray(value))
320
+ problem("attachments is a list.");
321
+ if (value.length > EMAIL_LIMITS.attachments) {
322
+ problem(`A message has at most ${EMAIL_LIMITS.attachments} attachments.`);
323
+ }
324
+ return value.map((entry, index) => {
325
+ const field = `attachments[${index}]`;
326
+ if (!isRecord(entry))
327
+ problem(`${field} is an object.`);
328
+ const { filename, contentType, content, disposition, contentId } = entry;
329
+ for (const key of Object.keys(entry)) {
330
+ if (![
331
+ "filename",
332
+ "contentType",
333
+ "content",
334
+ "disposition",
335
+ "contentId",
336
+ ].includes(key)) {
337
+ problem(`${field} has an unknown field "${key}".`);
338
+ }
339
+ }
340
+ if (typeof filename !== "string" ||
341
+ !filename.trim() ||
342
+ CONTROL.test(filename) ||
343
+ filename.length > MAX_FILENAME_LENGTH) {
344
+ problem(`${field}.filename is a file name on one line.`);
345
+ }
346
+ if (typeof contentType !== "string" || !MIME_TYPE.test(contentType)) {
347
+ problem(`${field}.contentType is a MIME type such as application/pdf.`);
348
+ }
349
+ if (typeof content !== "string" || !BASE64.test(content)) {
350
+ problem(`${field}.content is the file's bytes in base64.`);
351
+ }
352
+ if (disposition !== undefined &&
353
+ disposition !== "attachment" &&
354
+ disposition !== "inline") {
355
+ problem(`${field}.disposition is "attachment" or "inline".`);
356
+ }
357
+ const checked = {
358
+ filename: filename.trim(),
359
+ contentType,
360
+ content,
361
+ disposition: disposition ?? "attachment",
362
+ };
363
+ if (checked.disposition === "inline") {
364
+ if (typeof contentId !== "string" ||
365
+ !contentId.trim() ||
366
+ CONTROL.test(contentId)) {
367
+ problem(`${field} is inline, so it needs a contentId.`);
368
+ }
369
+ checked.contentId = contentId.trim();
370
+ }
371
+ else if (contentId !== undefined) {
372
+ problem(`${field}.contentId is for inline attachments only.`);
373
+ }
374
+ return checked;
375
+ });
376
+ }
377
+ function optionalString(value, field) {
378
+ if (value === undefined || value === null)
379
+ return undefined;
380
+ if (typeof value !== "string")
381
+ problem(`${field} is a string.`);
382
+ return value;
383
+ }
384
+ function isRecord(value) {
385
+ return typeof value === "object" && value !== null && !Array.isArray(value);
386
+ }
387
+ function byteLength(value) {
388
+ return new TextEncoder().encode(value).byteLength;
389
+ }
390
+ /**
391
+ * Whether `address` may be a message's From under `patterns` (the
392
+ * installation's senders, `apps.email_senders`): `*@example.com` takes any
393
+ * address on exactly example.com (not its subdomains), anything else that
394
+ * address alone. Case never matters.
395
+ */
396
+ export function senderAllowed(address, patterns) {
397
+ const candidate = address.trim().toLowerCase();
398
+ const at = candidate.lastIndexOf("@");
399
+ if (at <= 0)
400
+ return false;
401
+ const domain = candidate.slice(at + 1);
402
+ return patterns.some((pattern) => {
403
+ const wanted = pattern.trim().toLowerCase();
404
+ return wanted.startsWith("*@")
405
+ ? wanted.slice(2) === domain
406
+ : wanted === candidate;
407
+ });
408
+ }
409
+ /** The domain of an address, lowercased. */
410
+ export function emailDomain(address) {
411
+ return address.slice(address.lastIndexOf("@") + 1).toLowerCase();
412
+ }
413
+ /** The distinct recipients of a message, lowercased: what budgets count. */
414
+ export function distinctRecipients(message) {
415
+ return [
416
+ ...new Set([...message.to, ...message.cc, ...message.bcc].map((recipient) => recipient.toLowerCase())),
417
+ ];
418
+ }
419
+ /**
420
+ * Codes for a message the provider found malformed: the binding's `E_*`
421
+ * (Resend's validation errors map onto `E_VALIDATION_ERROR`).
422
+ */
423
+ const INVALID_MESSAGE_CODES = new Set([
424
+ "E_VALIDATION_ERROR",
425
+ "E_FIELD_MISSING",
426
+ "E_TOO_MANY_RECIPIENTS",
427
+ "E_TOO_MANY_ATTACHMENTS",
428
+ "E_CONTENT_TOO_LARGE",
429
+ "E_HEADER_NOT_ALLOWED",
430
+ "E_HEADER_USE_API_FIELD",
431
+ "E_HEADER_VALUE_INVALID",
432
+ "E_HEADER_VALUE_TOO_LONG",
433
+ "E_HEADER_NAME_INVALID",
434
+ "E_HEADERS_TOO_LARGE",
435
+ "E_HEADERS_TOO_MANY",
436
+ // REST: invalid request, too big, invalid email.
437
+ "10001",
438
+ "10200",
439
+ "10202",
440
+ ]);
441
+ /**
442
+ * The refusal for an error the provider answered with, by its code:
443
+ * Cloudflare's binding `E_*` or REST numeric one, or the `E_*` code the
444
+ * gateway's Resend provider maps Resend's errors onto. `senderDomain` is
445
+ * the From's domain, for the step that verifies it with `provider`.
446
+ */
447
+ export function emailProviderRefusal(providerCode, senderDomain, provider = "cloudflare") {
448
+ const label = EMAIL_PROVIDER_LABELS[provider];
449
+ const suffix = providerCode ? ` (${providerCode})` : "";
450
+ switch (providerCode) {
451
+ case "E_SENDER_NOT_VERIFIED":
452
+ case "E_SENDER_DOMAIN_NOT_AVAILABLE":
453
+ return {
454
+ status: 422,
455
+ code: "email_sender_not_verified",
456
+ message: provider === "resend"
457
+ ? `Resend won't send from ${senderDomain}${suffix}: the domain must be verified in the gateway's Resend account. Its operator adds ${senderDomain} at https://resend.com/domains and publishes the DNS records Resend lists.`
458
+ : `Cloudflare Email Sending won't send from ${senderDomain}${suffix}: the domain must be onboarded in the gateway's Cloudflare account. Its operator runs: cf email-sending subdomains create -z ${senderDomain} --name ${senderDomain} (or Compute > Email Service > Email Sending > Onboard Domain in the dashboard).`,
459
+ };
460
+ case "E_RATE_LIMIT_EXCEEDED":
461
+ case "E_DAILY_LIMIT_EXCEEDED":
462
+ case "10004":
463
+ return {
464
+ status: 429,
465
+ code: "email_provider_limited",
466
+ message: `${label} refused the message: the gateway's account has reached its sending limit${suffix}. Try again later.`,
467
+ };
468
+ default:
469
+ if (providerCode && INVALID_MESSAGE_CODES.has(providerCode)) {
470
+ return {
471
+ status: 400,
472
+ code: "email_invalid_message",
473
+ message: `${label} refused the message as invalid${suffix}.`,
474
+ };
475
+ }
476
+ return {
477
+ status: 502,
478
+ code: "email_send_failed",
479
+ message: `${label} failed to send the message${suffix}.`,
480
+ };
481
+ }
482
+ }
483
+ /** The URL of Email Sending's REST `send` for an account. */
484
+ export function emailRestUrl(accountId) {
485
+ return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/email/sending/send`;
486
+ }
487
+ /** A message as Email Sending's REST `send` takes it (snake_case fields). */
488
+ export function emailRestBody(message) {
489
+ const body = {
490
+ to: message.to.length === 1 ? message.to[0] : message.to,
491
+ from: restAddress(message.from),
492
+ // A reply goes to the sender unless the message says otherwise.
493
+ reply_to: restAddress(message.replyTo ?? message.from),
494
+ subject: message.subject,
495
+ };
496
+ if (message.cc.length > 0)
497
+ body["cc"] = message.cc;
498
+ if (message.bcc.length > 0)
499
+ body["bcc"] = message.bcc;
500
+ if (message.text)
501
+ body["text"] = message.text;
502
+ if (message.html)
503
+ body["html"] = message.html;
504
+ if (message.headers && Object.keys(message.headers).length > 0) {
505
+ body["headers"] = message.headers;
506
+ }
507
+ if (message.attachments && message.attachments.length > 0) {
508
+ body["attachments"] = message.attachments.map((attachment) => ({
509
+ content: attachment.content,
510
+ filename: attachment.filename,
511
+ type: attachment.contentType,
512
+ disposition: attachment.disposition,
513
+ ...(attachment.contentId ? { content_id: attachment.contentId } : {}),
514
+ }));
515
+ }
516
+ return body;
517
+ }
518
+ function restAddress(address) {
519
+ return address.name
520
+ ? { address: address.address, name: address.name }
521
+ : address.address;
522
+ }
523
+ /**
524
+ * Read Email Sending's REST answer: the message id and whether delivery is
525
+ * queued, or the provider's error code. A message whose every recipient
526
+ * bounced permanently failed; one that reached some of them did not.
527
+ */
528
+ export function readEmailRestResponse(httpStatus, body) {
529
+ const parsed = (isRecord(body) ? body : {});
530
+ const error = parsed.errors?.[0];
531
+ if (httpStatus < 200 || httpStatus >= 300 || parsed.success === false) {
532
+ const code = error?.code !== undefined
533
+ ? String(error.code)
534
+ : httpStatus === 429
535
+ ? "10004"
536
+ : undefined;
537
+ return {
538
+ ok: false,
539
+ ...(code ? { providerCode: code } : {}),
540
+ message: error?.message ?? `Email API returned ${httpStatus}.`,
541
+ };
542
+ }
543
+ const result = parsed.result;
544
+ if (!result) {
545
+ return { ok: false, message: "Email API returned no delivery result." };
546
+ }
547
+ const delivered = result.delivered ?? [];
548
+ const queued = result.queued ?? [];
549
+ if (delivered.length === 0 &&
550
+ queued.length === 0 &&
551
+ (result.permanent_bounces ?? []).length > 0) {
552
+ return {
553
+ ok: false,
554
+ message: "Email API permanently bounced every recipient.",
555
+ };
556
+ }
557
+ return {
558
+ ok: true,
559
+ ...(result.message_id ? { messageId: result.message_id } : {}),
560
+ status: queued.length > 0 ? "queued" : "delivered",
561
+ };
562
+ }
563
+ /**
564
+ * The sender EMAIL_FROM and EMAIL_FROM_NAME configure: EMAIL_FROM is an
565
+ * address, or `Name <address>` with the name in it. Null unless both an
566
+ * address and a name are set.
567
+ */
568
+ export function parseEmailSender(from, name) {
569
+ const configuredFrom = from?.trim();
570
+ if (!configuredFrom)
571
+ return null;
572
+ const combined = /^(.*?)\s*<([^<>]+)>$/.exec(configuredFrom);
573
+ const address = (combined?.[2] ?? configuredFrom).trim();
574
+ const combinedName = combined?.[1]
575
+ ?.trim()
576
+ .replace(/^(["'])(.*)\1$/, "$2")
577
+ .trim();
578
+ const resolvedName = name?.trim() || combinedName;
579
+ return address && resolvedName ? { address, name: resolvedName } : null;
580
+ }
581
+ /**
582
+ * The binding an app's Workers get in local dev when `.dev.vars` sets it
583
+ * (`everyAppWorker()` declares it like the other dev keys): the senders a
584
+ * gateway would hold for this installation, a comma- or whitespace-
585
+ * separated list or a JSON array. Deployed, the gateway reads them from the
586
+ * app's registry row (`apps.email_senders`, `everyapp email set --sender`).
587
+ */
588
+ export const EMAIL_SENDERS_ENV = "EVERYAPP_EMAIL_SENDERS";
589
+ /**
590
+ * A list of addresses or patterns: an `apps` row's `email_senders` or
591
+ * `email_inbound` (JSON), or {@link EMAIL_SENDERS_ENV} (JSON, or separated
592
+ * by commas or whitespace). None when unset, or when JSON that isn't an
593
+ * array; other text is read as a comma or whitespace list, so a word no
594
+ * address matches allows no sender. Only `everyapp email set` writes the
595
+ * columns, as JSON; the admin page reads them as JSON only.
596
+ */
597
+ export function parseEmailList(value) {
598
+ if (typeof value !== "string" || !value.trim())
599
+ return [];
600
+ const text = value.trim();
601
+ if (text.startsWith("[")) {
602
+ try {
603
+ const parsed = JSON.parse(text);
604
+ return Array.isArray(parsed)
605
+ ? parsed.filter((entry) => typeof entry === "string")
606
+ : [];
607
+ }
608
+ catch {
609
+ return [];
610
+ }
611
+ }
612
+ return text.split(/[\s,]+/).filter(Boolean);
613
+ }