void 0.20.0 → 0.20.2
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/README.md +5 -1
- package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
- package/dist/{auth-cmd-CYVhSNFy.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
- package/dist/auth-link-NZdjCmSc.mjs +28 -0
- package/dist/{build-cmd-CkVD1uOh.mjs → build-cmd-sI18tX_O.mjs} +3 -3
- package/dist/{cache-QcUfR-ff.mjs → cache-IHn5MwBC.mjs} +3 -3
- package/dist/{cancel-deploy-DsvWKFTe.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
- package/dist/cf-access-DRsQRe6k.mjs +75 -0
- package/dist/cli/cli.mjs +364 -1883
- package/dist/cli/env-schema-probe.mjs +11 -2
- package/dist/{client-CyCHWSO_.mjs → client-dHfSJvAN.mjs} +336 -58
- package/dist/{cloudflare-auth-DRkGe7-s.mjs → cloudflare-auth-6M5llVPC.mjs} +53 -6
- package/dist/{cloudflare-cmd-D3ME2GAe.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
- package/dist/{cloudflare-connect-BivMefMA.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
- package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
- package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
- package/dist/{connect-D9Yr-T5f.mjs → connect-Bfk31O_8.mjs} +6 -6
- package/dist/{create-project-DMV-csEm.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
- package/dist/{db-BxoUWL0F.mjs → db-BkRoptAt.mjs} +75 -48
- package/dist/{delete-iJOqxBz1.mjs → delete-DouASY9P.mjs} +3 -3
- package/dist/{deploy-CcDoeBIf.mjs → deploy-DTaWUS1S.mjs} +137 -128
- package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
- package/dist/{domain-gu_iaHmn.mjs → domain-1RhhOVrC.mjs} +4 -4
- package/dist/email-C-lGh51B.mjs +795 -0
- package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
- package/dist/{env-DmU2To0C.mjs → env-DJHsPE7Z.mjs} +5 -5
- package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
- package/dist/{gen-BKw6qHIg.mjs → gen-B_wPnVTK.mjs} +3 -3
- package/dist/generate-RTK8_kK1.mjs +47 -0
- package/dist/{github-cmd-DJS5Ab-H.mjs → github-cmd-PW7ZnWTp.mjs} +3 -3
- package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
- package/dist/help-CwOX-zmI.mjs +2216 -0
- package/dist/{inbound-BJ70in1n.d.mts → inbound-CH5Mksyy.d.mts} +37 -42
- package/dist/{inbound-CNKxb3FY.mjs → inbound-aVHEUhKo.mjs} +143 -101
- package/dist/index.mjs +69 -30
- package/dist/{init-Dx5cgKqK.mjs → init-BWZ7q5Z4.mjs} +11 -11
- package/dist/{link-D_rm5sRH.mjs → link-Rmvu2Wl_.mjs} +4 -4
- package/dist/{list-M8XShti7.mjs → list-DEE2S6mY.mjs} +4 -4
- package/dist/{local-d1-BE8KBbMy.mjs → local-d1-D2I6Ox5F.mjs} +1 -1
- package/dist/{login-C8-UuLnp.mjs → login-Uvferzmm.mjs} +28 -10
- package/dist/{logs-DwFU7dMW.mjs → logs-27FenuiC.mjs} +4 -4
- package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
- package/dist/{node-BkaXcpAc.mjs → node-Ez5KW5rn.mjs} +3 -3
- package/dist/operator-auth-B3e08unv.mjs +52 -0
- package/dist/operator-client-LUZnlnYk.mjs +82 -0
- package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CjOTmAYE.mjs} +35 -55
- package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
- package/dist/pages/index.mjs +2 -2
- package/dist/platform-auth-config-DrbQXXiW.mjs +368 -0
- package/dist/platform-auth-protection-Bhtvp0B_.mjs +219 -0
- package/dist/platform-auth-recovery-CeOKGVeJ.mjs +310 -0
- package/dist/{platform-cmd-BEOVv1PN.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
- package/dist/{platform-domain-BszUfiS8.mjs → platform-domain-C74PULqV.mjs} +4 -4
- package/dist/{platform-lifecycle-CjSr6xf0.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
- package/dist/{platform-management-W30iCaTL.mjs → platform-management-COogu_Se.mjs} +33 -7
- package/dist/{platform-recovery-pHHv4ZSg.mjs → platform-recovery-ewqLefp1.mjs} +6 -5
- package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
- package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
- package/dist/prerender-render.d.mts +11 -0
- package/dist/prerender-render.mjs +111 -0
- package/dist/{project-cmd-DCpk1cDt.mjs → project-cmd-DmZK9Hxf.mjs} +32 -14
- package/dist/project-team-D8jOJMUJ.mjs +130 -0
- package/dist/project-token-DA34bf-C.mjs +75 -0
- package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
- package/dist/{requests-Dn8Vheh1.mjs → requests-CUExwGQQ.mjs} +3 -3
- package/dist/{rollback-CtlPXEBi.mjs → rollback-CDNGU1gr.mjs} +4 -4
- package/dist/{runner-mysql-CgRFl3s6.mjs → runner-mysql-7BPUNGmL.mjs} +1 -1
- package/dist/{runner-pg-DCkWPsWS.mjs → runner-pg-BkEza-dX.mjs} +1 -1
- package/dist/runtime/ai.mjs +3 -2
- package/dist/runtime/email/testing.d.mts +1 -1
- package/dist/runtime/email/testing.mjs +3 -3
- package/dist/runtime/email-protocol.d.mts +15 -0
- package/dist/runtime/email-protocol.mjs +70 -0
- package/dist/runtime/email.d.mts +2 -2
- package/dist/runtime/email.mjs +189 -96
- package/dist/runtime/remote/index.mjs +5 -3
- package/dist/{secret-BqTxGqki.mjs → secret-Bzzi2e9E.mjs} +5 -5
- package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
- package/dist/sqlite-validation-BzKMWnO4.mjs +25 -0
- package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
- package/dist/{validate-Bihr8WBi.mjs → validate-tBBN_dXH.mjs} +1 -0
- package/package.json +12 -7
- package/skills/void/SKILL.md +35 -4
- package/skills/void/docs/guide/deployment.md +2 -0
- package/skills/void/docs/guide/email.md +121 -98
- package/skills/void/docs/guide/platform-administration.md +214 -2
- package/skills/void/docs/guide/platform-development.md +93 -2
- package/skills/void/docs/guide/project-collaboration.md +94 -0
- package/skills/void/docs/guide/self-hosted-platform.md +184 -30
- package/skills/void/docs/reference/cli.md +294 -15
- package/dist/cf-access-AJ1ehiFR.mjs +0 -42
- package/dist/cf-access-DsSsZUPr.mjs +0 -67
- package/dist/email-r6DHJyAB.mjs +0 -262
|
@@ -36,6 +36,12 @@ interface SendEmailOptions {
|
|
|
36
36
|
bcc?: Address | Array<Address>;
|
|
37
37
|
headers?: Record<string, string>;
|
|
38
38
|
attachments?: Array<Attachment>;
|
|
39
|
+
/**
|
|
40
|
+
* Deduplicates a platform send for 30 days. Reuse the same key and message
|
|
41
|
+
* when checking an uncertain attempt; a changed message is a conflict.
|
|
42
|
+
* Native Cloudflare bindings do not provide this guarantee.
|
|
43
|
+
*/
|
|
44
|
+
idempotencyKey?: string;
|
|
39
45
|
/**
|
|
40
46
|
* Per-call escape hatch for local development. When true, bypasses the
|
|
41
47
|
* `void dev` in-memory inbox for this one send.
|
|
@@ -52,7 +58,7 @@ interface SendEmailOptions {
|
|
|
52
58
|
*/
|
|
53
59
|
sendInDev?: boolean;
|
|
54
60
|
}
|
|
55
|
-
type SendEmailErrorCode = "BINDING_MISSING" | "INVALID_FROM" | "INVALID_TO" | "UNVERIFIED_DESTINATION" | "MIME_ERROR" | "QUOTA_EXCEEDED" | "UPSTREAM_ERROR";
|
|
61
|
+
type SendEmailErrorCode = "BINDING_MISSING" | "INVALID_FROM" | "INVALID_TO" | "UNVERIFIED_DESTINATION" | "MIME_ERROR" | "QUOTA_EXCEEDED" | "IDEMPOTENCY_CONFLICT" | "OUTCOME_UNKNOWN" | "UPSTREAM_ERROR";
|
|
56
62
|
interface SendEmailError {
|
|
57
63
|
code: SendEmailErrorCode;
|
|
58
64
|
message: string;
|
|
@@ -66,7 +72,9 @@ interface SendEmailError {
|
|
|
66
72
|
interface SendEmailDelivery {
|
|
67
73
|
recipient: string;
|
|
68
74
|
messageId: string;
|
|
75
|
+
state?: "provider_accepted";
|
|
69
76
|
}
|
|
77
|
+
type EmailDeliveryState = "rejected" | "reserved" | "attempt_started" | "provider_accepted" | "failed" | "outcome_unknown" | "cancelled";
|
|
70
78
|
/**
|
|
71
79
|
* Per-recipient outcome for a multi-recipient send. `ok: true` carries the
|
|
72
80
|
* CF-issued messageId; `ok: false` carries the error for that specific
|
|
@@ -76,10 +84,12 @@ type SendEmailRecipientResult = {
|
|
|
76
84
|
recipient: string;
|
|
77
85
|
ok: true;
|
|
78
86
|
messageId: string;
|
|
87
|
+
state?: "provider_accepted";
|
|
79
88
|
} | {
|
|
80
89
|
recipient: string;
|
|
81
90
|
ok: false;
|
|
82
91
|
error: SendEmailError;
|
|
92
|
+
state?: EmailDeliveryState;
|
|
83
93
|
};
|
|
84
94
|
/**
|
|
85
95
|
* Result of `sendEmail`.
|
|
@@ -89,25 +99,27 @@ type SendEmailRecipientResult = {
|
|
|
89
99
|
* `'deliveries' in result` (per-recipient failure, some may have succeeded):
|
|
90
100
|
*
|
|
91
101
|
* - `{ ok: true, ids }` — every envelope succeeded.
|
|
92
|
-
* - `{ ok: false, error }` —
|
|
93
|
-
*
|
|
102
|
+
* - `{ ok: false, error }` — validation or transport failure. An
|
|
103
|
+
* OUTCOME_UNKNOWN may have been submitted;
|
|
104
|
+
* never retry it with a new idempotency key.
|
|
94
105
|
* - `{ ok: false, deliveries }` — sends were attempted; at least one failed.
|
|
95
106
|
* `deliveries` carries the per-recipient
|
|
96
|
-
* outcome
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* fail with distinct errors, and picking one
|
|
100
|
-
* would be arbitrary.
|
|
107
|
+
* outcome. Provider acceptance does not
|
|
108
|
+
* establish mailbox delivery. Unknown
|
|
109
|
+
* outcomes must not be blindly retried.
|
|
101
110
|
*/
|
|
102
111
|
type SendEmailResult = {
|
|
103
112
|
ok: true;
|
|
104
113
|
ids: Array<SendEmailDelivery>;
|
|
114
|
+
operationId?: string;
|
|
105
115
|
} | {
|
|
106
116
|
ok: false;
|
|
107
117
|
error: SendEmailError;
|
|
118
|
+
operationId?: string;
|
|
108
119
|
} | {
|
|
109
120
|
ok: false;
|
|
110
121
|
deliveries: Array<SendEmailRecipientResult>;
|
|
122
|
+
operationId?: string;
|
|
111
123
|
};
|
|
112
124
|
/**
|
|
113
125
|
* Normalized representation produced by validation, consumed by both the
|
|
@@ -197,31 +209,24 @@ declare class ParseEmailError extends Error {
|
|
|
197
209
|
* one-shot.
|
|
198
210
|
*/
|
|
199
211
|
declare function parseEmail(message: ForwardableEmailMessage): Promise<ParsedEmail>;
|
|
200
|
-
/**
|
|
201
|
-
* Serialized inbound email message — sent from the platform router to the
|
|
202
|
-
* user worker over RPC. Every field is in workerd's RPC `BaseType`
|
|
203
|
-
* allowlist (string, plain object, ReadableStream<Uint8Array>, number).
|
|
204
|
-
*/
|
|
212
|
+
/** Inbound message decoded from the authenticated gateway request. */
|
|
205
213
|
interface EmailEnvelope {
|
|
206
214
|
from: string;
|
|
207
215
|
to: string;
|
|
208
|
-
/**
|
|
216
|
+
/** Decoded header metadata. */
|
|
209
217
|
headers: Record<string, string>;
|
|
210
218
|
/**
|
|
211
|
-
* Raw RFC 5322 bytes.
|
|
212
|
-
* receiving side (`parseEmail` consumes the stream once; if the handler
|
|
213
|
-
* also calls `parseEmail` directly, the platform tee'd a fresh stream per
|
|
214
|
-
* delivery).
|
|
219
|
+
* Raw RFC 5322 bytes. The message proxy caches them for repeated reads.
|
|
215
220
|
*/
|
|
216
221
|
raw: ReadableStream<Uint8Array> | Uint8Array;
|
|
217
222
|
rawSize: number;
|
|
223
|
+
/**
|
|
224
|
+
* Exact custom domain selected by the gateway. Custom-domain recipients
|
|
225
|
+
* have no project-slug prefix; replies default to the received address.
|
|
226
|
+
*/
|
|
227
|
+
viaDomain?: string;
|
|
218
228
|
}
|
|
219
|
-
/**
|
|
220
|
-
* What the user handler decided to do with a message. Returned from the
|
|
221
|
-
* worker's `email(envelope)` RPC method as an array (handlers may chain
|
|
222
|
-
* multiple actions, e.g. forward then reply); the router enacts each on the
|
|
223
|
-
* real `ForwardableEmailMessage` in order.
|
|
224
|
-
*/
|
|
229
|
+
/** Handler intents, validated and admitted by the gateway before delivery. */
|
|
225
230
|
type EmailAction = {
|
|
226
231
|
type: "reject";
|
|
227
232
|
reason: string;
|
|
@@ -261,7 +266,7 @@ interface CreateMessageProxyOptions {
|
|
|
261
266
|
* accept a native `EmailMessage` from `cloudflare:email`: that class exposes
|
|
262
267
|
* only `from` and `to` (workers-types declares nothing else, and the body
|
|
263
268
|
* has no public accessor), so its bytes cannot be recorded here, and nothing
|
|
264
|
-
* could
|
|
269
|
+
* could encode them for the gateway. The reply MIME bytes are
|
|
265
270
|
* buffered before being recorded so the action is fully self-contained.
|
|
266
271
|
*
|
|
267
272
|
* When `opts.slug` is provided, `message.to` is rewritten to remove the
|
|
@@ -314,8 +319,8 @@ interface ReplyEmailOptions {
|
|
|
314
319
|
/**
|
|
315
320
|
* Send a threading-aware reply to an inbound message. Builds the reply MIME
|
|
316
321
|
* (correct `In-Reply-To` / `References` / `Subject: Re: …`) and records a
|
|
317
|
-
* reply intent on the message proxy.
|
|
318
|
-
*
|
|
322
|
+
* reply intent on the message proxy. Delivery follows handler completion and,
|
|
323
|
+
* on the platform, authoritative admission.
|
|
319
324
|
*
|
|
320
325
|
* The threading headers are best-effort, since the inbound values they are
|
|
321
326
|
* built from are remote-sender controlled: an id no header line can carry
|
|
@@ -324,10 +329,8 @@ interface ReplyEmailOptions {
|
|
|
324
329
|
*/
|
|
325
330
|
declare function replyEmail(message: ForwardableEmailMessage, opts: ReplyEmailOptions): Promise<void>;
|
|
326
331
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* `EmailEnvelope` from the platform router. Functions do not survive RPC,
|
|
330
|
-
* so their presence is the discriminator.
|
|
332
|
+
* Recognize native email operations. This structural check is not an
|
|
333
|
+
* authentication boundary; managed entrypoints require receipt authentication.
|
|
331
334
|
*/
|
|
332
335
|
declare function isForwardableEmailMessage(value: unknown): value is ForwardableEmailMessage;
|
|
333
336
|
/**
|
|
@@ -338,17 +341,9 @@ declare function isForwardableEmailMessage(value: unknown): value is Forwardable
|
|
|
338
341
|
* duck-typed object — so `cloudflare:email` is loaded here, and only when
|
|
339
342
|
* a reply was recorded.
|
|
340
343
|
*
|
|
341
|
-
* The
|
|
342
|
-
*
|
|
343
|
-
* `forward()` already ran the record through that constructor, so a bad
|
|
344
|
-
* record failed inside the handler, before any replay. (`EmailMessage`
|
|
345
|
-
* construction neither validates nor reads its stream — measured under
|
|
346
|
-
* workerd.) A reject's reason reaches the native `setReject()` as the proxy
|
|
347
|
-
* recorded it — already sliced and stripped the way the platform router
|
|
348
|
-
* would (`normalizeRejectReason`), so the SMTP reply is the same in both
|
|
349
|
-
* deployments. What Cloudflare itself refuses at `forward()` / `reply()`
|
|
350
|
-
* still throws out of the loop, as any native call would.
|
|
344
|
+
* The proxy already validated forward headers and normalized reject reasons.
|
|
345
|
+
* Provider refusals still propagate to the native event caller.
|
|
351
346
|
*/
|
|
352
347
|
declare function enactEmailActions(message: ForwardableEmailMessage, actions: ReadonlyArray<EmailAction>): Promise<void>;
|
|
353
348
|
//#endregion
|
|
354
|
-
export {
|
|
349
|
+
export { SendEmailOptions as C, SendEmailErrorCode as S, SendEmailResult as T, Attachment as _, MessageProxy as a, SendEmailDelivery as b, ParsedEmail as c, defineEmail as d, enactEmailActions as f, Address as g, replyEmail as h, EmailHandlerInfo as i, ReplyEmailOptions as l, parseEmail as m, EmailEnvelope as n, ParseEmailError as o, isForwardableEmailMessage as p, EmailHandler as r, ParseEmailErrorCode as s, EmailAction as t, createMessageProxy as u, EmailDeliveryState as v, SendEmailRecipientResult as w, SendEmailError as x, NormalizedEmail as y };
|
|
@@ -1,58 +1,28 @@
|
|
|
1
|
-
import { i as normalizeEmail, n as MimeError, o as validateAddressParts, r as buildMime
|
|
1
|
+
import { i as normalizeEmail, n as MimeError, o as validateAddressParts, r as buildMime } from "./mime-D5Nmdzf7.mjs";
|
|
2
2
|
import { a as getRawRuntimeEnv } from "./env-raw-Cx8ElDdj.mjs";
|
|
3
|
+
import { EMAIL_MAX_RESPONSE_BYTES } from "@void/platform/email-protocol";
|
|
3
4
|
//#region src/runtime/email/inbound.ts
|
|
4
5
|
const VALID_SLUG = /^[a-z0-9](?:[a-z0-9-]{0,54}[a-z0-9])?$/;
|
|
5
|
-
/**
|
|
6
|
-
* Guardrails on what one handler may queue.
|
|
7
|
-
*
|
|
8
|
-
* These are NOT the security boundary. Recording happens inside the tenant's
|
|
9
|
-
* own worker, and on the platform a Workers-for-Platforms tenant can ship any
|
|
10
|
-
* `WorkerEntrypoint` whose `email()` returns any array at all, bypassing this
|
|
11
|
-
* file entirely — so the load-bearing caps are the matching ones in the
|
|
12
|
-
* platform router (`MAX_ACTIONS` / `MAX_ACTION_ADDRESS_LENGTH` in
|
|
13
|
-
* `email-router.ts`). What these buy is a fast, precise failure at the call
|
|
14
|
-
* that went wrong, instead of a whole action list silently dropped at the
|
|
15
|
-
* router with only a platform-side log to show for it.
|
|
16
|
-
*/
|
|
17
6
|
const MAX_ACTIONS = 100;
|
|
18
7
|
const MAX_ACTION_ADDRESS_LENGTH = 320;
|
|
8
|
+
const MAX_ACTION_METADATA_BYTES = 8192;
|
|
19
9
|
const MAX_FORWARD_HEADER_LENGTH = 1024;
|
|
20
|
-
/**
|
|
21
|
-
* Cap on a reject reason, restated from the router's
|
|
22
|
-
* `MAX_REJECT_REASON_LENGTH` in `email-router.ts` for the same reason as
|
|
23
|
-
* above, and pinned by the same test. See `normalizeRejectReason`.
|
|
24
|
-
*/
|
|
10
|
+
/** Maximum normalized SMTP rejection reason length. */
|
|
25
11
|
const MAX_REJECT_REASON_LENGTH = 1e3;
|
|
26
|
-
/**
|
|
27
|
-
* The router's ADMISSION bounds — what it refuses BEFORE the handler runs
|
|
28
|
-
* (`MAX_INBOUND_BYTES` / `MAX_INBOUND_HEADERS` in `email-router.ts`,
|
|
29
|
-
* restated for the same reason as above and pinned by the same test).
|
|
30
|
-
* `routeInbound` rejects a message over either one and never dispatches it,
|
|
31
|
-
* so a handler's behaviour on such a message is unreachable after a managed
|
|
32
|
-
* deploy; the test harness applies both before it invokes the handler.
|
|
33
|
-
*
|
|
34
|
-
* `MAX_INBOUND_BYTES` is NOT `MAX_MESSAGE_BYTES`, though the numbers agree
|
|
35
|
-
* today. That one is Cloudflare's cap on an OUTBOUND message (and the
|
|
36
|
-
* `parseEmail` guard); this one is the router's own heap headroom on an
|
|
37
|
-
* inbound one, set well under Cloudflare's ~25 MB inbound ceiling, and the
|
|
38
|
-
* two can move independently.
|
|
39
|
-
*/
|
|
12
|
+
/** Inbound admission limits also applied by the handler test harness. */
|
|
40
13
|
const MAX_INBOUND_BYTES = 10485760;
|
|
41
14
|
const MAX_INBOUND_HEADERS = 1024;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* overran.
|
|
54
|
-
*/
|
|
55
|
-
const MAX_REPLY_BYTES_TOTAL = 2 * MAX_MESSAGE_BYTES;
|
|
15
|
+
const MAX_REPLY_BYTES = 5242880;
|
|
16
|
+
const EMAIL_STREAM_DEADLINE_MS = 15e3;
|
|
17
|
+
const EMAIL_ACTION_TERMINAL_BYTES = 5;
|
|
18
|
+
const actionEncoder = new TextEncoder();
|
|
19
|
+
function emailActionWireSize(action) {
|
|
20
|
+
return 5 + actionEncoder.encode(JSON.stringify(action.type === "reply" ? {
|
|
21
|
+
type: action.type,
|
|
22
|
+
from: action.from,
|
|
23
|
+
to: action.to
|
|
24
|
+
} : action)).byteLength + (action.type === "reply" ? 5 + action.raw.byteLength : 0);
|
|
25
|
+
}
|
|
56
26
|
function readRawProjectSlug() {
|
|
57
27
|
let raw;
|
|
58
28
|
try {
|
|
@@ -75,6 +45,10 @@ function tryReadEmailDomain() {
|
|
|
75
45
|
}
|
|
76
46
|
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
77
47
|
}
|
|
48
|
+
function readRelayDomain(message) {
|
|
49
|
+
const raw = message.__voidRelayDomain;
|
|
50
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
51
|
+
}
|
|
78
52
|
/**
|
|
79
53
|
* Identity helper that types the inbound email handler. Use as the default
|
|
80
54
|
* export of `email/<address>.ts` or `email/_default.ts`.
|
|
@@ -104,7 +78,7 @@ var ParseEmailError = class extends Error {
|
|
|
104
78
|
* one-shot.
|
|
105
79
|
*/
|
|
106
80
|
async function parseEmail(message) {
|
|
107
|
-
if (message.rawSize > 10485760) throw new ParseEmailError("INBOUND_TOO_LARGE", `parseEmail: message size ${message.rawSize} bytes exceeds maximum of ${
|
|
81
|
+
if (message.rawSize > 10485760) throw new ParseEmailError("INBOUND_TOO_LARGE", `parseEmail: message size ${message.rawSize} bytes exceeds maximum of ${MAX_INBOUND_BYTES} bytes.`);
|
|
108
82
|
const { default: PostalMime } = await import("postal-mime");
|
|
109
83
|
const cached = proxyRawCache.get(message);
|
|
110
84
|
try {
|
|
@@ -134,10 +108,10 @@ function stripSlugFromAddress(address, slug) {
|
|
|
134
108
|
return address;
|
|
135
109
|
}
|
|
136
110
|
function replyTooLarge(size) {
|
|
137
|
-
return new MimeError("MIME_ERROR", `reply: message size ${size} bytes exceeds
|
|
111
|
+
return new MimeError("MIME_ERROR", `reply: message size ${size} bytes exceeds Void's sending limit of ${MAX_REPLY_BYTES} bytes.`);
|
|
138
112
|
}
|
|
139
113
|
function replyBudgetExhausted(size) {
|
|
140
|
-
return new MimeError("MIME_ERROR", `reply:
|
|
114
|
+
return new MimeError("MIME_ERROR", `reply: serialized actions would use ${size} bytes, over this delivery's ${EMAIL_MAX_RESPONSE_BYTES}-byte response budget.`);
|
|
141
115
|
}
|
|
142
116
|
function queueFull() {
|
|
143
117
|
return new MimeError("MIME_ERROR", `email: handler queued more than ${MAX_ACTIONS} actions for one message.`);
|
|
@@ -200,15 +174,29 @@ async function readStreamToBytes(stream, limit = Infinity, onOverflow = replyToo
|
|
|
200
174
|
const reader = stream.getReader();
|
|
201
175
|
const chunks = [];
|
|
202
176
|
let total = 0;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
177
|
+
const deadline = Date.now() + EMAIL_STREAM_DEADLINE_MS;
|
|
178
|
+
try {
|
|
179
|
+
for (;;) {
|
|
180
|
+
const remaining = deadline - Date.now();
|
|
181
|
+
if (remaining <= 0) throw new MimeError("MIME_ERROR", "email stream timed out");
|
|
182
|
+
let timer;
|
|
183
|
+
const { value, done } = await Promise.race([reader.read(), new Promise((_, reject) => {
|
|
184
|
+
timer = setTimeout(() => reject(new MimeError("MIME_ERROR", "email stream timed out")), remaining);
|
|
185
|
+
})]).finally(() => {
|
|
186
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
187
|
+
});
|
|
188
|
+
if (done) break;
|
|
189
|
+
total += value.byteLength;
|
|
190
|
+
if (total > limit) throw onOverflow(total);
|
|
191
|
+
chunks.push(value);
|
|
210
192
|
}
|
|
211
|
-
|
|
193
|
+
} catch (error) {
|
|
194
|
+
reader.cancel().catch(() => {});
|
|
195
|
+
throw error;
|
|
196
|
+
} finally {
|
|
197
|
+
try {
|
|
198
|
+
reader.releaseLock();
|
|
199
|
+
} catch {}
|
|
212
200
|
}
|
|
213
201
|
const out = new Uint8Array(total);
|
|
214
202
|
let offset = 0;
|
|
@@ -235,7 +223,7 @@ function bytesToReadableStream(bytes) {
|
|
|
235
223
|
* accept a native `EmailMessage` from `cloudflare:email`: that class exposes
|
|
236
224
|
* only `from` and `to` (workers-types declares nothing else, and the body
|
|
237
225
|
* has no public accessor), so its bytes cannot be recorded here, and nothing
|
|
238
|
-
* could
|
|
226
|
+
* could encode them for the gateway. The reply MIME bytes are
|
|
239
227
|
* buffered before being recorded so the action is fully self-contained.
|
|
240
228
|
*
|
|
241
229
|
* When `opts.slug` is provided, `message.to` is rewritten to remove the
|
|
@@ -246,7 +234,8 @@ function bytesToReadableStream(bytes) {
|
|
|
246
234
|
function createMessageProxy(envelope, opts = {}) {
|
|
247
235
|
const actions = [];
|
|
248
236
|
const headers = new Headers(envelope.headers);
|
|
249
|
-
const
|
|
237
|
+
const slug = envelope.viaDomain === void 0 || envelope.viaDomain === "" ? opts.slug : void 0;
|
|
238
|
+
const userVisibleTo = stripSlugFromAddress(envelope.to, slug);
|
|
250
239
|
let cachedRaw;
|
|
251
240
|
let bufferingPromise;
|
|
252
241
|
function readRawOnce() {
|
|
@@ -265,9 +254,20 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
265
254
|
}
|
|
266
255
|
if (envelope.raw instanceof Uint8Array) cachedRaw = envelope.raw;
|
|
267
256
|
let rejected = false;
|
|
268
|
-
let
|
|
269
|
-
|
|
257
|
+
let actionWireBytesUsed = EMAIL_ACTION_TERMINAL_BYTES;
|
|
258
|
+
let pendingReplies = 0;
|
|
259
|
+
let replyRecordingTail = Promise.resolve();
|
|
260
|
+
function recordAction(action) {
|
|
261
|
+
if (rejected) return false;
|
|
270
262
|
if (actions.length >= MAX_ACTIONS) throw queueFull();
|
|
263
|
+
const nextSize = actionWireBytesUsed + emailActionWireSize(action);
|
|
264
|
+
if (nextSize > EMAIL_MAX_RESPONSE_BYTES) throw replyBudgetExhausted(nextSize);
|
|
265
|
+
actions.push(action);
|
|
266
|
+
actionWireBytesUsed = nextSize;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
function assertQueueHasRoom() {
|
|
270
|
+
if (actions.length + pendingReplies >= MAX_ACTIONS) throw queueFull();
|
|
271
271
|
}
|
|
272
272
|
const message = {
|
|
273
273
|
from: envelope.from,
|
|
@@ -295,10 +295,12 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
if (dropped > 0) console.warn(`[void/email] setReject() after ${dropped} forward/reply call(s) — those actions were dropped. CF rejects are terminal: anything queued before setReject() never runs. Move the reject earlier in the handler.`);
|
|
298
|
-
|
|
298
|
+
const action = {
|
|
299
299
|
type: "reject",
|
|
300
300
|
reason: normalizeRejectReason(reason)
|
|
301
|
-
}
|
|
301
|
+
};
|
|
302
|
+
actions.push(action);
|
|
303
|
+
actionWireBytesUsed = EMAIL_ACTION_TERMINAL_BYTES + emailActionWireSize(action);
|
|
302
304
|
},
|
|
303
305
|
async forward(to, fwdHeaders) {
|
|
304
306
|
if (rejected) {
|
|
@@ -319,11 +321,13 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
319
321
|
headersObj = { ...fwdHeaders };
|
|
320
322
|
}
|
|
321
323
|
if (headersObj) assertForwardHeadersWithinBounds(headersObj);
|
|
322
|
-
|
|
324
|
+
const action = {
|
|
323
325
|
type: "forward",
|
|
324
326
|
to,
|
|
325
327
|
...headersObj ? { headers: headersObj } : {}
|
|
326
|
-
}
|
|
328
|
+
};
|
|
329
|
+
if (new TextEncoder().encode(JSON.stringify(action)).byteLength > MAX_ACTION_METADATA_BYTES) throw new MimeError("MIME_ERROR", `forward: action metadata exceeds ${MAX_ACTION_METADATA_BYTES} bytes.`);
|
|
330
|
+
recordAction(action);
|
|
327
331
|
},
|
|
328
332
|
async reply(reply) {
|
|
329
333
|
if (rejected) {
|
|
@@ -335,25 +339,47 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
335
339
|
if (reply.to.length > MAX_ACTION_ADDRESS_LENGTH) throw addressTooLong("to", reply.to.length);
|
|
336
340
|
validateAddressParts("from", reply.from);
|
|
337
341
|
validateAddressParts("to", reply.to);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
rawBytes = reply.raw;
|
|
344
|
-
} else {
|
|
345
|
-
const raw = reply.raw;
|
|
346
|
-
if (typeof raw !== "object" || raw === null || typeof raw.getReader !== "function") throw new MimeError("MIME_ERROR", "reply: expected { from, to, raw } with raw as a Uint8Array or ReadableStream. A native EmailMessage cannot be replayed from a Void handler — its body has no public accessor. Use replyEmail() from void/email, or pass the MIME bytes as raw.");
|
|
347
|
-
const limit = Math.min(MAX_MESSAGE_BYTES, remaining);
|
|
348
|
-
rawBytes = await readStreamToBytes(reply.raw, limit, (total) => limit === 10485760 ? replyTooLarge(total) : replyBudgetExhausted(replyBytesUsed + total));
|
|
349
|
-
}
|
|
350
|
-
replyBytesUsed += rawBytes.byteLength;
|
|
351
|
-
actions.push({
|
|
352
|
-
type: "reply",
|
|
353
|
-
from: reply.from,
|
|
354
|
-
to: reply.to,
|
|
355
|
-
raw: rawBytes
|
|
342
|
+
pendingReplies++;
|
|
343
|
+
const previousReply = replyRecordingTail;
|
|
344
|
+
let releaseReply;
|
|
345
|
+
replyRecordingTail = new Promise((resolve) => {
|
|
346
|
+
releaseReply = resolve;
|
|
356
347
|
});
|
|
348
|
+
await previousReply;
|
|
349
|
+
try {
|
|
350
|
+
if (rejected) {
|
|
351
|
+
console.warn("[void/email] reply() completed after setReject() — ignored. CF rejects are terminal: nothing else runs once setReject fires.");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const replyFrameBytes = emailActionWireSize({
|
|
355
|
+
type: "reply",
|
|
356
|
+
from: reply.from,
|
|
357
|
+
to: reply.to,
|
|
358
|
+
raw: /* @__PURE__ */ new Uint8Array()
|
|
359
|
+
});
|
|
360
|
+
const remaining = EMAIL_MAX_RESPONSE_BYTES - actionWireBytesUsed - replyFrameBytes;
|
|
361
|
+
if (remaining < 0) throw replyBudgetExhausted(EMAIL_MAX_RESPONSE_BYTES - remaining);
|
|
362
|
+
let rawBytes;
|
|
363
|
+
if (reply.raw instanceof Uint8Array) {
|
|
364
|
+
if (reply.raw.byteLength > MAX_REPLY_BYTES) throw replyTooLarge(reply.raw.byteLength);
|
|
365
|
+
if (reply.raw.byteLength > remaining) throw replyBudgetExhausted(actionWireBytesUsed + replyFrameBytes + reply.raw.byteLength);
|
|
366
|
+
rawBytes = reply.raw;
|
|
367
|
+
} else {
|
|
368
|
+
const raw = reply.raw;
|
|
369
|
+
if (typeof raw !== "object" || raw === null || typeof raw.getReader !== "function") throw new MimeError("MIME_ERROR", "reply: expected { from, to, raw } with raw as a Uint8Array or ReadableStream. A native EmailMessage cannot be replayed from a Void handler — its body has no public accessor. Use replyEmail() from void/email, or pass the MIME bytes as raw.");
|
|
370
|
+
const limit = Math.min(MAX_REPLY_BYTES, remaining);
|
|
371
|
+
rawBytes = await readStreamToBytes(reply.raw, limit, (total) => limit === MAX_REPLY_BYTES ? replyTooLarge(total) : replyBudgetExhausted(actionWireBytesUsed + replyFrameBytes + total));
|
|
372
|
+
}
|
|
373
|
+
if (!recordAction({
|
|
374
|
+
type: "reply",
|
|
375
|
+
from: reply.from,
|
|
376
|
+
to: reply.to,
|
|
377
|
+
raw: rawBytes
|
|
378
|
+
})) console.warn("[void/email] reply() completed after setReject() — ignored. CF rejects are terminal: nothing else runs once setReject fires.");
|
|
379
|
+
} finally {
|
|
380
|
+
pendingReplies--;
|
|
381
|
+
releaseReply();
|
|
382
|
+
}
|
|
357
383
|
}
|
|
358
384
|
};
|
|
359
385
|
if (envelope.to !== userVisibleTo) Object.defineProperty(message, "__voidPlatformTo", {
|
|
@@ -362,6 +388,12 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
362
388
|
writable: false,
|
|
363
389
|
configurable: false
|
|
364
390
|
});
|
|
391
|
+
if (typeof envelope.viaDomain === "string" && envelope.viaDomain.length > 0) Object.defineProperty(message, "__voidRelayDomain", {
|
|
392
|
+
value: envelope.viaDomain,
|
|
393
|
+
enumerable: false,
|
|
394
|
+
writable: false,
|
|
395
|
+
configurable: false
|
|
396
|
+
});
|
|
365
397
|
proxyRawCache.set(message, { read: readRawOnce });
|
|
366
398
|
return {
|
|
367
399
|
message,
|
|
@@ -592,6 +624,7 @@ async function buildReplyMessage(message, opts) {
|
|
|
592
624
|
const originalReferences = headers.get("References") ?? "";
|
|
593
625
|
let fromAddr;
|
|
594
626
|
if (opts.from !== void 0) fromAddr = opts.from;
|
|
627
|
+
else if (readRelayDomain(message) !== void 0) fromAddr = message.to;
|
|
595
628
|
else {
|
|
596
629
|
const slug = tryReadProjectSlug();
|
|
597
630
|
if (slug) {
|
|
@@ -629,8 +662,8 @@ async function buildReplyMessage(message, opts) {
|
|
|
629
662
|
/**
|
|
630
663
|
* Send a threading-aware reply to an inbound message. Builds the reply MIME
|
|
631
664
|
* (correct `In-Reply-To` / `References` / `Subject: Re: …`) and records a
|
|
632
|
-
* reply intent on the message proxy.
|
|
633
|
-
*
|
|
665
|
+
* reply intent on the message proxy. Delivery follows handler completion and,
|
|
666
|
+
* on the platform, authoritative admission.
|
|
634
667
|
*
|
|
635
668
|
* The threading headers are best-effort, since the inbound values they are
|
|
636
669
|
* built from are remote-sender controlled: an id no header line can carry
|
|
@@ -646,16 +679,32 @@ async function replyEmail(message, opts) {
|
|
|
646
679
|
});
|
|
647
680
|
}
|
|
648
681
|
/**
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* `EmailEnvelope` from the platform router. Functions do not survive RPC,
|
|
652
|
-
* so their presence is the discriminator.
|
|
682
|
+
* Recognize native email operations. This structural check is not an
|
|
683
|
+
* authentication boundary; managed entrypoints require receipt authentication.
|
|
653
684
|
*/
|
|
654
685
|
function isForwardableEmailMessage(value) {
|
|
655
686
|
if (typeof value !== "object" || value === null) return false;
|
|
656
687
|
const m = value;
|
|
657
688
|
return typeof m.setReject === "function" && typeof m.forward === "function" && typeof m.reply === "function";
|
|
658
689
|
}
|
|
690
|
+
const INBOUND_PROVIDER_DEADLINE_MS = 3e4;
|
|
691
|
+
const INBOUND_ACTION_BATCH_DEADLINE_MS = 6e4;
|
|
692
|
+
function createInboundProviderDeadline() {
|
|
693
|
+
const deadline = Date.now() + INBOUND_ACTION_BATCH_DEADLINE_MS;
|
|
694
|
+
return { async run(submit) {
|
|
695
|
+
const remaining = Math.min(INBOUND_PROVIDER_DEADLINE_MS, deadline - Date.now());
|
|
696
|
+
if (remaining <= 0) throw new Error("email action batch deadline elapsed before provider submission");
|
|
697
|
+
const work = submit();
|
|
698
|
+
let timer;
|
|
699
|
+
try {
|
|
700
|
+
return await Promise.race([work, new Promise((_, reject) => {
|
|
701
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("email action provider timed out; outcome unknown, do not retry automatically")), remaining);
|
|
702
|
+
})]);
|
|
703
|
+
} finally {
|
|
704
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
705
|
+
}
|
|
706
|
+
} };
|
|
707
|
+
}
|
|
659
708
|
/**
|
|
660
709
|
* Replay recorded `EmailAction`s on a real `ForwardableEmailMessage`, in
|
|
661
710
|
* order. The proxy already made `setReject` terminal (earlier forward/reply
|
|
@@ -664,28 +713,21 @@ function isForwardableEmailMessage(value) {
|
|
|
664
713
|
* duck-typed object — so `cloudflare:email` is loaded here, and only when
|
|
665
714
|
* a reply was recorded.
|
|
666
715
|
*
|
|
667
|
-
* The
|
|
668
|
-
*
|
|
669
|
-
* `forward()` already ran the record through that constructor, so a bad
|
|
670
|
-
* record failed inside the handler, before any replay. (`EmailMessage`
|
|
671
|
-
* construction neither validates nor reads its stream — measured under
|
|
672
|
-
* workerd.) A reject's reason reaches the native `setReject()` as the proxy
|
|
673
|
-
* recorded it — already sliced and stripped the way the platform router
|
|
674
|
-
* would (`normalizeRejectReason`), so the SMTP reply is the same in both
|
|
675
|
-
* deployments. What Cloudflare itself refuses at `forward()` / `reply()`
|
|
676
|
-
* still throws out of the loop, as any native call would.
|
|
716
|
+
* The proxy already validated forward headers and normalized reject reasons.
|
|
717
|
+
* Provider refusals still propagate to the native event caller.
|
|
677
718
|
*/
|
|
678
719
|
async function enactEmailActions(message, actions) {
|
|
720
|
+
const providerDeadline = createInboundProviderDeadline();
|
|
679
721
|
for (const action of actions) switch (action.type) {
|
|
680
722
|
case "reject":
|
|
681
723
|
message.setReject(action.reason);
|
|
682
724
|
break;
|
|
683
725
|
case "forward":
|
|
684
|
-
await message.forward(action.to, action.headers ? new Headers(action.headers) : void 0);
|
|
726
|
+
await providerDeadline.run(() => message.forward(action.to, action.headers ? new Headers(action.headers) : void 0));
|
|
685
727
|
break;
|
|
686
728
|
case "reply": {
|
|
687
729
|
const { EmailMessage } = await import("cloudflare:email");
|
|
688
|
-
await message.reply(new EmailMessage(action.from, action.to, bytesToReadableStream(action.raw)));
|
|
730
|
+
await providerDeadline.run(() => message.reply(new EmailMessage(action.from, action.to, bytesToReadableStream(action.raw))));
|
|
689
731
|
break;
|
|
690
732
|
}
|
|
691
733
|
}
|