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.
Files changed (93) hide show
  1. package/README.md +5 -1
  2. package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
  3. package/dist/{auth-cmd-CYVhSNFy.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
  4. package/dist/auth-link-NZdjCmSc.mjs +28 -0
  5. package/dist/{build-cmd-CkVD1uOh.mjs → build-cmd-sI18tX_O.mjs} +3 -3
  6. package/dist/{cache-QcUfR-ff.mjs → cache-IHn5MwBC.mjs} +3 -3
  7. package/dist/{cancel-deploy-DsvWKFTe.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
  8. package/dist/cf-access-DRsQRe6k.mjs +75 -0
  9. package/dist/cli/cli.mjs +364 -1883
  10. package/dist/cli/env-schema-probe.mjs +11 -2
  11. package/dist/{client-CyCHWSO_.mjs → client-dHfSJvAN.mjs} +336 -58
  12. package/dist/{cloudflare-auth-DRkGe7-s.mjs → cloudflare-auth-6M5llVPC.mjs} +53 -6
  13. package/dist/{cloudflare-cmd-D3ME2GAe.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
  14. package/dist/{cloudflare-connect-BivMefMA.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
  15. package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
  16. package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
  17. package/dist/{connect-D9Yr-T5f.mjs → connect-Bfk31O_8.mjs} +6 -6
  18. package/dist/{create-project-DMV-csEm.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
  19. package/dist/{db-BxoUWL0F.mjs → db-BkRoptAt.mjs} +75 -48
  20. package/dist/{delete-iJOqxBz1.mjs → delete-DouASY9P.mjs} +3 -3
  21. package/dist/{deploy-CcDoeBIf.mjs → deploy-DTaWUS1S.mjs} +137 -128
  22. package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
  23. package/dist/{domain-gu_iaHmn.mjs → domain-1RhhOVrC.mjs} +4 -4
  24. package/dist/email-C-lGh51B.mjs +795 -0
  25. package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
  26. package/dist/{env-DmU2To0C.mjs → env-DJHsPE7Z.mjs} +5 -5
  27. package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
  28. package/dist/{gen-BKw6qHIg.mjs → gen-B_wPnVTK.mjs} +3 -3
  29. package/dist/generate-RTK8_kK1.mjs +47 -0
  30. package/dist/{github-cmd-DJS5Ab-H.mjs → github-cmd-PW7ZnWTp.mjs} +3 -3
  31. package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
  32. package/dist/help-CwOX-zmI.mjs +2216 -0
  33. package/dist/{inbound-BJ70in1n.d.mts → inbound-CH5Mksyy.d.mts} +37 -42
  34. package/dist/{inbound-CNKxb3FY.mjs → inbound-aVHEUhKo.mjs} +143 -101
  35. package/dist/index.mjs +69 -30
  36. package/dist/{init-Dx5cgKqK.mjs → init-BWZ7q5Z4.mjs} +11 -11
  37. package/dist/{link-D_rm5sRH.mjs → link-Rmvu2Wl_.mjs} +4 -4
  38. package/dist/{list-M8XShti7.mjs → list-DEE2S6mY.mjs} +4 -4
  39. package/dist/{local-d1-BE8KBbMy.mjs → local-d1-D2I6Ox5F.mjs} +1 -1
  40. package/dist/{login-C8-UuLnp.mjs → login-Uvferzmm.mjs} +28 -10
  41. package/dist/{logs-DwFU7dMW.mjs → logs-27FenuiC.mjs} +4 -4
  42. package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
  43. package/dist/{node-BkaXcpAc.mjs → node-Ez5KW5rn.mjs} +3 -3
  44. package/dist/operator-auth-B3e08unv.mjs +52 -0
  45. package/dist/operator-client-LUZnlnYk.mjs +82 -0
  46. package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CjOTmAYE.mjs} +35 -55
  47. package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
  48. package/dist/pages/index.mjs +2 -2
  49. package/dist/platform-auth-config-DrbQXXiW.mjs +368 -0
  50. package/dist/platform-auth-protection-Bhtvp0B_.mjs +219 -0
  51. package/dist/platform-auth-recovery-CeOKGVeJ.mjs +310 -0
  52. package/dist/{platform-cmd-BEOVv1PN.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
  53. package/dist/{platform-domain-BszUfiS8.mjs → platform-domain-C74PULqV.mjs} +4 -4
  54. package/dist/{platform-lifecycle-CjSr6xf0.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
  55. package/dist/{platform-management-W30iCaTL.mjs → platform-management-COogu_Se.mjs} +33 -7
  56. package/dist/{platform-recovery-pHHv4ZSg.mjs → platform-recovery-ewqLefp1.mjs} +6 -5
  57. package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
  58. package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
  59. package/dist/prerender-render.d.mts +11 -0
  60. package/dist/prerender-render.mjs +111 -0
  61. package/dist/{project-cmd-DCpk1cDt.mjs → project-cmd-DmZK9Hxf.mjs} +32 -14
  62. package/dist/project-team-D8jOJMUJ.mjs +130 -0
  63. package/dist/project-token-DA34bf-C.mjs +75 -0
  64. package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
  65. package/dist/{requests-Dn8Vheh1.mjs → requests-CUExwGQQ.mjs} +3 -3
  66. package/dist/{rollback-CtlPXEBi.mjs → rollback-CDNGU1gr.mjs} +4 -4
  67. package/dist/{runner-mysql-CgRFl3s6.mjs → runner-mysql-7BPUNGmL.mjs} +1 -1
  68. package/dist/{runner-pg-DCkWPsWS.mjs → runner-pg-BkEza-dX.mjs} +1 -1
  69. package/dist/runtime/ai.mjs +3 -2
  70. package/dist/runtime/email/testing.d.mts +1 -1
  71. package/dist/runtime/email/testing.mjs +3 -3
  72. package/dist/runtime/email-protocol.d.mts +15 -0
  73. package/dist/runtime/email-protocol.mjs +70 -0
  74. package/dist/runtime/email.d.mts +2 -2
  75. package/dist/runtime/email.mjs +189 -96
  76. package/dist/runtime/remote/index.mjs +5 -3
  77. package/dist/{secret-BqTxGqki.mjs → secret-Bzzi2e9E.mjs} +5 -5
  78. package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
  79. package/dist/sqlite-validation-BzKMWnO4.mjs +25 -0
  80. package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
  81. package/dist/{validate-Bihr8WBi.mjs → validate-tBBN_dXH.mjs} +1 -0
  82. package/package.json +12 -7
  83. package/skills/void/SKILL.md +35 -4
  84. package/skills/void/docs/guide/deployment.md +2 -0
  85. package/skills/void/docs/guide/email.md +121 -98
  86. package/skills/void/docs/guide/platform-administration.md +214 -2
  87. package/skills/void/docs/guide/platform-development.md +93 -2
  88. package/skills/void/docs/guide/project-collaboration.md +94 -0
  89. package/skills/void/docs/guide/self-hosted-platform.md +184 -30
  90. package/skills/void/docs/reference/cli.md +294 -15
  91. package/dist/cf-access-AJ1ehiFR.mjs +0 -42
  92. package/dist/cf-access-DsSsZUPr.mjs +0 -67
  93. package/dist/email-r6DHJyAB.mjs +0 -262
@@ -1,6 +1,6 @@
1
- import { C as cancel, D as log, E as intro, F as isCancel, a as dim, c as import_picocolors, i as createSpinner, j as select, k as outro, o as ensureUtc, r as cliTitle, w as confirm } from "./output-tFQLLj26.mjs";
2
- import { b as readDeploymentPlatform, c as getToken, x as readProjectConfig } from "./auth-W9WII-mN.mjs";
3
- import { n as PlatformClient } from "./client-CyCHWSO_.mjs";
1
+ import { A as outro, C as cancel, D as log, E as intro, I as isCancel, M as select, a as dim, c as import_picocolors, i as createSpinner, o as ensureUtc, r as cliTitle, w as confirm } from "./output-B0cfNSx5.mjs";
2
+ import { S as readProjectConfig, l as getToken, x as readDeploymentPlatform } from "./auth-DPl6kck4.mjs";
3
+ import { n as PlatformClient } from "./client-dHfSJvAN.mjs";
4
4
  //#region src/cli/rollback.ts
5
5
  async function runRollbackCommand(root, deployId) {
6
6
  console.log();
@@ -81,7 +81,7 @@ async function runRollbackCommand(root, deployId) {
81
81
  outro(dim("Traffic is now serving the rolled-back deployment."));
82
82
  }
83
83
  async function runCloudflareRollback(root, deployId) {
84
- const { activateCloudflareExistingVersion, readCloudflareWorkerStatus, resolveCloudflareOperationContext } = await import("./cloudflare-operations-CPTpRW6d.mjs").then((n) => n.t);
84
+ const { activateCloudflareExistingVersion, readCloudflareWorkerStatus, resolveCloudflareOperationContext } = await import("./cloudflare-operations-BzWnlC1_.mjs").then((n) => n.t);
85
85
  const context = resolveCloudflareOperationContext(root);
86
86
  const status = await readCloudflareWorkerStatus(context);
87
87
  const deployedVersionIds = new Set(status.deployedVersionIds);
@@ -1,5 +1,5 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-rQ84J-ij.mjs";
2
- import { c as collectMigrations } from "./validate-Bihr8WBi.mjs";
2
+ import { c as collectMigrations } from "./validate-tBBN_dXH.mjs";
3
3
  //#region src/migrations/runner-mysql.ts
4
4
  var runner_mysql_exports = /* @__PURE__ */ __exportAll({
5
5
  applyMySqlMigrations: () => applyMySqlMigrations,
@@ -1,5 +1,5 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-rQ84J-ij.mjs";
2
- import { c as collectMigrations } from "./validate-Bihr8WBi.mjs";
2
+ import { c as collectMigrations } from "./validate-tBBN_dXH.mjs";
3
3
  //#region src/migrations/runner-pg.ts
4
4
  var runner_pg_exports = /* @__PURE__ */ __exportAll({
5
5
  applyPgMigrations: () => applyPgMigrations,
@@ -1,4 +1,4 @@
1
- import { t as cfAccessHeaders } from "../cf-access-AJ1ehiFR.mjs";
1
+ import { t as cfAccessHeaders } from "../cf-access-DRsQRe6k.mjs";
2
2
  import { a as getRawRuntimeEnv } from "../env-raw-Cx8ElDdj.mjs";
3
3
  //#region src/runtime/ai-types.ts
4
4
  /** Maps provider prefix to expected env var name for the API key */
@@ -392,7 +392,8 @@ function makeHttpBackend(token, projectId, runtimeEnv) {
392
392
  action,
393
393
  ...body,
394
394
  projectId
395
- })
395
+ }),
396
+ redirect: "manual"
396
397
  });
397
398
  if (options.throwOnError !== false && !res.ok) {
398
399
  const text = await res.text();
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
- import { r as EmailHandler } from "../../inbound-BJ70in1n.mjs";
2
+ import { r as EmailHandler } from "../../inbound-CH5Mksyy.mjs";
3
3
  //#region src/runtime/email/dev-inbox.d.ts
4
4
  interface CapturedMessage {
5
5
  id: string;
@@ -1,7 +1,7 @@
1
- import { i as normalizeEmail, r as buildMime } from "../../mime-BJD7d_qL.mjs";
2
- import { y as registerEmailSink } from "../../dev-inbox-DkgRWLkW.mjs";
1
+ import { i as normalizeEmail, r as buildMime } from "../../mime-D5Nmdzf7.mjs";
2
+ import { y as registerEmailSink } from "../../dev-inbox-P0u4tM8Y.mjs";
3
3
  import { n as compileEmailPattern, t as compareEmailRoutes } from "../../email-Ce6SQq-i.mjs";
4
- import { i as createMessageProxy, n as MAX_INBOUND_HEADERS, t as MAX_INBOUND_BYTES } from "../../inbound-CNKxb3FY.mjs";
4
+ import { i as createMessageProxy, n as MAX_INBOUND_HEADERS, t as MAX_INBOUND_BYTES } from "../../inbound-aVHEUhKo.mjs";
5
5
  //#region src/runtime/email/testing.ts
6
6
  /**
7
7
  * Test harnesses for void/email.
@@ -0,0 +1,15 @@
1
+ import { EMAIL_PROTOCOL_VERSION, encodeEmailActions, readEmailEnvelope } from "@void/platform/email-protocol";
2
+ //#region src/runtime/email-protocol.d.ts
3
+ type ReceiptVerificationInput = {
4
+ receipt: string;
5
+ projectId: string;
6
+ deploymentId: string;
7
+ recipient: string;
8
+ };
9
+ type ReceiptVerifier = {
10
+ fetch(input: string | Request, init?: RequestInit): Promise<Response>;
11
+ };
12
+ /** Validate a gateway receipt without exposing the signing secret to the tenant worker. */
13
+ export declare function verifyEmailReceiptThroughProxy(proxy: ReceiptVerifier, projectToken: string, input: ReceiptVerificationInput): Promise<boolean>;
14
+ //#endregion
15
+ export { EMAIL_PROTOCOL_VERSION, encodeEmailActions, readEmailEnvelope };
@@ -0,0 +1,70 @@
1
+ import { EMAIL_PROTOCOL_VERSION, encodeEmailActions, readEmailEnvelope } from "@void/platform/email-protocol";
2
+ //#region src/runtime/email-protocol.ts
3
+ const RECEIPT_VERIFICATION_DEADLINE_MS = 5e3;
4
+ const MAX_RECEIPT_VERIFICATION_BYTES = 2048;
5
+ const decoder = new TextDecoder("utf-8", {
6
+ fatal: true,
7
+ ignoreBOM: false
8
+ });
9
+ async function beforeDeadline(deadline, run) {
10
+ const remaining = deadline - Date.now();
11
+ if (remaining <= 0) throw new Error("email receipt verification timeout");
12
+ let timer;
13
+ return Promise.race([run(), new Promise((_, reject) => {
14
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("email receipt verification timeout")), remaining);
15
+ })]).finally(() => {
16
+ if (timer !== void 0) clearTimeout(timer);
17
+ });
18
+ }
19
+ async function readResponseJson(response, deadline) {
20
+ const declaredLength = response.headers.get("content-length");
21
+ if (response.headers.has("content-encoding") || !response.body || declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > MAX_RECEIPT_VERIFICATION_BYTES)) throw new Error("invalid email receipt verification response");
22
+ const expectedLength = declaredLength === null ? null : Number(declaredLength);
23
+ const reader = response.body.getReader();
24
+ const bytes = new Uint8Array(MAX_RECEIPT_VERIFICATION_BYTES);
25
+ let size = 0;
26
+ try {
27
+ for (;;) {
28
+ const chunk = await beforeDeadline(deadline, () => reader.read());
29
+ if (chunk.done) break;
30
+ const nextSize = size + chunk.value.byteLength;
31
+ if (nextSize > bytes.byteLength) throw new Error("email receipt verification response too large");
32
+ bytes.set(chunk.value, size);
33
+ size = nextSize;
34
+ }
35
+ } finally {
36
+ reader.cancel().catch(() => {});
37
+ try {
38
+ reader.releaseLock();
39
+ } catch {}
40
+ }
41
+ if (size === 0 || expectedLength !== null && size !== expectedLength) throw new Error("invalid email receipt verification response length");
42
+ return JSON.parse(decoder.decode(bytes.subarray(0, size)));
43
+ }
44
+ /** Validate a gateway receipt without exposing the signing secret to the tenant worker. */
45
+ async function verifyEmailReceiptThroughProxy(proxy, projectToken, input) {
46
+ const deadline = Date.now() + RECEIPT_VERIFICATION_DEADLINE_MS;
47
+ const controller = new AbortController();
48
+ let response;
49
+ try {
50
+ response = await beforeDeadline(deadline, () => proxy.fetch("https://proxy/email/verify-receipt", {
51
+ method: "POST",
52
+ headers: {
53
+ "content-type": "application/json",
54
+ "x-void-internal": projectToken
55
+ },
56
+ body: JSON.stringify(input),
57
+ signal: controller.signal
58
+ }));
59
+ if (!response.ok) return false;
60
+ const body = await readResponseJson(response, deadline);
61
+ return !!body && typeof body === "object" && !Array.isArray(body) && body.valid === true;
62
+ } catch {
63
+ return false;
64
+ } finally {
65
+ controller.abort();
66
+ response?.body?.cancel().catch(() => {});
67
+ }
68
+ }
69
+ //#endregion
70
+ export { EMAIL_PROTOCOL_VERSION, encodeEmailActions, readEmailEnvelope, verifyEmailReceiptThroughProxy };
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
- import { C as SendEmailRecipientResult, S as SendEmailOptions, _ as Attachment, a as MessageProxy, b as SendEmailError, c as ParsedEmail, d as defineEmail, f as enactEmailActions, g as Address, h as replyEmail, i as EmailHandlerInfo, l as ReplyEmailOptions, m as parseEmail, n as EmailEnvelope, o as ParseEmailError, p as isForwardableEmailMessage, r as EmailHandler, s as ParseEmailErrorCode, t as EmailAction, u as createMessageProxy, v as NormalizedEmail, w as SendEmailResult, x as SendEmailErrorCode, y as SendEmailDelivery } from "../inbound-BJ70in1n.mjs";
2
+ import { C as SendEmailOptions, S as SendEmailErrorCode, T as SendEmailResult, _ as Attachment, a as MessageProxy, b as SendEmailDelivery, c as ParsedEmail, d as defineEmail, f as enactEmailActions, g as Address, h as replyEmail, i as EmailHandlerInfo, l as ReplyEmailOptions, m as parseEmail, n as EmailEnvelope, o as ParseEmailError, p as isForwardableEmailMessage, r as EmailHandler, s as ParseEmailErrorCode, t as EmailAction, u as createMessageProxy, v as EmailDeliveryState, w as SendEmailRecipientResult, x as SendEmailError, y as NormalizedEmail } from "../inbound-CH5Mksyy.mjs";
3
3
  //#region src/runtime/email/mime.d.ts
4
4
  type MimeErrorCode = "INVALID_FROM" | "INVALID_TO" | "MIME_ERROR";
5
5
  export declare class MimeError extends Error {
@@ -27,4 +27,4 @@ export declare function createEmail(opts: SendEmailOptions): Promise<{
27
27
  messageId: string;
28
28
  }>;
29
29
  //#endregion
30
- export { type Address, type Attachment, type EmailAction, type EmailEnvelope, type EmailHandler, type EmailHandlerInfo, type MessageProxy, type MimeErrorCode, type NormalizedEmail, ParseEmailError, type ParseEmailErrorCode, type ParsedEmail, type ReplyEmailOptions, type SendEmailDelivery, type SendEmailError, type SendEmailErrorCode, type SendEmailOptions, type SendEmailRecipientResult, type SendEmailResult, createMessageProxy, defineEmail, enactEmailActions, isForwardableEmailMessage, parseEmail, replyEmail };
30
+ export { type Address, type Attachment, type EmailAction, type EmailDeliveryState, type EmailEnvelope, type EmailHandler, type EmailHandlerInfo, type MessageProxy, type MimeErrorCode, type NormalizedEmail, ParseEmailError, type ParseEmailErrorCode, type ParsedEmail, type ReplyEmailOptions, type SendEmailDelivery, type SendEmailError, type SendEmailErrorCode, type SendEmailOptions, type SendEmailRecipientResult, type SendEmailResult, createMessageProxy, defineEmail, enactEmailActions, isForwardableEmailMessage, parseEmail, replyEmail };
@@ -1,7 +1,7 @@
1
- import { i as normalizeEmail, n as MimeError, r as buildMime } from "../mime-BJD7d_qL.mjs";
2
- import { a as DEV_INBOX_ENV_VAR, b as serializeCapturedMessage, c as DEV_INBOX_TOKEN_ENV_VAR, d as captureMessage, h as hasActiveSink, i as DEV_INBOX_CAPTURE_URL, p as dispatchToSinks } from "../dev-inbox-DkgRWLkW.mjs";
1
+ import { i as normalizeEmail, n as MimeError, r as buildMime } from "../mime-D5Nmdzf7.mjs";
2
+ import { a as DEV_INBOX_ENV_VAR, b as serializeCapturedMessage, c as DEV_INBOX_TOKEN_ENV_VAR, d as captureMessage, h as hasActiveSink, i as DEV_INBOX_CAPTURE_URL, p as dispatchToSinks } from "../dev-inbox-P0u4tM8Y.mjs";
3
3
  import { a as getRawRuntimeEnv } from "../env-raw-Cx8ElDdj.mjs";
4
- import { a as defineEmail, c as parseEmail, i as createMessageProxy, l as replyEmail, o as enactEmailActions, r as ParseEmailError, s as isForwardableEmailMessage } from "../inbound-CNKxb3FY.mjs";
4
+ import { a as defineEmail, c as parseEmail, i as createMessageProxy, l as replyEmail, o as enactEmailActions, r as ParseEmailError, s as isForwardableEmailMessage } from "../inbound-aVHEUhKo.mjs";
5
5
  //#region src/runtime/email.ts
6
6
  const VALID_SLUG = /^[a-z0-9](?:[a-z0-9-]{0,54}[a-z0-9])?$/;
7
7
  function makeError(code, message, cause) {
@@ -35,6 +35,33 @@ function readEmailFrom() {
35
35
  return typeof raw === "string" && raw.length > 0 ? raw : void 0;
36
36
  }
37
37
  const SEND_EMAIL_BINDING_NAME = "SEND_EMAIL";
38
+ const EMAIL_PROVIDER_DEADLINE_MS = 3e4;
39
+ const PLATFORM_SEND_DEADLINE_MS = 6e4;
40
+ var EmailBindingTimeoutError = class extends Error {
41
+ constructor() {
42
+ super("email binding outcome unknown after timeout");
43
+ this.name = "EmailBindingTimeoutError";
44
+ }
45
+ };
46
+ function createBindingSendDeadline() {
47
+ const deadline = Date.now() + PLATFORM_SEND_DEADLINE_MS;
48
+ return {
49
+ expired: () => Date.now() >= deadline,
50
+ async run(submit) {
51
+ const remaining = Math.min(EMAIL_PROVIDER_DEADLINE_MS, deadline - Date.now());
52
+ if (remaining <= 0) throw new EmailBindingTimeoutError();
53
+ const work = submit();
54
+ let timer;
55
+ try {
56
+ return await Promise.race([work, new Promise((_, reject) => {
57
+ timer = setTimeout(() => reject(new EmailBindingTimeoutError()), remaining);
58
+ })]);
59
+ } finally {
60
+ if (timer !== void 0) clearTimeout(timer);
61
+ }
62
+ }
63
+ };
64
+ }
38
65
  function resolveSendEmailBinding(env) {
39
66
  const binding = env?.[SEND_EMAIL_BINDING_NAME];
40
67
  if (typeof binding !== "object" || binding === null) return;
@@ -47,14 +74,10 @@ function resolveSendEmailBinding(env) {
47
74
  * Cloudflare's Workers API reference); the older Email Routing sender
48
75
  * throws message-only errors, so the destination-verification phrasing the
49
76
  * platform proxy already keys on is kept as a fallback. Anything else is
50
- * `UPSTREAM_ERROR` with the original error as `cause` — the framework has
51
- * no way to act on a delivery failure or a service outage beyond
52
- * reporting it.
77
+ * `OUTCOME_UNKNOWN` with the original error as `cause`: once `send()` has
78
+ * started, an undocumented rejection does not prove the provider declined
79
+ * the message before accepting it.
53
80
  *
54
- * The platform proxy's `classifyUpstream` mirrors this table for its own
55
- * binding, except the two sender codes: there `from` is pinned to the
56
- * project sender, so a sender failure is the platform's to fix and stays
57
- * `UPSTREAM_ERROR`. Keep the recipient and limit rows in lockstep.
58
81
  */
59
82
  function classifyBindingError(e) {
60
83
  const message = e instanceof Error ? e.message : String(e);
@@ -66,7 +89,7 @@ function classifyBindingError(e) {
66
89
  case "E_DAILY_LIMIT_EXCEEDED": return makeError("QUOTA_EXCEEDED", message, e);
67
90
  }
68
91
  if (UNVERIFIED_DESTINATION_RE.test(message)) return makeError("UNVERIFIED_DESTINATION", message, e);
69
- return makeError("UPSTREAM_ERROR", message, e);
92
+ return makeError("OUTCOME_UNKNOWN", message, e);
70
93
  }
71
94
  const UNVERIFIED_DESTINATION_RE = /^[^\n]{0,200}destination address[^\n]{0,200}not (?:yet )?verified/i;
72
95
  function bytesToReadableStream(bytes) {
@@ -77,10 +100,10 @@ function bytesToReadableStream(bytes) {
77
100
  }
78
101
  /**
79
102
  * Deliver through the worker's own `SEND_EMAIL` binding. One `EmailMessage`
80
- * per recipient, every recipient attempted, and the same `deliveries` shape
81
- * the proxy returns so a caller retries the failed ones the same way in both
82
- * modes. The stream is rebuilt per send because a `ReadableStream` is
83
- * one-shot.
103
+ * per recipient, within one bounded batch, and the same `deliveries` shape
104
+ * the proxy returns so a caller can inspect each attempted or cancelled
105
+ * outcome in either mode. The stream is rebuilt per send because a
106
+ * `ReadableStream` is one-shot.
84
107
  */
85
108
  async function sendViaBinding(binding, from, recipients, raw, fallbackId) {
86
109
  let EmailMessage;
@@ -90,20 +113,35 @@ async function sendViaBinding(binding, from, recipients, raw, fallbackId) {
90
113
  return err("UPSTREAM_ERROR", `email: SEND_EMAIL is bound but cloudflare:email could not be loaded: ${e instanceof Error ? e.message : String(e)}`, e);
91
114
  }
92
115
  const deliveries = [];
93
- for (const recipient of recipients) try {
94
- const result = await binding.send(new EmailMessage(from, recipient, bytesToReadableStream(raw)));
95
- const mid = typeof result?.messageId === "string" ? result.messageId : "";
96
- deliveries.push({
97
- recipient,
98
- ok: true,
99
- messageId: mid || fallbackId
100
- });
101
- } catch (e) {
102
- deliveries.push({
103
- recipient,
104
- ok: false,
105
- error: classifyBindingError(e)
106
- });
116
+ const deadline = createBindingSendDeadline();
117
+ for (let index = 0; index < recipients.length; index++) {
118
+ const recipient = recipients[index];
119
+ if (deadline.expired()) {
120
+ for (const unstarted of recipients.slice(index)) deliveries.push({
121
+ recipient: unstarted,
122
+ ok: false,
123
+ state: "cancelled",
124
+ error: makeError("UPSTREAM_ERROR", "email: batch deadline elapsed before provider submission")
125
+ });
126
+ break;
127
+ }
128
+ try {
129
+ const result = await deadline.run(() => binding.send(new EmailMessage(from, recipient, bytesToReadableStream(raw))));
130
+ const mid = typeof result?.messageId === "string" ? result.messageId : "";
131
+ deliveries.push({
132
+ recipient,
133
+ ok: true,
134
+ messageId: mid || fallbackId
135
+ });
136
+ } catch (e) {
137
+ const error = e instanceof EmailBindingTimeoutError ? makeError("OUTCOME_UNKNOWN", "email: binding submission timed out; inspect provider logs before retrying", e) : classifyBindingError(e);
138
+ deliveries.push({
139
+ recipient,
140
+ ok: false,
141
+ ...error.code === "OUTCOME_UNKNOWN" ? { state: "outcome_unknown" } : {},
142
+ error
143
+ });
144
+ }
107
145
  }
108
146
  if (deliveries.some((d) => !d.ok)) return {
109
147
  ok: false,
@@ -191,6 +229,8 @@ function coerceErrorCode(code) {
191
229
  "UNVERIFIED_DESTINATION",
192
230
  "MIME_ERROR",
193
231
  "QUOTA_EXCEEDED",
232
+ "IDEMPOTENCY_CONFLICT",
233
+ "OUTCOME_UNKNOWN",
194
234
  "UPSTREAM_ERROR"
195
235
  ].includes(code) ? code : "UPSTREAM_ERROR";
196
236
  }
@@ -216,29 +256,10 @@ function isValidProxyResponse(b) {
216
256
  return false;
217
257
  }
218
258
  /**
219
- * Validate a per-recipient list from the proxy against the recipients this
220
- * send asked for. Both list-bearing responses go through here — `ids` on
221
- * `ok: true` and `deliveries` on a mid-batch `ok: false` — so the two cannot
222
- * drift: the proxy builds both from the same per-recipient list, one entry
223
- * per requested address, and a caller following the documented retry
224
- * pattern (retry only `ok: false` entries) can act on an outcome only if
225
- * every recipient has one.
226
- *
227
- * The rules shared by both shapes: the count equals the recipients sent;
228
- * every entry is an object naming a requested recipient exactly once. The
229
- * list is compared as a set against the unique, lowercased recipients the
230
- * request carried and returned in the order the proxy gave it — order is
231
- * not part of the contract (the proxy lists allowlist rejections before the
232
- * sends it attempted). `readEntry` decides the rest of the entry's shape and
233
- * returns `undefined` when it does not hold; `shape` names that shape in the
234
- * reason.
235
- *
236
- * Anything else is a schema-skewed or corrupt proxy and is refused as a
237
- * whole rather than trimmed to the entries that parse: a dropped entry is a
238
- * recipient whose outcome the caller can neither trust nor retry. Entries
239
- * are named by index only — the body is attached as `cause`, and a recipient
240
- * string from a corrupt response is not a value to interpolate into an
241
- * error message.
259
+ * Require exactly one valid outcome per requested recipient in either list
260
+ * shape. A missing, duplicate, or foreign entry makes the whole result unknown;
261
+ * trimming it could hide a submission. Order is not part of the contract.
262
+ * Diagnostics identify entries by index rather than echoing corrupt addresses.
242
263
  */
243
264
  function readRecipientList(raw, recipients, shape, readEntry) {
244
265
  if (raw.length !== recipients.length) return {
@@ -277,81 +298,149 @@ function readRecipientList(raw, recipients, shape, readEntry) {
277
298
  };
278
299
  }
279
300
  /**
280
- * The `ids` list of an `ok: true` response. The proxy answers `ok: true`
281
- * only once every requested recipient passed its allowlist and
282
- * `SEND_EMAIL.send()` confirmed each one, so every entry is
283
- * `{ recipient, messageId }` with `messageId` always a string (`''` when
284
- * Cloudflare returned none; that is the proxy's explicit convention, and the
285
- * framework's own Message-ID stands in for it). An `ok: true` with a
286
- * delivery missing would tell the caller a send was confirmed when it never
287
- * was, and a success carries no per-recipient failure to retry from.
301
+ * The `ids` list of an `ok: true` response. The proxy answers `ok: true` only
302
+ * after every requested recipient has an accepted provider outcome. Each entry
303
+ * is `{ recipient, messageId }`; the framework's own Message-ID stands in for
304
+ * an empty provider reference. A missing delivery would leave no safe retry
305
+ * decision, so the whole response is rejected as malformed.
288
306
  */
289
307
  function readDeliveryList(raw, recipients, fallbackId) {
290
308
  return readRecipientList(raw, recipients, "{ recipient: string, messageId: string }", (entry, recipient) => typeof entry.messageId === "string" ? {
291
309
  recipient,
292
- messageId: entry.messageId || fallbackId
310
+ messageId: entry.messageId || fallbackId,
311
+ ...entry.state === "provider_accepted" ? { state: "provider_accepted" } : {}
293
312
  } : void 0);
294
313
  }
295
314
  /**
296
- * The `deliveries` list of a mid-batch `ok: false` response. Each entry is
297
- * either a confirmed send (`ok: true`, `messageId` a string with the same
298
- * `''` convention as above) or a failure (`ok: false`, `error` with a string
299
- * `message`; the code is coerced, unknown values become `UPSTREAM_ERROR`).
300
- * Both are the proxy's own outcome for that recipient; `ok` is what the
301
- * caller retries on, so an entry that does not state it is not an outcome
302
- * at all — treating it as failed would retry a recipient that may have been
303
- * delivered, and treating it as delivered would silently lose one.
315
+ * Preserve each recipient's acceptance or error and recorded state. `ok: false`
316
+ * alone does not establish that retrying is safe: submission may be unresolved.
304
317
  */
305
318
  function readFailureDeliveries(raw, recipients, fallbackId) {
306
319
  return readRecipientList(raw, recipients, "{ recipient: string, ok: true, messageId: string } | { recipient: string, ok: false, error: { message: string } }", (entry, recipient) => {
307
320
  if (entry.ok === true) return typeof entry.messageId === "string" ? {
308
321
  recipient,
309
322
  ok: true,
310
- messageId: entry.messageId || fallbackId
323
+ messageId: entry.messageId || fallbackId,
324
+ ...entry.state === "provider_accepted" ? { state: "provider_accepted" } : {}
311
325
  } : void 0;
312
326
  if (entry.ok === false) {
313
327
  const error = entry.error;
314
328
  return typeof error === "object" && error !== null && typeof error.message === "string" ? {
315
329
  recipient,
316
330
  ok: false,
317
- error: makeError(coerceErrorCode(error.code), error.message)
331
+ error: makeError(coerceErrorCode(error.code), error.message),
332
+ ...readDeliveryState(entry.state) ? { state: readDeliveryState(entry.state) } : {}
318
333
  } : void 0;
319
334
  }
320
335
  });
321
336
  }
322
- async function readResponseAsResult(res, recipients, fallbackId) {
337
+ function readDeliveryState(value) {
338
+ return [
339
+ "rejected",
340
+ "reserved",
341
+ "attempt_started",
342
+ "provider_accepted",
343
+ "failed",
344
+ "outcome_unknown",
345
+ "cancelled"
346
+ ].find((state) => state === value);
347
+ }
348
+ const MAX_PROXY_RESPONSE_BYTES = 65536;
349
+ function createPlatformSendDeadline() {
350
+ const controller = new AbortController();
351
+ const deadline = Date.now() + PLATFORM_SEND_DEADLINE_MS;
352
+ return {
353
+ signal: controller.signal,
354
+ async beforeDeadline(run) {
355
+ const remaining = deadline - Date.now();
356
+ if (remaining <= 0) {
357
+ controller.abort();
358
+ throw new Error("email platform send timed out");
359
+ }
360
+ const promise = run();
361
+ let timer;
362
+ try {
363
+ return await Promise.race([promise, new Promise((_, reject) => {
364
+ timer = setTimeout(() => {
365
+ controller.abort();
366
+ reject(/* @__PURE__ */ new Error("email platform send timed out"));
367
+ }, remaining);
368
+ })]);
369
+ } finally {
370
+ if (timer !== void 0) clearTimeout(timer);
371
+ }
372
+ }
373
+ };
374
+ }
375
+ async function readBoundedProxyJson(response, beforeDeadline) {
376
+ const declared = response.headers.get("content-length");
377
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > MAX_PROXY_RESPONSE_BYTES)) {
378
+ response.body?.cancel().catch(() => {});
379
+ throw new Error("email proxy response too large");
380
+ }
381
+ const reader = response.body?.getReader();
382
+ if (!reader) throw new Error("email proxy response is empty");
383
+ const bytes = new Uint8Array(MAX_PROXY_RESPONSE_BYTES);
384
+ let size = 0;
385
+ try {
386
+ for (;;) {
387
+ const chunk = await beforeDeadline(() => reader.read());
388
+ if (chunk.done) break;
389
+ if (size + chunk.value.byteLength > bytes.byteLength) throw new Error("email proxy response too large");
390
+ bytes.set(chunk.value, size);
391
+ size += chunk.value.byteLength;
392
+ }
393
+ if (declared !== null && size !== Number(declared)) throw new Error("email proxy response length mismatch");
394
+ return JSON.parse(new TextDecoder("utf-8", {
395
+ fatal: true,
396
+ ignoreBOM: false
397
+ }).decode(bytes.subarray(0, size)));
398
+ } finally {
399
+ reader.cancel().catch(() => {});
400
+ try {
401
+ reader.releaseLock();
402
+ } catch {}
403
+ }
404
+ }
405
+ async function readResponseAsResult(res, recipients, fallbackId, beforeDeadline) {
323
406
  try {
324
407
  let body;
325
408
  try {
326
- body = await res.json();
409
+ body = await readBoundedProxyJson(res, beforeDeadline);
327
410
  } catch {
328
- return err("UPSTREAM_ERROR", `email: proxy returned ${res.status} with non-JSON body`);
411
+ return err("OUTCOME_UNKNOWN", `email: could not establish the send outcome (status ${res.status}); check email logs or repeat the same idempotency key`);
329
412
  }
330
413
  if (!isValidProxyResponse(body)) {
331
414
  const refusal = readProxyRefusal(body);
332
415
  if (!res.ok && refusal !== null) return err("UPSTREAM_ERROR", `email: proxy returned ${res.status}: ${refusal}`, body);
333
- return err("UPSTREAM_ERROR", `email: proxy returned malformed response (status ${res.status})`, body);
416
+ return err("OUTCOME_UNKNOWN", `email: proxy returned malformed response (status ${res.status})`, body);
334
417
  }
335
- const malformedList = (reason) => err("UPSTREAM_ERROR", `email: proxy returned a malformed delivery list (${reason})`, body);
418
+ const malformedList = (reason) => err("OUTCOME_UNKNOWN", `email: proxy returned a malformed delivery list (${reason}); do not retry with a new idempotency key`, body);
419
+ const operation = typeof body.operationId === "string" && body.operationId.length <= 128 ? { operationId: body.operationId } : {};
336
420
  if (body.ok === true) {
337
421
  const list = readDeliveryList(body.ids, recipients, fallbackId);
338
422
  return list.ok ? {
339
423
  ok: true,
340
- ids: list.entries
424
+ ids: list.entries,
425
+ ...operation
341
426
  } : malformedList(list.reason);
342
427
  }
343
428
  const failure = body;
344
- if ("error" in failure && failure.error) {
345
- const e = failure.error;
346
- return err(coerceErrorCode(e.code), typeof e.message === "string" ? e.message : "email: upstream send failed");
429
+ if (Array.isArray(failure.deliveries)) {
430
+ const list = readFailureDeliveries(failure.deliveries, recipients, fallbackId);
431
+ return list.ok ? {
432
+ ok: false,
433
+ deliveries: list.entries,
434
+ ...operation
435
+ } : malformedList(list.reason);
347
436
  }
348
- const list = readFailureDeliveries(failure.deliveries, recipients, fallbackId);
349
- return list.ok ? {
350
- ok: false,
351
- deliveries: list.entries
352
- } : malformedList(list.reason);
437
+ const e = failure.error;
438
+ return {
439
+ ...err(coerceErrorCode(e.code), typeof e.message === "string" ? e.message : "email: upstream send failed"),
440
+ ...operation
441
+ };
353
442
  } catch (e) {
354
- return err("UPSTREAM_ERROR", `email: failed to parse proxy response (status ${res.status}): ${e instanceof Error ? e.message : String(e)}`, e);
443
+ return err("OUTCOME_UNKNOWN", `email: failed to parse proxy response (status ${res.status}): ${e instanceof Error ? e.message : String(e)}`, e);
355
444
  }
356
445
  }
357
446
  /**
@@ -488,6 +577,7 @@ function capturedResult(normalized, captureId) {
488
577
  * override for a single call.
489
578
  */
490
579
  async function sendEmail(opts) {
580
+ if (opts.idempotencyKey !== void 0 && (typeof opts.idempotencyKey !== "string" || !/^[\x21-\x7e]{1,128}$/.test(opts.idempotencyKey))) return err("MIME_ERROR", "email: idempotencyKey must contain 1–128 printable non-space ASCII characters.");
491
581
  const resolvedOpts = resolveDefaultFrom(opts);
492
582
  if (resolvedOpts.from === void 0) return err("INVALID_FROM", "email: 'from' is required. Set it explicitly or deploy via Void so the platform-injected __VOID_PROJECT_SLUG and __VOID_EMAIL_DOMAIN can default it to <slug>+noreply@<domain>.");
493
583
  const optsWithFrom = resolvedOpts;
@@ -526,36 +616,39 @@ async function sendEmail(opts) {
526
616
  const recipients = uniqueRecipients(normalized);
527
617
  if (recipients.length === 0) return err("INVALID_TO", "email: 'to' must include at least one recipient.");
528
618
  const sendBinding = resolveSendEmailBinding(rawEnv);
529
- if (sendBinding) return sendViaBinding(sendBinding, normalized.from.email, recipients, raw, messageId);
619
+ if (sendBinding) {
620
+ if (opts.idempotencyKey !== void 0) return err("MIME_ERROR", "email: idempotencyKey requires a Void platform; the native Cloudflare binding cannot deduplicate sends.");
621
+ return sendViaBinding(sendBinding, normalized.from.email, recipients, raw, messageId);
622
+ }
530
623
  const env = rawEnv;
531
624
  const proxy = env?.__VOID_PROXY;
532
625
  const projectId = env?.__PROJECT_ID;
533
626
  const proxyToken = env?.__VOID_PROXY_TOKEN;
534
627
  if (!proxy || !projectId || !proxyToken) return err("BINDING_MISSING", "email: neither SEND_EMAIL nor __VOID_PROXY is available, so nothing was sent. Deploy via Void to bind the platform proxy, or set `email.from` in void.json so `void deploy --platform cloudflare` provisions the SEND_EMAIL binding. Under `void dev` messages are captured by the local inbox at /__void/inbox instead — reaching this error there means that path was bypassed (`sendInDev: true`, which does not deliver: `void dev` binds no send transport at all, so there is nothing to fall through to) or unavailable (the worker has no assets binding to reach the dev server through).");
535
628
  const requestId = newRequestId();
536
- const fromDomain = normalized.from.email.split("@")[1] ?? "";
537
629
  const body = {
538
630
  projectId,
539
631
  aiToken: proxyToken,
540
- proxyToken,
541
632
  from: normalized.from.email,
542
633
  recipients,
543
634
  mimeBase64: bytesToBase64(raw),
544
- byteSize: raw.byteLength,
545
635
  requestId,
546
- fromDomain
636
+ ...opts.idempotencyKey === void 0 ? {} : { idempotencyKey: opts.idempotencyKey }
547
637
  };
638
+ const requestBody = JSON.stringify(body);
639
+ const deadline = createPlatformSendDeadline();
548
640
  let res;
549
641
  try {
550
- res = await proxy.fetch("https://email-proxy/email/send", {
642
+ res = await deadline.beforeDeadline(() => proxy.fetch("https://email-proxy/email/send", {
551
643
  method: "POST",
552
644
  headers: { "content-type": "application/json" },
553
- body: JSON.stringify(body)
554
- });
645
+ body: requestBody,
646
+ signal: deadline.signal
647
+ }));
555
648
  } catch (e) {
556
- return err("UPSTREAM_ERROR", `email: proxy unreachable: ${e instanceof Error ? e.message : String(e)}`, e);
649
+ return err("OUTCOME_UNKNOWN", `email: send outcome is unknown: ${e instanceof Error ? e.message : String(e)}. Check email logs or repeat the same idempotency key; do not create a new send.`, e);
557
650
  }
558
- return readResponseAsResult(res, recipients, messageId);
651
+ return readResponseAsResult(res, recipients, messageId, deadline.beforeDeadline);
559
652
  }
560
653
  /**
561
654
  * Build a normalized email + raw MIME for advanced callers who want to feed
@@ -1,4 +1,4 @@
1
- import { t as cfAccessHeaders } from "../../cf-access-AJ1ehiFR.mjs";
1
+ import { t as cfAccessHeaders } from "../../cf-access-DRsQRe6k.mjs";
2
2
  import { n as serializeR2Conditional } from "../../r2-conditions-D1Wk8i7b.mjs";
3
3
  //#region src/runtime/remote/client.ts
4
4
  /**
@@ -34,7 +34,8 @@ var ProxyClient = class {
34
34
  body: JSON.stringify({
35
35
  ...body,
36
36
  projectId: this.projectId
37
- })
37
+ }),
38
+ redirect: "manual"
38
39
  });
39
40
  if (!res.ok) {
40
41
  const text = await res.text().catch(() => res.statusText);
@@ -62,7 +63,8 @@ var ProxyClient = class {
62
63
  ...init.headers,
63
64
  ...this.authHeaders
64
65
  },
65
- body
66
+ body,
67
+ redirect: "manual"
66
68
  });
67
69
  if (!res.ok) {
68
70
  const text = await res.text().catch(() => res.statusText);
@@ -1,7 +1,7 @@
1
- import { A as password, C as cancel, D as log, E as intro, F as isCancel, O as note, k as outro, r as cliTitle, w as confirm } from "./output-tFQLLj26.mjs";
2
- import { b as readDeploymentPlatform, c as getToken, x as readProjectConfig } from "./auth-W9WII-mN.mjs";
3
- import { m as validateSecretValues } from "./env-validation-ENpMy6Ez.mjs";
4
- import { n as PlatformClient } from "./client-CyCHWSO_.mjs";
1
+ import { A as outro, C as cancel, D as log, E as intro, I as isCancel, j as password, k as note, r as cliTitle, w as confirm } from "./output-B0cfNSx5.mjs";
2
+ import { S as readProjectConfig, l as getToken, x as readDeploymentPlatform } from "./auth-DPl6kck4.mjs";
3
+ import { m as validateSecretValues } from "./env-validation-CF6KvTRf.mjs";
4
+ import { n as PlatformClient } from "./client-dHfSJvAN.mjs";
5
5
  import { r as resolveProjectBySlug, t as getRequestedProjectSlug } from "./resolve-project--Vxawf7z.mjs";
6
6
  import { readFileSync } from "node:fs";
7
7
  //#region src/cli/secret.ts
@@ -76,7 +76,7 @@ async function runSecretCommand(root, args) {
76
76
  }
77
77
  async function runCloudflareSecretCommand(root, args) {
78
78
  if (args.projectName) throw new Error("secret: --project selects a Void platform project and is not used for direct Cloudflare apps.");
79
- const { deleteCloudflareSecret, listCloudflareSecrets, putCloudflareSecret, resolveCloudflareOperationContext, syncCloudflareSecrets } = await import("./cloudflare-operations-CPTpRW6d.mjs").then((n) => n.t);
79
+ const { deleteCloudflareSecret, listCloudflareSecrets, putCloudflareSecret, resolveCloudflareOperationContext, syncCloudflareSecrets } = await import("./cloudflare-operations-BzWnlC1_.mjs").then((n) => n.t);
80
80
  const context = resolveCloudflareOperationContext(root);
81
81
  if (args.action === "list") {
82
82
  const secrets = await listCloudflareSecrets(context);