void 0.20.1 → 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-CAH62yDU.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
- package/dist/auth-link-NZdjCmSc.mjs +28 -0
- package/dist/{build-cmd-CJvZvPQO.mjs → build-cmd-sI18tX_O.mjs} +3 -3
- package/dist/{cache-BlNeQjuP.mjs → cache-IHn5MwBC.mjs} +3 -3
- package/dist/{cancel-deploy-CmlAZ9P6.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
- package/dist/cf-access-DRsQRe6k.mjs +75 -0
- package/dist/cli/cli.mjs +308 -1958
- package/dist/cli/env-schema-probe.mjs +11 -2
- package/dist/{client-Clirrol3.mjs → client-dHfSJvAN.mjs} +300 -81
- package/dist/{cloudflare-auth-B1QtTO1b.mjs → cloudflare-auth-6M5llVPC.mjs} +2 -2
- package/dist/{cloudflare-cmd-B6_OZx2V.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
- package/dist/{cloudflare-connect-j5D4hhrG.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-C04Wdy_h.mjs → connect-Bfk31O_8.mjs} +6 -6
- package/dist/{create-project-ChGZ1DFd.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
- package/dist/{db-D2d_mUsB.mjs → db-BkRoptAt.mjs} +47 -30
- package/dist/{delete-D8GigDk8.mjs → delete-DouASY9P.mjs} +3 -3
- package/dist/{deploy-iXZ3F0N6.mjs → deploy-DTaWUS1S.mjs} +121 -110
- package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
- package/dist/{domain-B1VmoSr0.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-BcQzYgoG.mjs → env-DJHsPE7Z.mjs} +5 -5
- package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
- package/dist/{gen-DI2YwdBM.mjs → gen-B_wPnVTK.mjs} +2 -2
- package/dist/{github-cmd-xItS5Zwf.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-afAcWeQ9.d.mts → inbound-CH5Mksyy.d.mts} +34 -48
- package/dist/{inbound-2d0zi2yS.mjs → inbound-aVHEUhKo.mjs} +130 -100
- package/dist/index.mjs +54 -17
- package/dist/{init-BD-9THgn.mjs → init-BWZ7q5Z4.mjs} +11 -11
- package/dist/{link-RMdgjF1v.mjs → link-Rmvu2Wl_.mjs} +4 -4
- package/dist/{list-3F52R_yO.mjs → list-DEE2S6mY.mjs} +4 -4
- package/dist/{login-pV69H-ZO.mjs → login-Uvferzmm.mjs} +28 -10
- package/dist/{logs-DFHHD6wE.mjs → logs-27FenuiC.mjs} +4 -4
- package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
- package/dist/{node-Dk3H2jmU.mjs → node-Ez5KW5rn.mjs} +2 -2
- 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-DxJ2FRwR.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
- package/dist/{platform-domain-ChvbJkdy.mjs → platform-domain-C74PULqV.mjs} +4 -4
- package/dist/{platform-lifecycle-DN4MzJF_.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
- package/dist/{platform-management-Db2PXw0B.mjs → platform-management-COogu_Se.mjs} +33 -7
- package/dist/{platform-recovery-C_YO-tIs.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-Mo0V9yKS.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-BcKOVpRg.mjs → requests-CUExwGQQ.mjs} +3 -3
- package/dist/{rollback-Bx85-0xh.mjs → rollback-CDNGU1gr.mjs} +4 -4
- 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-ByhJ9AMl.mjs → secret-Bzzi2e9E.mjs} +5 -5
- package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
- package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
- 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 +102 -110
- package/skills/void/docs/guide/platform-administration.md +214 -2
- package/skills/void/docs/guide/platform-development.md +64 -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 +249 -21
- package/dist/cf-access-AJ1ehiFR.mjs +0 -42
- package/dist/cf-access-DsSsZUPr.mjs +0 -67
- package/dist/email-uKyQYUVY.mjs +0 -1016
|
@@ -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,40 +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;
|
|
218
223
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* domains on Cloudflare"). Undefined on the platform mail zone. A relayed
|
|
222
|
-
* recipient carries no `<slug>+` prefix, so the generated dispatcher runs
|
|
223
|
-
* in no-prefix mode for it (Mode B item 4) and `replyEmail` defaults
|
|
224
|
-
* `from` to the address that received it, on this domain.
|
|
224
|
+
* Exact custom domain selected by the gateway. Custom-domain recipients
|
|
225
|
+
* have no project-slug prefix; replies default to the received address.
|
|
225
226
|
*/
|
|
226
227
|
viaDomain?: string;
|
|
227
228
|
}
|
|
228
|
-
/**
|
|
229
|
-
* What the user handler decided to do with a message. Returned from the
|
|
230
|
-
* worker's `email(envelope)` RPC method as an array (handlers may chain
|
|
231
|
-
* multiple actions, e.g. forward then reply); the router enacts each on the
|
|
232
|
-
* real `ForwardableEmailMessage` in order.
|
|
233
|
-
*/
|
|
229
|
+
/** Handler intents, validated and admitted by the gateway before delivery. */
|
|
234
230
|
type EmailAction = {
|
|
235
231
|
type: "reject";
|
|
236
232
|
reason: string;
|
|
@@ -270,7 +266,7 @@ interface CreateMessageProxyOptions {
|
|
|
270
266
|
* accept a native `EmailMessage` from `cloudflare:email`: that class exposes
|
|
271
267
|
* only `from` and `to` (workers-types declares nothing else, and the body
|
|
272
268
|
* has no public accessor), so its bytes cannot be recorded here, and nothing
|
|
273
|
-
* could
|
|
269
|
+
* could encode them for the gateway. The reply MIME bytes are
|
|
274
270
|
* buffered before being recorded so the action is fully self-contained.
|
|
275
271
|
*
|
|
276
272
|
* When `opts.slug` is provided, `message.to` is rewritten to remove the
|
|
@@ -323,8 +319,8 @@ interface ReplyEmailOptions {
|
|
|
323
319
|
/**
|
|
324
320
|
* Send a threading-aware reply to an inbound message. Builds the reply MIME
|
|
325
321
|
* (correct `In-Reply-To` / `References` / `Subject: Re: …`) and records a
|
|
326
|
-
* reply intent on the message proxy.
|
|
327
|
-
*
|
|
322
|
+
* reply intent on the message proxy. Delivery follows handler completion and,
|
|
323
|
+
* on the platform, authoritative admission.
|
|
328
324
|
*
|
|
329
325
|
* The threading headers are best-effort, since the inbound values they are
|
|
330
326
|
* built from are remote-sender controlled: an id no header line can carry
|
|
@@ -333,10 +329,8 @@ interface ReplyEmailOptions {
|
|
|
333
329
|
*/
|
|
334
330
|
declare function replyEmail(message: ForwardableEmailMessage, opts: ReplyEmailOptions): Promise<void>;
|
|
335
331
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* `EmailEnvelope` from the platform router. Functions do not survive RPC,
|
|
339
|
-
* 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.
|
|
340
334
|
*/
|
|
341
335
|
declare function isForwardableEmailMessage(value: unknown): value is ForwardableEmailMessage;
|
|
342
336
|
/**
|
|
@@ -347,17 +341,9 @@ declare function isForwardableEmailMessage(value: unknown): value is Forwardable
|
|
|
347
341
|
* duck-typed object — so `cloudflare:email` is loaded here, and only when
|
|
348
342
|
* a reply was recorded.
|
|
349
343
|
*
|
|
350
|
-
* The
|
|
351
|
-
*
|
|
352
|
-
* `forward()` already ran the record through that constructor, so a bad
|
|
353
|
-
* record failed inside the handler, before any replay. (`EmailMessage`
|
|
354
|
-
* construction neither validates nor reads its stream — measured under
|
|
355
|
-
* workerd.) A reject's reason reaches the native `setReject()` as the proxy
|
|
356
|
-
* recorded it — already sliced and stripped the way the platform router
|
|
357
|
-
* would (`normalizeRejectReason`), so the SMTP reply is the same in both
|
|
358
|
-
* deployments. What Cloudflare itself refuses at `forward()` / `reply()`
|
|
359
|
-
* 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.
|
|
360
346
|
*/
|
|
361
347
|
declare function enactEmailActions(message: ForwardableEmailMessage, actions: ReadonlyArray<EmailAction>): Promise<void>;
|
|
362
348
|
//#endregion
|
|
363
|
-
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 {
|
|
@@ -108,7 +78,7 @@ var ParseEmailError = class extends Error {
|
|
|
108
78
|
* one-shot.
|
|
109
79
|
*/
|
|
110
80
|
async function parseEmail(message) {
|
|
111
|
-
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.`);
|
|
112
82
|
const { default: PostalMime } = await import("postal-mime");
|
|
113
83
|
const cached = proxyRawCache.get(message);
|
|
114
84
|
try {
|
|
@@ -138,10 +108,10 @@ function stripSlugFromAddress(address, slug) {
|
|
|
138
108
|
return address;
|
|
139
109
|
}
|
|
140
110
|
function replyTooLarge(size) {
|
|
141
|
-
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.`);
|
|
142
112
|
}
|
|
143
113
|
function replyBudgetExhausted(size) {
|
|
144
|
-
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.`);
|
|
145
115
|
}
|
|
146
116
|
function queueFull() {
|
|
147
117
|
return new MimeError("MIME_ERROR", `email: handler queued more than ${MAX_ACTIONS} actions for one message.`);
|
|
@@ -204,15 +174,29 @@ async function readStreamToBytes(stream, limit = Infinity, onOverflow = replyToo
|
|
|
204
174
|
const reader = stream.getReader();
|
|
205
175
|
const chunks = [];
|
|
206
176
|
let total = 0;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
214
192
|
}
|
|
215
|
-
|
|
193
|
+
} catch (error) {
|
|
194
|
+
reader.cancel().catch(() => {});
|
|
195
|
+
throw error;
|
|
196
|
+
} finally {
|
|
197
|
+
try {
|
|
198
|
+
reader.releaseLock();
|
|
199
|
+
} catch {}
|
|
216
200
|
}
|
|
217
201
|
const out = new Uint8Array(total);
|
|
218
202
|
let offset = 0;
|
|
@@ -239,7 +223,7 @@ function bytesToReadableStream(bytes) {
|
|
|
239
223
|
* accept a native `EmailMessage` from `cloudflare:email`: that class exposes
|
|
240
224
|
* only `from` and `to` (workers-types declares nothing else, and the body
|
|
241
225
|
* has no public accessor), so its bytes cannot be recorded here, and nothing
|
|
242
|
-
* could
|
|
226
|
+
* could encode them for the gateway. The reply MIME bytes are
|
|
243
227
|
* buffered before being recorded so the action is fully self-contained.
|
|
244
228
|
*
|
|
245
229
|
* When `opts.slug` is provided, `message.to` is rewritten to remove the
|
|
@@ -270,9 +254,20 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
270
254
|
}
|
|
271
255
|
if (envelope.raw instanceof Uint8Array) cachedRaw = envelope.raw;
|
|
272
256
|
let rejected = false;
|
|
273
|
-
let
|
|
274
|
-
|
|
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;
|
|
275
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();
|
|
276
271
|
}
|
|
277
272
|
const message = {
|
|
278
273
|
from: envelope.from,
|
|
@@ -300,10 +295,12 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
300
295
|
}
|
|
301
296
|
}
|
|
302
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.`);
|
|
303
|
-
|
|
298
|
+
const action = {
|
|
304
299
|
type: "reject",
|
|
305
300
|
reason: normalizeRejectReason(reason)
|
|
306
|
-
}
|
|
301
|
+
};
|
|
302
|
+
actions.push(action);
|
|
303
|
+
actionWireBytesUsed = EMAIL_ACTION_TERMINAL_BYTES + emailActionWireSize(action);
|
|
307
304
|
},
|
|
308
305
|
async forward(to, fwdHeaders) {
|
|
309
306
|
if (rejected) {
|
|
@@ -324,11 +321,13 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
324
321
|
headersObj = { ...fwdHeaders };
|
|
325
322
|
}
|
|
326
323
|
if (headersObj) assertForwardHeadersWithinBounds(headersObj);
|
|
327
|
-
|
|
324
|
+
const action = {
|
|
328
325
|
type: "forward",
|
|
329
326
|
to,
|
|
330
327
|
...headersObj ? { headers: headersObj } : {}
|
|
331
|
-
}
|
|
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);
|
|
332
331
|
},
|
|
333
332
|
async reply(reply) {
|
|
334
333
|
if (rejected) {
|
|
@@ -340,25 +339,47 @@ function createMessageProxy(envelope, opts = {}) {
|
|
|
340
339
|
if (reply.to.length > MAX_ACTION_ADDRESS_LENGTH) throw addressTooLong("to", reply.to.length);
|
|
341
340
|
validateAddressParts("from", reply.from);
|
|
342
341
|
validateAddressParts("to", reply.to);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
rawBytes = reply.raw;
|
|
349
|
-
} else {
|
|
350
|
-
const raw = reply.raw;
|
|
351
|
-
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.");
|
|
352
|
-
const limit = Math.min(MAX_MESSAGE_BYTES, remaining);
|
|
353
|
-
rawBytes = await readStreamToBytes(reply.raw, limit, (total) => limit === 10485760 ? replyTooLarge(total) : replyBudgetExhausted(replyBytesUsed + total));
|
|
354
|
-
}
|
|
355
|
-
replyBytesUsed += rawBytes.byteLength;
|
|
356
|
-
actions.push({
|
|
357
|
-
type: "reply",
|
|
358
|
-
from: reply.from,
|
|
359
|
-
to: reply.to,
|
|
360
|
-
raw: rawBytes
|
|
342
|
+
pendingReplies++;
|
|
343
|
+
const previousReply = replyRecordingTail;
|
|
344
|
+
let releaseReply;
|
|
345
|
+
replyRecordingTail = new Promise((resolve) => {
|
|
346
|
+
releaseReply = resolve;
|
|
361
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
|
+
}
|
|
362
383
|
}
|
|
363
384
|
};
|
|
364
385
|
if (envelope.to !== userVisibleTo) Object.defineProperty(message, "__voidPlatformTo", {
|
|
@@ -641,8 +662,8 @@ async function buildReplyMessage(message, opts) {
|
|
|
641
662
|
/**
|
|
642
663
|
* Send a threading-aware reply to an inbound message. Builds the reply MIME
|
|
643
664
|
* (correct `In-Reply-To` / `References` / `Subject: Re: …`) and records a
|
|
644
|
-
* reply intent on the message proxy.
|
|
645
|
-
*
|
|
665
|
+
* reply intent on the message proxy. Delivery follows handler completion and,
|
|
666
|
+
* on the platform, authoritative admission.
|
|
646
667
|
*
|
|
647
668
|
* The threading headers are best-effort, since the inbound values they are
|
|
648
669
|
* built from are remote-sender controlled: an id no header line can carry
|
|
@@ -658,16 +679,32 @@ async function replyEmail(message, opts) {
|
|
|
658
679
|
});
|
|
659
680
|
}
|
|
660
681
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
* `EmailEnvelope` from the platform router. Functions do not survive RPC,
|
|
664
|
-
* 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.
|
|
665
684
|
*/
|
|
666
685
|
function isForwardableEmailMessage(value) {
|
|
667
686
|
if (typeof value !== "object" || value === null) return false;
|
|
668
687
|
const m = value;
|
|
669
688
|
return typeof m.setReject === "function" && typeof m.forward === "function" && typeof m.reply === "function";
|
|
670
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
|
+
}
|
|
671
708
|
/**
|
|
672
709
|
* Replay recorded `EmailAction`s on a real `ForwardableEmailMessage`, in
|
|
673
710
|
* order. The proxy already made `setReject` terminal (earlier forward/reply
|
|
@@ -676,28 +713,21 @@ function isForwardableEmailMessage(value) {
|
|
|
676
713
|
* duck-typed object — so `cloudflare:email` is loaded here, and only when
|
|
677
714
|
* a reply was recorded.
|
|
678
715
|
*
|
|
679
|
-
* The
|
|
680
|
-
*
|
|
681
|
-
* `forward()` already ran the record through that constructor, so a bad
|
|
682
|
-
* record failed inside the handler, before any replay. (`EmailMessage`
|
|
683
|
-
* construction neither validates nor reads its stream — measured under
|
|
684
|
-
* workerd.) A reject's reason reaches the native `setReject()` as the proxy
|
|
685
|
-
* recorded it — already sliced and stripped the way the platform router
|
|
686
|
-
* would (`normalizeRejectReason`), so the SMTP reply is the same in both
|
|
687
|
-
* deployments. What Cloudflare itself refuses at `forward()` / `reply()`
|
|
688
|
-
* 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.
|
|
689
718
|
*/
|
|
690
719
|
async function enactEmailActions(message, actions) {
|
|
720
|
+
const providerDeadline = createInboundProviderDeadline();
|
|
691
721
|
for (const action of actions) switch (action.type) {
|
|
692
722
|
case "reject":
|
|
693
723
|
message.setReject(action.reason);
|
|
694
724
|
break;
|
|
695
725
|
case "forward":
|
|
696
|
-
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));
|
|
697
727
|
break;
|
|
698
728
|
case "reply": {
|
|
699
729
|
const { EmailMessage } = await import("cloudflare:email");
|
|
700
|
-
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))));
|
|
701
731
|
break;
|
|
702
732
|
}
|
|
703
733
|
}
|