void 0.20.1 → 0.20.3
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-CzwquNiP.mjs} +5 -4
- package/dist/auth-link-ElDTgF7j.mjs +28 -0
- package/dist/{build-cmd-CJvZvPQO.mjs → build-cmd-BpJe6boP.mjs} +3 -3
- package/dist/{cache-BlNeQjuP.mjs → cache-D98YTqeE.mjs} +3 -3
- package/dist/{cancel-deploy-CmlAZ9P6.mjs → cancel-deploy-CPbEQLMG.mjs} +3 -3
- package/dist/cf-access-DRsQRe6k.mjs +75 -0
- package/dist/cli/cli.mjs +309 -1958
- package/dist/cli/env-schema-probe.mjs +11 -2
- package/dist/client-BQBrZoCX.mjs +989 -0
- 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-WCQZ_u3m.mjs} +6 -6
- package/dist/{create-project-ChGZ1DFd.mjs → create-project-D0oXA090.mjs} +7 -7
- package/dist/{db-D2d_mUsB.mjs → db-DJ-9qs3S.mjs} +47 -30
- package/dist/{delete-D8GigDk8.mjs → delete-BZ4-WaGm.mjs} +3 -3
- package/dist/{deploy-iXZ3F0N6.mjs → deploy-BAhhcg5q.mjs} +135 -117
- package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
- package/dist/{domain-B1VmoSr0.mjs → domain-BxAyhxXN.mjs} +4 -4
- package/dist/email-Bj7Cvdwp.mjs +795 -0
- package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
- package/dist/{env-BcQzYgoG.mjs → env-DP_EErve.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-C-z_xRrQ.mjs} +15 -21
- 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-BGktCXgA.mjs} +11 -11
- package/dist/{link-RMdgjF1v.mjs → link-D2kbqhWb.mjs} +4 -4
- package/dist/{list-3F52R_yO.mjs → list-CvkK_G7k.mjs} +4 -4
- package/dist/{login-pV69H-ZO.mjs → login-WIjNc77c.mjs} +28 -10
- package/dist/{logs-DFHHD6wE.mjs → logs-dLUFCapG.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-CKJ7xIRs.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-CdVWRRJr.mjs +368 -0
- package/dist/platform-auth-protection-Drl0qhrn.mjs +219 -0
- package/dist/platform-auth-recovery-CmKEWpDo.mjs +310 -0
- package/dist/{platform-cmd-DxJ2FRwR.mjs → platform-cmd-5q_k56xS.mjs} +16 -6
- package/dist/{platform-domain-ChvbJkdy.mjs → platform-domain-4GiDlcqx.mjs} +4 -4
- package/dist/{platform-lifecycle-DN4MzJF_.mjs → platform-lifecycle-R9xAxvtG.mjs} +1716 -204
- package/dist/{platform-management-Db2PXw0B.mjs → platform-management-BfWsXHEW.mjs} +35 -7
- package/dist/{platform-recovery-C_YO-tIs.mjs → platform-recovery-CbK-I1FB.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-CTdmnzvc.mjs} +32 -14
- package/dist/project-team-CGxsQe3_.mjs +132 -0
- package/dist/project-token-Cirx7uwZ.mjs +75 -0
- package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
- package/dist/{requests-BcKOVpRg.mjs → requests-4Nq59hOr.mjs} +3 -3
- package/dist/{rollback-Bx85-0xh.mjs → rollback-Dr7u0Ljx.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 +54 -11
- package/dist/runtime/sandbox.d.mts +4 -56
- package/dist/runtime/sandbox.mjs +81 -220
- package/dist/{secret-ByhJ9AMl.mjs → secret-AcPi-FoA.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 +16 -14
- package/skills/void/docs/guide/email.md +114 -120
- package/skills/void/docs/guide/platform/administration/access.md +163 -0
- package/skills/void/docs/guide/platform/administration/email.md +121 -0
- package/skills/void/docs/guide/platform/administration/operations.md +97 -0
- package/skills/void/docs/guide/platform/administration/projects.md +54 -0
- package/skills/void/docs/guide/platform/development/local.md +119 -0
- package/skills/void/docs/guide/platform/development/runtime.md +124 -0
- package/skills/void/docs/guide/platform/development/schema-ci.md +95 -0
- package/skills/void/docs/guide/platform/installation/ci.md +55 -0
- package/skills/void/docs/guide/platform/installation/credentials.md +80 -0
- package/skills/void/docs/guide/platform/installation/domains.md +68 -0
- package/skills/void/docs/guide/platform/installation/first-deployment.md +82 -0
- package/skills/void/docs/guide/platform/installation/maintenance.md +137 -0
- package/skills/void/docs/guide/platform/installation/prerequisites.md +86 -0
- package/skills/void/docs/guide/platform/installation/setup.md +169 -0
- package/skills/void/docs/guide/platform/installation/uninstall.md +54 -0
- package/skills/void/docs/guide/platform-administration.md +6 -202
- package/skills/void/docs/guide/platform-development.md +5 -254
- package/skills/void/docs/guide/project-collaboration.md +94 -0
- package/skills/void/docs/guide/sandboxes.md +9 -24
- package/skills/void/docs/guide/self-hosted-platform.md +13 -542
- package/skills/void/docs/reference/api.md +34 -34
- package/skills/void/docs/reference/cli.md +276 -31
- package/skills/void/docs/reference/config.md +1 -1
- package/skills/void/docs/reference/resource-inference.md +10 -10
- package/dist/cf-access-AJ1ehiFR.mjs +0 -42
- package/dist/cf-access-DsSsZUPr.mjs +0 -67
- package/dist/client-Clirrol3.mjs +0 -705
- package/dist/email-uKyQYUVY.mjs +0 -1016
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as cancel, D as log, E as intro,
|
|
2
|
-
import {
|
|
3
|
-
import { n as PlatformClient } from "./client-
|
|
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-BQBrZoCX.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-
|
|
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);
|
package/dist/runtime/ai.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as cfAccessHeaders } from "../cf-access-
|
|
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-
|
|
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-
|
|
2
|
-
import { y as registerEmailSink } from "../../dev-inbox-
|
|
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-
|
|
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 };
|
package/dist/runtime/email.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import { C as
|
|
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 };
|
package/dist/runtime/email.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as normalizeEmail, n as MimeError, r as buildMime } from "../mime-
|
|
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-
|
|
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-
|
|
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
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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("
|
|
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,
|
|
81
|
-
* the proxy returns so a caller
|
|
82
|
-
*
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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
|
-
*
|
|
282
|
-
* `
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
-
*
|
|
297
|
-
*
|
|
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
|
-
|
|
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
|
|
409
|
+
body = await readBoundedProxyJson(res, beforeDeadline);
|
|
327
410
|
} catch {
|
|
328
|
-
return err("
|
|
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("
|
|
416
|
+
return err("OUTCOME_UNKNOWN", `email: proxy returned malformed response (status ${res.status})`, body);
|
|
334
417
|
}
|
|
335
|
-
const malformedList = (reason) => err("
|
|
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 (
|
|
345
|
-
const
|
|
346
|
-
return
|
|
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
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
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("
|
|
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)
|
|
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
|
-
|
|
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:
|
|
554
|
-
|
|
645
|
+
body: requestBody,
|
|
646
|
+
signal: deadline.signal
|
|
647
|
+
}));
|
|
555
648
|
} catch (e) {
|
|
556
|
-
return err("
|
|
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
|