@void/platform 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.
Files changed (32) hide show
  1. package/README.md +10 -1
  2. package/dist/email-protocol.d.mts +48 -0
  3. package/dist/email-protocol.mjs +278 -0
  4. package/dist/index.d.mts +154 -15
  5. package/dist/index.mjs +349 -22
  6. package/dist/installer.d.mts +5 -3
  7. package/dist/installer.mjs +14 -5
  8. package/dist/runtime/api/README.md +1 -1
  9. package/dist/runtime/api/index.js +79311 -65332
  10. package/dist/runtime/api/index.js.map +4 -4
  11. package/dist/runtime/dispatch/README.md +1 -1
  12. package/dist/runtime/dispatch/index.js +35 -13
  13. package/dist/runtime/dispatch/index.js.map +2 -2
  14. package/dist/runtime/drizzle/0041_military_beyonder.sql +119 -0
  15. package/dist/runtime/drizzle/0042_dark_the_anarchist.sql +1 -0
  16. package/dist/runtime/drizzle/0043_nostalgic_meggan.sql +21 -0
  17. package/dist/runtime/drizzle/0044_smart_mentor.sql +15 -0
  18. package/dist/runtime/drizzle/0045_platform_settings.sql +5 -0
  19. package/dist/runtime/drizzle/0046_email_domains_platform_credential.sql +8 -0
  20. package/dist/runtime/drizzle/0047_email_gateway.sql +226 -0
  21. package/dist/runtime/drizzle/0048_project_teams.sql +133 -0
  22. package/dist/runtime/drizzle/compatibility.json +4 -0
  23. package/dist/runtime/drizzle/meta/_journal.json +56 -0
  24. package/dist/runtime/emailGateway/README.md +1 -0
  25. package/dist/runtime/emailGateway/index.js +5893 -0
  26. package/dist/runtime/emailGateway/index.js.map +8 -0
  27. package/dist/runtime/manifest.json +71 -12
  28. package/dist/runtime/proxy/README.md +1 -1
  29. package/dist/runtime/proxy/index.js +1646 -2240
  30. package/dist/runtime/proxy/index.js.map +4 -4
  31. package/dist/runtime/tail/README.md +1 -1
  32. package/package.json +5 -1
@@ -1,365 +1,310 @@
1
- var __create = Object.create;
2
1
  var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
- };
11
- var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
- };
14
- var __copyProps = (to, from, except, desc) => {
15
- if (from && typeof from === "object" || typeof from === "function") {
16
- for (let key of __getOwnPropNames(from))
17
- if (!__hasOwnProp.call(to, key) && key !== except)
18
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
- }
20
- return to;
21
- };
22
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
- // If the importer is in node compatibility mode or this is not an ESM
24
- // file that has been converted to a CommonJS file using a Babel-
25
- // compatible transform (i.e. "__esModule" has not been set), then set
26
- // "default" to the CommonJS "module.exports" for node compatibility.
27
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
- mod
29
- ));
30
3
 
31
4
  // ../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/_internal/utils.mjs
32
5
  // @__NO_SIDE_EFFECTS__
33
6
  function createNotImplementedError(name) {
34
7
  return new Error(`[unenv] ${name} is not implemented yet!`);
35
8
  }
36
- var init_utils = __esm({
37
- "../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/_internal/utils.mjs"() {
38
- init_performance2();
39
- __name(createNotImplementedError, "createNotImplementedError");
40
- }
41
- });
9
+ __name(createNotImplementedError, "createNotImplementedError");
42
10
 
43
11
  // ../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/node/internal/perf_hooks/performance.mjs
44
- var _timeOrigin, _performanceNow, nodeTiming, PerformanceEntry, PerformanceMark, PerformanceMeasure, PerformanceResourceTiming, PerformanceObserverEntryList, Performance, PerformanceObserver, performance;
45
- var init_performance = __esm({
46
- "../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/node/internal/perf_hooks/performance.mjs"() {
47
- init_performance2();
48
- init_utils();
49
- _timeOrigin = globalThis.performance?.timeOrigin ?? Date.now();
50
- _performanceNow = globalThis.performance?.now ? globalThis.performance.now.bind(globalThis.performance) : () => Date.now() - _timeOrigin;
51
- nodeTiming = {
52
- name: "node",
53
- entryType: "node",
54
- startTime: 0,
55
- duration: 0,
56
- nodeStart: 0,
57
- v8Start: 0,
58
- bootstrapComplete: 0,
59
- environment: 0,
60
- loopStart: 0,
61
- loopExit: 0,
62
- idleTime: 0,
63
- uvMetricsInfo: {
64
- loopCount: 0,
65
- events: 0,
66
- eventsWaiting: 0
67
- },
68
- detail: void 0,
69
- toJSON() {
70
- return this;
71
- }
72
- };
73
- PerformanceEntry = class {
74
- static {
75
- __name(this, "PerformanceEntry");
76
- }
77
- __unenv__ = true;
78
- detail;
79
- entryType = "event";
80
- name;
81
- startTime;
82
- constructor(name, options) {
83
- this.name = name;
84
- this.startTime = options?.startTime || _performanceNow();
85
- this.detail = options?.detail;
86
- }
87
- get duration() {
88
- return _performanceNow() - this.startTime;
89
- }
90
- toJSON() {
91
- return {
92
- name: this.name,
93
- entryType: this.entryType,
94
- startTime: this.startTime,
95
- duration: this.duration,
96
- detail: this.detail
97
- };
98
- }
99
- };
100
- PerformanceMark = class PerformanceMark2 extends PerformanceEntry {
101
- static {
102
- __name(this, "PerformanceMark");
103
- }
104
- entryType = "mark";
105
- constructor() {
106
- super(...arguments);
107
- }
108
- get duration() {
109
- return 0;
110
- }
111
- };
112
- PerformanceMeasure = class extends PerformanceEntry {
113
- static {
114
- __name(this, "PerformanceMeasure");
115
- }
116
- entryType = "measure";
117
- };
118
- PerformanceResourceTiming = class extends PerformanceEntry {
119
- static {
120
- __name(this, "PerformanceResourceTiming");
121
- }
122
- entryType = "resource";
123
- serverTiming = [];
124
- connectEnd = 0;
125
- connectStart = 0;
126
- decodedBodySize = 0;
127
- domainLookupEnd = 0;
128
- domainLookupStart = 0;
129
- encodedBodySize = 0;
130
- fetchStart = 0;
131
- initiatorType = "";
132
- name = "";
133
- nextHopProtocol = "";
134
- redirectEnd = 0;
135
- redirectStart = 0;
136
- requestStart = 0;
137
- responseEnd = 0;
138
- responseStart = 0;
139
- secureConnectionStart = 0;
140
- startTime = 0;
141
- transferSize = 0;
142
- workerStart = 0;
143
- responseStatus = 0;
144
- };
145
- PerformanceObserverEntryList = class {
146
- static {
147
- __name(this, "PerformanceObserverEntryList");
148
- }
149
- __unenv__ = true;
150
- getEntries() {
151
- return [];
152
- }
153
- getEntriesByName(_name, _type) {
154
- return [];
155
- }
156
- getEntriesByType(type) {
157
- return [];
158
- }
159
- };
160
- Performance = class {
161
- static {
162
- __name(this, "Performance");
163
- }
164
- __unenv__ = true;
165
- timeOrigin = _timeOrigin;
166
- eventCounts = /* @__PURE__ */ new Map();
167
- _entries = [];
168
- _resourceTimingBufferSize = 0;
169
- navigation = void 0;
170
- timing = void 0;
171
- timerify(_fn, _options) {
172
- throw createNotImplementedError("Performance.timerify");
173
- }
174
- get nodeTiming() {
175
- return nodeTiming;
176
- }
177
- eventLoopUtilization() {
178
- return {};
179
- }
180
- markResourceTiming() {
181
- return new PerformanceResourceTiming("");
182
- }
183
- onresourcetimingbufferfull = null;
184
- now() {
185
- if (this.timeOrigin === _timeOrigin) {
186
- return _performanceNow();
187
- }
188
- return Date.now() - this.timeOrigin;
189
- }
190
- clearMarks(markName) {
191
- this._entries = markName ? this._entries.filter((e) => e.name !== markName) : this._entries.filter((e) => e.entryType !== "mark");
192
- }
193
- clearMeasures(measureName) {
194
- this._entries = measureName ? this._entries.filter((e) => e.name !== measureName) : this._entries.filter((e) => e.entryType !== "measure");
195
- }
196
- clearResourceTimings() {
197
- this._entries = this._entries.filter((e) => e.entryType !== "resource" || e.entryType !== "navigation");
198
- }
199
- getEntries() {
200
- return this._entries;
201
- }
202
- getEntriesByName(name, type) {
203
- return this._entries.filter((e) => e.name === name && (!type || e.entryType === type));
204
- }
205
- getEntriesByType(type) {
206
- return this._entries.filter((e) => e.entryType === type);
207
- }
208
- mark(name, options) {
209
- const entry = new PerformanceMark(name, options);
210
- this._entries.push(entry);
211
- return entry;
212
- }
213
- measure(measureName, startOrMeasureOptions, endMark) {
214
- let start;
215
- let end;
216
- if (typeof startOrMeasureOptions === "string") {
217
- start = this.getEntriesByName(startOrMeasureOptions, "mark")[0]?.startTime;
218
- end = this.getEntriesByName(endMark, "mark")[0]?.startTime;
219
- } else {
220
- start = Number.parseFloat(startOrMeasureOptions?.start) || this.now();
221
- end = Number.parseFloat(startOrMeasureOptions?.end) || this.now();
222
- }
223
- const entry = new PerformanceMeasure(measureName, {
224
- startTime: start,
225
- detail: {
226
- start,
227
- end
228
- }
229
- });
230
- this._entries.push(entry);
231
- return entry;
232
- }
233
- setResourceTimingBufferSize(maxSize) {
234
- this._resourceTimingBufferSize = maxSize;
235
- }
236
- addEventListener(type, listener, options) {
237
- throw createNotImplementedError("Performance.addEventListener");
238
- }
239
- removeEventListener(type, listener, options) {
240
- throw createNotImplementedError("Performance.removeEventListener");
241
- }
242
- dispatchEvent(event) {
243
- throw createNotImplementedError("Performance.dispatchEvent");
244
- }
245
- toJSON() {
246
- return this;
247
- }
12
+ var _timeOrigin = globalThis.performance?.timeOrigin ?? Date.now();
13
+ var _performanceNow = globalThis.performance?.now ? globalThis.performance.now.bind(globalThis.performance) : () => Date.now() - _timeOrigin;
14
+ var nodeTiming = {
15
+ name: "node",
16
+ entryType: "node",
17
+ startTime: 0,
18
+ duration: 0,
19
+ nodeStart: 0,
20
+ v8Start: 0,
21
+ bootstrapComplete: 0,
22
+ environment: 0,
23
+ loopStart: 0,
24
+ loopExit: 0,
25
+ idleTime: 0,
26
+ uvMetricsInfo: {
27
+ loopCount: 0,
28
+ events: 0,
29
+ eventsWaiting: 0
30
+ },
31
+ detail: void 0,
32
+ toJSON() {
33
+ return this;
34
+ }
35
+ };
36
+ var PerformanceEntry = class {
37
+ static {
38
+ __name(this, "PerformanceEntry");
39
+ }
40
+ __unenv__ = true;
41
+ detail;
42
+ entryType = "event";
43
+ name;
44
+ startTime;
45
+ constructor(name, options) {
46
+ this.name = name;
47
+ this.startTime = options?.startTime || _performanceNow();
48
+ this.detail = options?.detail;
49
+ }
50
+ get duration() {
51
+ return _performanceNow() - this.startTime;
52
+ }
53
+ toJSON() {
54
+ return {
55
+ name: this.name,
56
+ entryType: this.entryType,
57
+ startTime: this.startTime,
58
+ duration: this.duration,
59
+ detail: this.detail
248
60
  };
249
- PerformanceObserver = class {
250
- static {
251
- __name(this, "PerformanceObserver");
252
- }
253
- __unenv__ = true;
254
- static supportedEntryTypes = [];
255
- _callback = null;
256
- constructor(callback) {
257
- this._callback = callback;
258
- }
259
- takeRecords() {
260
- return [];
261
- }
262
- disconnect() {
263
- throw createNotImplementedError("PerformanceObserver.disconnect");
264
- }
265
- observe(options) {
266
- throw createNotImplementedError("PerformanceObserver.observe");
267
- }
268
- bind(fn) {
269
- return fn;
270
- }
271
- runInAsyncScope(fn, thisArg, ...args) {
272
- return fn.call(thisArg, ...args);
273
- }
274
- asyncId() {
275
- return 0;
276
- }
277
- triggerAsyncId() {
278
- return 0;
279
- }
280
- emitDestroy() {
281
- return this;
61
+ }
62
+ };
63
+ var PerformanceMark = class PerformanceMark2 extends PerformanceEntry {
64
+ static {
65
+ __name(this, "PerformanceMark");
66
+ }
67
+ entryType = "mark";
68
+ constructor() {
69
+ super(...arguments);
70
+ }
71
+ get duration() {
72
+ return 0;
73
+ }
74
+ };
75
+ var PerformanceMeasure = class extends PerformanceEntry {
76
+ static {
77
+ __name(this, "PerformanceMeasure");
78
+ }
79
+ entryType = "measure";
80
+ };
81
+ var PerformanceResourceTiming = class extends PerformanceEntry {
82
+ static {
83
+ __name(this, "PerformanceResourceTiming");
84
+ }
85
+ entryType = "resource";
86
+ serverTiming = [];
87
+ connectEnd = 0;
88
+ connectStart = 0;
89
+ decodedBodySize = 0;
90
+ domainLookupEnd = 0;
91
+ domainLookupStart = 0;
92
+ encodedBodySize = 0;
93
+ fetchStart = 0;
94
+ initiatorType = "";
95
+ name = "";
96
+ nextHopProtocol = "";
97
+ redirectEnd = 0;
98
+ redirectStart = 0;
99
+ requestStart = 0;
100
+ responseEnd = 0;
101
+ responseStart = 0;
102
+ secureConnectionStart = 0;
103
+ startTime = 0;
104
+ transferSize = 0;
105
+ workerStart = 0;
106
+ responseStatus = 0;
107
+ };
108
+ var PerformanceObserverEntryList = class {
109
+ static {
110
+ __name(this, "PerformanceObserverEntryList");
111
+ }
112
+ __unenv__ = true;
113
+ getEntries() {
114
+ return [];
115
+ }
116
+ getEntriesByName(_name, _type) {
117
+ return [];
118
+ }
119
+ getEntriesByType(type) {
120
+ return [];
121
+ }
122
+ };
123
+ var Performance = class {
124
+ static {
125
+ __name(this, "Performance");
126
+ }
127
+ __unenv__ = true;
128
+ timeOrigin = _timeOrigin;
129
+ eventCounts = /* @__PURE__ */ new Map();
130
+ _entries = [];
131
+ _resourceTimingBufferSize = 0;
132
+ navigation = void 0;
133
+ timing = void 0;
134
+ timerify(_fn, _options) {
135
+ throw createNotImplementedError("Performance.timerify");
136
+ }
137
+ get nodeTiming() {
138
+ return nodeTiming;
139
+ }
140
+ eventLoopUtilization() {
141
+ return {};
142
+ }
143
+ markResourceTiming() {
144
+ return new PerformanceResourceTiming("");
145
+ }
146
+ onresourcetimingbufferfull = null;
147
+ now() {
148
+ if (this.timeOrigin === _timeOrigin) {
149
+ return _performanceNow();
150
+ }
151
+ return Date.now() - this.timeOrigin;
152
+ }
153
+ clearMarks(markName) {
154
+ this._entries = markName ? this._entries.filter((e) => e.name !== markName) : this._entries.filter((e) => e.entryType !== "mark");
155
+ }
156
+ clearMeasures(measureName) {
157
+ this._entries = measureName ? this._entries.filter((e) => e.name !== measureName) : this._entries.filter((e) => e.entryType !== "measure");
158
+ }
159
+ clearResourceTimings() {
160
+ this._entries = this._entries.filter((e) => e.entryType !== "resource" || e.entryType !== "navigation");
161
+ }
162
+ getEntries() {
163
+ return this._entries;
164
+ }
165
+ getEntriesByName(name, type) {
166
+ return this._entries.filter((e) => e.name === name && (!type || e.entryType === type));
167
+ }
168
+ getEntriesByType(type) {
169
+ return this._entries.filter((e) => e.entryType === type);
170
+ }
171
+ mark(name, options) {
172
+ const entry = new PerformanceMark(name, options);
173
+ this._entries.push(entry);
174
+ return entry;
175
+ }
176
+ measure(measureName, startOrMeasureOptions, endMark) {
177
+ let start;
178
+ let end;
179
+ if (typeof startOrMeasureOptions === "string") {
180
+ start = this.getEntriesByName(startOrMeasureOptions, "mark")[0]?.startTime;
181
+ end = this.getEntriesByName(endMark, "mark")[0]?.startTime;
182
+ } else {
183
+ start = Number.parseFloat(startOrMeasureOptions?.start) || this.now();
184
+ end = Number.parseFloat(startOrMeasureOptions?.end) || this.now();
185
+ }
186
+ const entry = new PerformanceMeasure(measureName, {
187
+ startTime: start,
188
+ detail: {
189
+ start,
190
+ end
282
191
  }
283
- };
284
- performance = globalThis.performance && "addEventListener" in globalThis.performance ? globalThis.performance : new Performance();
192
+ });
193
+ this._entries.push(entry);
194
+ return entry;
285
195
  }
286
- });
287
-
288
- // ../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/node/perf_hooks.mjs
289
- var init_perf_hooks = __esm({
290
- "../../home/runner/work/void/void/node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/node/perf_hooks.mjs"() {
291
- init_performance2();
292
- init_performance();
196
+ setResourceTimingBufferSize(maxSize) {
197
+ this._resourceTimingBufferSize = maxSize;
293
198
  }
294
- });
199
+ addEventListener(type, listener, options) {
200
+ throw createNotImplementedError("Performance.addEventListener");
201
+ }
202
+ removeEventListener(type, listener, options) {
203
+ throw createNotImplementedError("Performance.removeEventListener");
204
+ }
205
+ dispatchEvent(event) {
206
+ throw createNotImplementedError("Performance.dispatchEvent");
207
+ }
208
+ toJSON() {
209
+ return this;
210
+ }
211
+ };
212
+ var PerformanceObserver = class {
213
+ static {
214
+ __name(this, "PerformanceObserver");
215
+ }
216
+ __unenv__ = true;
217
+ static supportedEntryTypes = [];
218
+ _callback = null;
219
+ constructor(callback) {
220
+ this._callback = callback;
221
+ }
222
+ takeRecords() {
223
+ return [];
224
+ }
225
+ disconnect() {
226
+ throw createNotImplementedError("PerformanceObserver.disconnect");
227
+ }
228
+ observe(options) {
229
+ throw createNotImplementedError("PerformanceObserver.observe");
230
+ }
231
+ bind(fn) {
232
+ return fn;
233
+ }
234
+ runInAsyncScope(fn, thisArg, ...args) {
235
+ return fn.call(thisArg, ...args);
236
+ }
237
+ asyncId() {
238
+ return 0;
239
+ }
240
+ triggerAsyncId() {
241
+ return 0;
242
+ }
243
+ emitDestroy() {
244
+ return this;
245
+ }
246
+ };
247
+ var performance = globalThis.performance && "addEventListener" in globalThis.performance ? globalThis.performance : new Performance();
295
248
 
296
249
  // ../../home/runner/work/void/void/node_modules/.pnpm/@cloudflare+unenv-preset@2.16.1_unenv@2.0.0-rc.24_workerd@1.20260910.1/node_modules/@cloudflare/unenv-preset/dist/runtime/polyfill/performance.mjs
297
- var init_performance2 = __esm({
298
- "../../home/runner/work/void/void/node_modules/.pnpm/@cloudflare+unenv-preset@2.16.1_unenv@2.0.0-rc.24_workerd@1.20260910.1/node_modules/@cloudflare/unenv-preset/dist/runtime/polyfill/performance.mjs"() {
299
- init_perf_hooks();
300
- if (!("__unenv__" in performance)) {
301
- const proto = Performance.prototype;
302
- for (const key of Object.getOwnPropertyNames(proto)) {
303
- if (key !== "constructor" && !(key in performance)) {
304
- const desc = Object.getOwnPropertyDescriptor(proto, key);
305
- if (desc) {
306
- Object.defineProperty(performance, key, desc);
307
- }
308
- }
250
+ if (!("__unenv__" in performance)) {
251
+ const proto = Performance.prototype;
252
+ for (const key of Object.getOwnPropertyNames(proto)) {
253
+ if (key !== "constructor" && !(key in performance)) {
254
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
255
+ if (desc) {
256
+ Object.defineProperty(performance, key, desc);
309
257
  }
310
258
  }
311
- globalThis.performance = performance;
312
- globalThis.Performance = Performance;
313
- globalThis.PerformanceEntry = PerformanceEntry;
314
- globalThis.PerformanceMark = PerformanceMark;
315
- globalThis.PerformanceMeasure = PerformanceMeasure;
316
- globalThis.PerformanceObserver = PerformanceObserver;
317
- globalThis.PerformanceObserverEntryList = PerformanceObserverEntryList;
318
- globalThis.PerformanceResourceTiming = PerformanceResourceTiming;
319
259
  }
320
- });
260
+ }
261
+ globalThis.performance = performance;
262
+ globalThis.Performance = Performance;
263
+ globalThis.PerformanceEntry = PerformanceEntry;
264
+ globalThis.PerformanceMark = PerformanceMark;
265
+ globalThis.PerformanceMeasure = PerformanceMeasure;
266
+ globalThis.PerformanceObserver = PerformanceObserver;
267
+ globalThis.PerformanceObserverEntryList = PerformanceObserverEntryList;
268
+ globalThis.PerformanceResourceTiming = PerformanceResourceTiming;
321
269
 
322
- // ../../home/runner/work/void/void/node_modules/.pnpm/email-validator@2.0.4/node_modules/email-validator/index.js
323
- var require_email_validator = __commonJS({
324
- "../../home/runner/work/void/void/node_modules/.pnpm/email-validator@2.0.4/node_modules/email-validator/index.js"(exports) {
325
- "use strict";
326
- init_performance2();
327
- var tester = /^[-!#$%&'*+\/0-9=?A-Z^_a-z{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
328
- exports.validate = function(email) {
329
- if (!email)
330
- return false;
331
- if (email.length > 254)
332
- return false;
333
- var valid = tester.test(email);
334
- if (!valid)
335
- return false;
336
- var parts = email.split("@");
337
- if (parts[0].length > 64)
338
- return false;
339
- var domainParts = parts[1].split(".");
340
- if (domainParts.some(function(part) {
341
- return part.length > 63;
342
- }))
343
- return false;
344
- return true;
345
- };
270
+ // ../../home/runner/work/void/void/packages/platform/dist/email-protocol.mjs
271
+ var encoder = new TextEncoder();
272
+ var decoder = new TextDecoder("utf-8", { fatal: true });
273
+ function unbase64url(value) {
274
+ if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length > 1024) throw new Error("invalid receipt");
275
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4));
276
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
277
+ }
278
+ __name(unbase64url, "unbase64url");
279
+ async function receiptKey(secret, usage) {
280
+ if (!secret) throw new Error("Email signing key is not configured");
281
+ return crypto.subtle.importKey("raw", encoder.encode(secret), {
282
+ name: "HMAC",
283
+ hash: "SHA-256"
284
+ }, false, [usage]);
285
+ }
286
+ __name(receiptKey, "receiptKey");
287
+ async function verifyEmailReceipt(token, signingSecret, expected, now = Date.now()) {
288
+ try {
289
+ if (token.length > 1536) return null;
290
+ const parts = token.split(".");
291
+ if (parts.length !== 2) return null;
292
+ const payload = unbase64url(parts[0]);
293
+ const signature = unbase64url(parts[1]);
294
+ if (payload.byteLength > 768 || signature.byteLength !== 32) return null;
295
+ if (!await crypto.subtle.verify("HMAC", await receiptKey(signingSecret, "verify"), Uint8Array.from(signature), Uint8Array.from(payload))) return null;
296
+ const claims = JSON.parse(decoder.decode(payload));
297
+ if (typeof claims !== "object" || claims === null) return null;
298
+ const value = claims;
299
+ if (value.version !== 1 || typeof value.receiptId !== "string" || !/^[a-f0-9-]{16,64}$/.test(value.receiptId) || value.projectId !== expected.projectId || value.deploymentId !== expected.deploymentId || value.recipient !== expected.recipient || typeof value.expiresAt !== "number" || !Number.isSafeInteger(value.expiresAt) || value.expiresAt < now || value.expiresAt > now + 6e4) return null;
300
+ return value;
301
+ } catch {
302
+ return null;
346
303
  }
347
- });
348
-
349
- // ../../home/runner/work/void/void/platform/packages/proxy/src/index.ts
350
- init_performance2();
351
-
352
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/index.js
353
- init_performance2();
354
-
355
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/hono.js
356
- init_performance2();
357
-
358
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/hono-base.js
359
- init_performance2();
304
+ }
305
+ __name(verifyEmailReceipt, "verifyEmailReceipt");
360
306
 
361
307
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
362
- init_performance2();
363
308
  var compose = /* @__PURE__ */ __name((middleware, onError, onNotFound) => {
364
309
  return (context, next) => {
365
310
  let index = -1;
@@ -404,28 +349,9 @@ var compose = /* @__PURE__ */ __name((middleware, onError, onNotFound) => {
404
349
  };
405
350
  }, "compose");
406
351
 
407
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/context.js
408
- init_performance2();
409
-
410
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/request.js
411
- init_performance2();
412
-
413
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/http-exception.js
414
- init_performance2();
415
-
416
352
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/request/constants.js
417
- init_performance2();
418
353
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
419
354
 
420
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/body.js
421
- init_performance2();
422
-
423
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/buffer.js
424
- init_performance2();
425
-
426
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/crypto.js
427
- init_performance2();
428
-
429
355
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/buffer.js
430
356
  var bufferToFormData = /* @__PURE__ */ __name((arrayBuffer, contentType) => {
431
357
  const response = new Response(arrayBuffer, {
@@ -538,7 +464,6 @@ var throwNestingLimitExceeded = /* @__PURE__ */ __name(() => {
538
464
  }, "throwNestingLimitExceeded");
539
465
 
540
466
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
541
- init_performance2();
542
467
  var splitPath = /* @__PURE__ */ __name((path) => {
543
468
  const paths = path.split("/");
544
469
  if (paths[0] === "") {
@@ -591,13 +516,13 @@ var getPattern = /* @__PURE__ */ __name((label, next) => {
591
516
  }
592
517
  return null;
593
518
  }, "getPattern");
594
- var tryDecode = /* @__PURE__ */ __name((str, decoder2) => {
519
+ var tryDecode = /* @__PURE__ */ __name((str, decoder5) => {
595
520
  try {
596
- return decoder2(str);
521
+ return decoder5(str);
597
522
  } catch {
598
523
  return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
599
524
  try {
600
- return decoder2(match2);
525
+ return decoder5(match2);
601
526
  } catch {
602
527
  return match2;
603
528
  }
@@ -1029,7 +954,6 @@ var HonoRequest = class {
1029
954
  };
1030
955
 
1031
956
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/html.js
1032
- init_performance2();
1033
957
  var HtmlEscapedCallbackPhase = {
1034
958
  Stringify: 1,
1035
959
  BeforeStream: 2,
@@ -1501,7 +1425,6 @@ var Context = class {
1501
1425
  };
1502
1426
 
1503
1427
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router.js
1504
- init_performance2();
1505
1428
  var METHOD_NAME_ALL = "ALL";
1506
1429
  var METHOD_NAME_ALL_LOWERCASE = "all";
1507
1430
  var METHODS = ["get", "post", "put", "delete", "options", "patch", "query"];
@@ -1513,7 +1436,6 @@ var UnsupportedPathError = class extends Error {
1513
1436
  };
1514
1437
 
1515
1438
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/constants.js
1516
- init_performance2();
1517
1439
  var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
1518
1440
 
1519
1441
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/hono-base.js
@@ -1627,14 +1549,14 @@ var Hono = class _Hono {
1627
1549
  * app.route("/api", app2) // GET /api/user
1628
1550
  * ```
1629
1551
  */
1630
- route(path, app5) {
1552
+ route(path, app4) {
1631
1553
  const subApp = this.basePath(path);
1632
- app5.routes.map((r) => {
1554
+ app4.routes.map((r) => {
1633
1555
  let handler;
1634
- if (app5.errorHandler === errorHandler) {
1556
+ if (app4.errorHandler === errorHandler) {
1635
1557
  handler = r.handler;
1636
1558
  } else {
1637
- handler = /* @__PURE__ */ __name(async (c, next) => (await compose([], app5.errorHandler)(c, () => r.handler(c, next))).res, "handler");
1559
+ handler = /* @__PURE__ */ __name(async (c, next) => (await compose([], app4.errorHandler)(c, () => r.handler(c, next))).res, "handler");
1638
1560
  handler[COMPOSED_HANDLER] = r.handler;
1639
1561
  }
1640
1562
  subApp.#addRoute(r.method, r.path, handler, r.basePath);
@@ -1897,18 +1819,10 @@ var Hono = class _Hono {
1897
1819
  }, "fire");
1898
1820
  };
1899
1821
 
1900
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/index.js
1901
- init_performance2();
1902
-
1903
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
1904
- init_performance2();
1905
-
1906
1822
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/utils.js
1907
- init_performance2();
1908
1823
  var createNullObject = /* @__PURE__ */ __name(() => /* @__PURE__ */ Object.create(null), "createNullObject");
1909
1824
 
1910
1825
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
1911
- init_performance2();
1912
1826
  var emptyParam = [];
1913
1827
  function match(method, path) {
1914
1828
  const matchers = this.buildAllMatchers();
@@ -1931,7 +1845,6 @@ function match(method, path) {
1931
1845
  __name(match, "match");
1932
1846
 
1933
1847
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
1934
- init_performance2();
1935
1848
  var LABEL_REG_EXP_STR = "[^/]+";
1936
1849
  var ONLY_WILDCARD_REG_EXP_STR = ".*";
1937
1850
  var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
@@ -2043,7 +1956,6 @@ var Node = class _Node {
2043
1956
  };
2044
1957
 
2045
1958
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/trie.js
2046
- init_performance2();
2047
1959
  var Trie = class {
2048
1960
  static {
2049
1961
  __name(this, "Trie");
@@ -2236,14 +2148,7 @@ var RegExpRouter = class {
2236
2148
  }
2237
2149
  };
2238
2150
 
2239
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
2240
- init_performance2();
2241
-
2242
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/smart-router/index.js
2243
- init_performance2();
2244
-
2245
2151
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/smart-router/router.js
2246
- init_performance2();
2247
2152
  var SmartRouter = class {
2248
2153
  static {
2249
2154
  __name(this, "SmartRouter");
@@ -2301,14 +2206,7 @@ var SmartRouter = class {
2301
2206
  }
2302
2207
  };
2303
2208
 
2304
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/trie-router/index.js
2305
- init_performance2();
2306
-
2307
- // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/trie-router/router.js
2308
- init_performance2();
2309
-
2310
2209
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/trie-router/node.js
2311
- init_performance2();
2312
2210
  var emptyParams = createNullObject();
2313
2211
  var order = 0;
2314
2212
  var Node2 = class _Node2 {
@@ -2507,7 +2405,6 @@ var Hono2 = class extends Hono {
2507
2405
  };
2508
2406
 
2509
2407
  // ../../home/runner/work/void/void/node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/middleware/cors/index.js
2510
- init_performance2();
2511
2408
  var cors = /* @__PURE__ */ __name((options) => {
2512
2409
  const opts = {
2513
2410
  origin: "*",
@@ -2593,11 +2490,7 @@ var cors = /* @__PURE__ */ __name((options) => {
2593
2490
  }, "cors2");
2594
2491
  }, "cors");
2595
2492
 
2596
- // ../../home/runner/work/void/void/platform/packages/proxy/src/ai.ts
2597
- init_performance2();
2598
-
2599
2493
  // ../../home/runner/work/void/void/platform/packages/proxy/src/body.ts
2600
- init_performance2();
2601
2494
  function isProxyBody(value) {
2602
2495
  return typeof value === "object" && value !== null && !Array.isArray(value);
2603
2496
  }
@@ -2606,10 +2499,6 @@ function asString(value) {
2606
2499
  return typeof value === "string" ? value : null;
2607
2500
  }
2608
2501
  __name(asString, "asString");
2609
- function asNumber(value) {
2610
- return typeof value === "number" && Number.isFinite(value) ? value : null;
2611
- }
2612
- __name(asNumber, "asNumber");
2613
2502
  function asRecord(value) {
2614
2503
  return isProxyBody(value) ? value : {};
2615
2504
  }
@@ -2984,7 +2873,6 @@ ai.post("/toMarkdown", async (context) => {
2984
2873
  });
2985
2874
 
2986
2875
  // ../../home/runner/work/void/void/platform/packages/proxy/src/capabilities.ts
2987
- init_performance2();
2988
2876
  function proxyCapabilityEnabled(value, capability) {
2989
2877
  if (capability === "sandbox") {
2990
2878
  return false;
@@ -3001,11 +2889,7 @@ function proxyCapabilityEnabled(value, capability) {
3001
2889
  }
3002
2890
  __name(proxyCapabilityEnabled, "proxyCapabilityEnabled");
3003
2891
 
3004
- // ../../home/runner/work/void/void/platform/packages/proxy/src/d1.ts
3005
- init_performance2();
3006
-
3007
2892
  // ../../home/runner/work/void/void/platform/packages/proxy/src/dispatch.ts
3008
- init_performance2();
3009
2893
  async function resolveWorker(api, jwtSecret, projectId) {
3010
2894
  const token = await hmacSign(projectId, jwtSecret);
3011
2895
  const res = await api.fetch(`https://api/internal/resolve-worker/${projectId}`, {
@@ -3066,7 +2950,7 @@ async function dispatchToWorker(env, projectId, path, body, options = {}) {
3066
2950
  }
3067
2951
  }
3068
2952
  __name(dispatchToWorker, "dispatchToWorker");
3069
- var encoder = new TextEncoder();
2953
+ var encoder2 = new TextEncoder();
3070
2954
  var cachedSignKey;
3071
2955
  var cachedKeySecret;
3072
2956
  async function getSignKey(secret) {
@@ -3075,7 +2959,7 @@ async function getSignKey(secret) {
3075
2959
  }
3076
2960
  cachedSignKey = await crypto.subtle.importKey(
3077
2961
  "raw",
3078
- encoder.encode(secret),
2962
+ encoder2.encode(secret),
3079
2963
  { name: "HMAC", hash: "SHA-256" },
3080
2964
  false,
3081
2965
  ["sign"]
@@ -3086,14 +2970,13 @@ async function getSignKey(secret) {
3086
2970
  __name(getSignKey, "getSignKey");
3087
2971
  async function hmacSign(data, secret) {
3088
2972
  const key = await getSignKey(secret);
3089
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
2973
+ const sig = await crypto.subtle.sign("HMAC", key, encoder2.encode(data));
3090
2974
  const binary = String.fromCharCode(...new Uint8Array(sig));
3091
2975
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3092
2976
  }
3093
2977
  __name(hmacSign, "hmacSign");
3094
2978
 
3095
2979
  // ../../home/runner/work/void/void/platform/packages/proxy/src/resolve.ts
3096
- init_performance2();
3097
2980
  var cache = /* @__PURE__ */ new Map();
3098
2981
  async function resolveResource(api, jwtSecret, projectId, type, bindingName) {
3099
2982
  const cacheKey = `${projectId}:${type}:${bindingName}`;
@@ -3119,24 +3002,38 @@ async function resolveResource(api, jwtSecret, projectId, type, bindingName) {
3119
3002
  return data.cf_id;
3120
3003
  }
3121
3004
  __name(resolveResource, "resolveResource");
3122
- async function verifyProjectOwner(api, jwtSecret, projectId, userId) {
3005
+ async function verifyProjectDeployAccess(api, jwtSecret, projectId, userId, jwt, accessAssertion, proxyOrigin) {
3123
3006
  try {
3124
- const token = await hmacSign2(projectId, jwtSecret);
3007
+ if (!proxyOrigin || new URL(proxyOrigin).origin !== proxyOrigin) {
3008
+ return false;
3009
+ }
3010
+ const token = await hmacSign2(ownerVerificationMessage(projectId, proxyOrigin), jwtSecret);
3125
3011
  const res = await api.fetch(`https://api/internal/verify-owner/${projectId}`, {
3126
3012
  method: "POST",
3127
3013
  headers: {
3128
3014
  "content-type": "application/json",
3129
3015
  "x-void-internal": token
3130
3016
  },
3131
- body: JSON.stringify({ userId })
3017
+ body: JSON.stringify({
3018
+ userId,
3019
+ ...jwt ? { jwt } : {},
3020
+ ...accessAssertion ? { accessAssertion } : {},
3021
+ proxyOrigin
3022
+ })
3132
3023
  });
3133
3024
  return res.ok;
3134
3025
  } catch {
3135
3026
  return false;
3136
3027
  }
3137
3028
  }
3138
- __name(verifyProjectOwner, "verifyProjectOwner");
3139
- async function verifyPrerenderBuild(api, jwtSecret, projectId, buildId) {
3029
+ __name(verifyProjectDeployAccess, "verifyProjectDeployAccess");
3030
+ function ownerVerificationMessage(projectId, proxyOrigin) {
3031
+ return `void-proxy-owner-v2
3032
+ ${projectId}
3033
+ ${proxyOrigin}`;
3034
+ }
3035
+ __name(ownerVerificationMessage, "ownerVerificationMessage");
3036
+ async function verifyPrerenderAuthority(api, jwtSecret, projectId, userId, provenance) {
3140
3037
  try {
3141
3038
  const token = await hmacSign2(projectId, jwtSecret);
3142
3039
  const res = await api.fetch(`https://api/internal/verify-prerender-build/${projectId}`, {
@@ -3145,15 +3042,19 @@ async function verifyPrerenderBuild(api, jwtSecret, projectId, buildId) {
3145
3042
  "content-type": "application/json",
3146
3043
  "x-void-internal": token
3147
3044
  },
3148
- body: JSON.stringify({ buildId })
3045
+ body: JSON.stringify({ userId, ...provenance })
3149
3046
  });
3047
+ if (res.status === 400 && !provenance.buildId && !provenance.credentialId) {
3048
+ const body = await res.json().catch(() => null);
3049
+ return body?.error === "userId and credential provenance required";
3050
+ }
3150
3051
  return res.ok;
3151
3052
  } catch {
3152
3053
  return false;
3153
3054
  }
3154
3055
  }
3155
- __name(verifyPrerenderBuild, "verifyPrerenderBuild");
3156
- var encoder2 = new TextEncoder();
3056
+ __name(verifyPrerenderAuthority, "verifyPrerenderAuthority");
3057
+ var encoder3 = new TextEncoder();
3157
3058
  var cachedSignKey2;
3158
3059
  var cachedKeySecret2;
3159
3060
  async function getSignKey2(secret) {
@@ -3162,7 +3063,7 @@ async function getSignKey2(secret) {
3162
3063
  }
3163
3064
  cachedSignKey2 = await crypto.subtle.importKey(
3164
3065
  "raw",
3165
- encoder2.encode(secret),
3066
+ encoder3.encode(secret),
3166
3067
  { name: "HMAC", hash: "SHA-256" },
3167
3068
  false,
3168
3069
  ["sign"]
@@ -3173,7 +3074,7 @@ async function getSignKey2(secret) {
3173
3074
  __name(getSignKey2, "getSignKey");
3174
3075
  async function hmacSign2(data, secret) {
3175
3076
  const key = await getSignKey2(secret);
3176
- const sig = await crypto.subtle.sign("HMAC", key, encoder2.encode(data));
3077
+ const sig = await crypto.subtle.sign("HMAC", key, encoder3.encode(data));
3177
3078
  const binary = String.fromCharCode(...new Uint8Array(sig));
3178
3079
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3179
3080
  }
@@ -3252,95 +3153,478 @@ d1.post("/batch", async (context) => {
3252
3153
  return context.json({ error: "atomic D1 batch unavailable for active deployment" }, 503);
3253
3154
  });
3254
3155
 
3255
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-relay.ts
3256
- init_performance2();
3257
-
3258
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-router.ts
3259
- init_performance2();
3260
-
3261
- // ../../home/runner/work/void/void/platform/packages/api/src/reserved-slugs.ts
3262
- init_performance2();
3263
- var EMAIL_RESERVED_SLUGS = /* @__PURE__ */ new Set([
3264
- "abuse",
3265
- "bounce",
3266
- "noreply",
3267
- "postmaster",
3268
- "security"
3269
- ]);
3270
- function isReservedEmailSlug(slug) {
3271
- return EMAIL_RESERVED_SLUGS.has(slug);
3156
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/email-request-body.ts
3157
+ var EMAIL_REQUEST_DEADLINE_MS = 5e3;
3158
+ var decoder2 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
3159
+ async function beforeDeadline(deadline, run) {
3160
+ const remaining = deadline - Date.now();
3161
+ if (remaining <= 0) {
3162
+ throw new Error("email request timeout");
3163
+ }
3164
+ let timer;
3165
+ return Promise.race([
3166
+ run(),
3167
+ new Promise((_, reject) => {
3168
+ timer = setTimeout(() => reject(new Error("email request timeout")), remaining);
3169
+ })
3170
+ ]).finally(() => {
3171
+ if (timer !== void 0) {
3172
+ clearTimeout(timer);
3173
+ }
3174
+ });
3175
+ }
3176
+ __name(beforeDeadline, "beforeDeadline");
3177
+ async function readEmailRequestJson(request, maxBytes = 2 * 1024) {
3178
+ const declaredLength = request.headers.get("content-length");
3179
+ if (request.headers.has("content-encoding") || !request.body || declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxBytes)) {
3180
+ void request.body?.cancel().catch(() => {
3181
+ });
3182
+ throw new Error("invalid email request body");
3183
+ }
3184
+ const expectedLength = declaredLength === null ? null : Number(declaredLength);
3185
+ const reader = request.body.getReader();
3186
+ let bytes = new Uint8Array(Math.min(1024, maxBytes));
3187
+ const deadline = Date.now() + EMAIL_REQUEST_DEADLINE_MS;
3188
+ let size = 0;
3189
+ try {
3190
+ for (; ; ) {
3191
+ const chunk = await beforeDeadline(deadline, () => reader.read());
3192
+ if (chunk.done) {
3193
+ break;
3194
+ }
3195
+ const nextSize = size + chunk.value.byteLength;
3196
+ if (nextSize > maxBytes) {
3197
+ throw new Error("email request body too large");
3198
+ }
3199
+ if (nextSize > bytes.byteLength) {
3200
+ const grown = new Uint8Array(Math.min(maxBytes, Math.max(nextSize, bytes.byteLength * 2)));
3201
+ grown.set(bytes.subarray(0, size));
3202
+ bytes = grown;
3203
+ }
3204
+ bytes.set(chunk.value, size);
3205
+ size = nextSize;
3206
+ }
3207
+ } finally {
3208
+ void reader.cancel().catch(() => {
3209
+ });
3210
+ reader.releaseLock();
3211
+ }
3212
+ if (size === 0 || expectedLength !== null && size !== expectedLength) {
3213
+ throw new Error("invalid email request body length");
3214
+ }
3215
+ return JSON.parse(decoder2.decode(bytes.subarray(0, size)));
3272
3216
  }
3273
- __name(isReservedEmailSlug, "isReservedEmailSlug");
3217
+ __name(readEmailRequestJson, "readEmailRequestJson");
3274
3218
 
3275
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-router.ts
3276
- var import_email_validator = __toESM(require_email_validator(), 1);
3277
- import { EmailMessage as EmailMessage2 } from "cloudflare:email";
3219
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/email.ts
3220
+ import { EmailMessage } from "cloudflare:email";
3278
3221
 
3279
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-lease.ts
3280
- init_performance2();
3281
- var LEASE_CACHE_TTL_MS = 45 * 60 * 1e3;
3282
- var leaseCache = /* @__PURE__ */ new Map();
3283
- function dropEmailDomainLease(domain) {
3284
- leaseCache.delete(domain);
3285
- }
3286
- __name(dropEmailDomainLease, "dropEmailDomainLease");
3287
- function leaseBearer(lease) {
3288
- return lease.kind === "api_token" ? lease.token : lease.accessToken;
3222
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/email-admission.ts
3223
+ var encoder4 = new TextEncoder();
3224
+ var decoder3 = new TextDecoder();
3225
+ var EMAIL_DEFAULT_DEADLINE_MS = 5e3;
3226
+ var EMAIL_ACTION_DEADLINE_MS = 15e3;
3227
+ var EMAIL_ADMISSION_DEADLINE_MS = 15e3;
3228
+ var MAX_ADMISSION_RESPONSE_BYTES = 65536;
3229
+ async function sign(data, secret) {
3230
+ if (!secret) {
3231
+ throw new Error("Email signing key is not configured");
3232
+ }
3233
+ const key = await crypto.subtle.importKey(
3234
+ "raw",
3235
+ encoder4.encode(secret),
3236
+ { name: "HMAC", hash: "SHA-256" },
3237
+ false,
3238
+ ["sign"]
3239
+ );
3240
+ const bytes = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder4.encode(data)));
3241
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3289
3242
  }
3290
- __name(leaseBearer, "leaseBearer");
3291
- async function leaseEmailDomainCredential(env, domain) {
3292
- const cached = leaseCache.get(domain);
3293
- if (cached && cached.expiresAt > Date.now()) {
3294
- return cached.lease;
3295
- }
3296
- let res;
3297
- try {
3298
- const token = await hmacSign3(domain, env.JWT_SECRET);
3299
- res = await env.API.fetch("https://api/internal/email/domain-credential", {
3243
+ __name(sign, "sign");
3244
+ async function call(env, path, body, deadlineMs = EMAIL_DEFAULT_DEADLINE_MS) {
3245
+ const controller = new AbortController();
3246
+ const deadline = Date.now() + deadlineMs;
3247
+ const beforeDeadline2 = /* @__PURE__ */ __name(async (run) => {
3248
+ const remaining = deadline - Date.now();
3249
+ if (remaining <= 0) {
3250
+ controller.abort();
3251
+ throw new Error("EMAIL_ADMISSION_TIMEOUT");
3252
+ }
3253
+ const promise = run();
3254
+ let timer;
3255
+ try {
3256
+ return await Promise.race([
3257
+ promise,
3258
+ new Promise((_, reject) => {
3259
+ timer = setTimeout(() => {
3260
+ controller.abort();
3261
+ reject(new Error("EMAIL_ADMISSION_TIMEOUT"));
3262
+ }, remaining);
3263
+ })
3264
+ ]);
3265
+ } finally {
3266
+ if (timer !== void 0) {
3267
+ clearTimeout(timer);
3268
+ }
3269
+ }
3270
+ }, "beforeDeadline");
3271
+ const raw2 = JSON.stringify(body);
3272
+ const proof = await sign(raw2, env.EMAIL_SIGNING_SECRET);
3273
+ const response = await beforeDeadline2(
3274
+ () => env.API.fetch(`https://api/internal/email/actions${path}`, {
3300
3275
  method: "POST",
3301
3276
  headers: {
3302
3277
  "content-type": "application/json",
3303
- "x-void-internal": token
3278
+ "x-void-internal": proof
3304
3279
  },
3305
- body: JSON.stringify({ domain })
3280
+ body: raw2,
3281
+ signal: controller.signal
3282
+ })
3283
+ );
3284
+ if (!response.ok) {
3285
+ void response.body?.cancel().catch(() => {
3306
3286
  });
3307
- } catch {
3308
- return null;
3287
+ if (response.status === 409) {
3288
+ throw new Error("IDEMPOTENCY_CONFLICT");
3289
+ }
3290
+ throw new Error(`EMAIL_ADMISSION_${response.status}`);
3309
3291
  }
3310
- if (!res.ok) {
3311
- return null;
3292
+ const reader = response.body?.getReader();
3293
+ if (!reader) {
3294
+ throw new Error("EMAIL_ADMISSION_EMPTY_RESPONSE");
3312
3295
  }
3313
- let data;
3296
+ const bytes = new Uint8Array(MAX_ADMISSION_RESPONSE_BYTES);
3297
+ let size = 0;
3314
3298
  try {
3315
- data = await res.json();
3316
- } catch {
3317
- return null;
3318
- }
3319
- const lease = parseLease(data);
3320
- if (!lease) {
3321
- return null;
3299
+ for (; ; ) {
3300
+ const chunk = await beforeDeadline2(() => reader.read());
3301
+ if (chunk.done) {
3302
+ break;
3303
+ }
3304
+ if (size + chunk.value.byteLength > bytes.length) {
3305
+ throw new Error("EMAIL_ADMISSION_RESPONSE_TOO_LARGE");
3306
+ }
3307
+ bytes.set(chunk.value, size);
3308
+ size += chunk.value.byteLength;
3309
+ }
3310
+ return JSON.parse(decoder3.decode(bytes.subarray(0, size)));
3311
+ } finally {
3312
+ void reader.cancel().catch(() => {
3313
+ });
3314
+ try {
3315
+ reader.releaseLock();
3316
+ } catch {
3317
+ }
3322
3318
  }
3323
- leaseCache.set(domain, { lease, expiresAt: Date.now() + LEASE_CACHE_TTL_MS });
3324
- return lease;
3325
3319
  }
3326
- __name(leaseEmailDomainCredential, "leaseEmailDomainCredential");
3327
- function parseLease(data) {
3328
- if (typeof data !== "object" || data === null) {
3329
- return null;
3320
+ __name(call, "call");
3321
+ function canonicalizeVoidMime(raw2) {
3322
+ if (findCrlfCrlf(raw2) === -1) {
3323
+ return raw2;
3324
+ }
3325
+ const boundaries = /* @__PURE__ */ new Map();
3326
+ const output = [];
3327
+ let outputBytes = 0;
3328
+ let copyStart = 0;
3329
+ let changed = false;
3330
+ let inHeaders = true;
3331
+ let topHeaders = true;
3332
+ const append = /* @__PURE__ */ __name((bytes) => {
3333
+ if (bytes.byteLength === 0) {
3334
+ return;
3335
+ }
3336
+ output.push(bytes);
3337
+ outputBytes += bytes.byteLength;
3338
+ }, "append");
3339
+ const replace = /* @__PURE__ */ __name((start, end, replacement) => {
3340
+ append(raw2.subarray(copyStart, start));
3341
+ append(replacement);
3342
+ copyStart = end;
3343
+ changed = true;
3344
+ }, "replace");
3345
+ let lineStart = 0;
3346
+ while (lineStart < raw2.byteLength) {
3347
+ const lineEnd = findCrlf(raw2, lineStart);
3348
+ const nextLine = lineEnd === raw2.byteLength ? raw2.byteLength : lineEnd + 2;
3349
+ if (inHeaders) {
3350
+ if (lineEnd === lineStart) {
3351
+ inHeaders = false;
3352
+ topHeaders = false;
3353
+ } else if (topHeaders && (asciiPrefixEquals(raw2, lineStart, lineEnd, "Date: ") || asciiPrefixEquals(raw2, lineStart, lineEnd, "Message-ID: "))) {
3354
+ replace(lineStart, nextLine, new Uint8Array());
3355
+ } else if (isMultipartContentType(raw2, lineStart, lineEnd)) {
3356
+ const line = decoder3.decode(raw2.subarray(lineStart, lineEnd));
3357
+ const normalized2 = line.replace(
3358
+ /boundary="(=_void_(?:mixed|alternative|related)_[A-Za-z0-9_-]+)"/g,
3359
+ (_, token) => {
3360
+ let marker = boundaries.get(token);
3361
+ if (!marker) {
3362
+ marker = `<void-boundary-${boundaries.size}>`;
3363
+ boundaries.set(token, marker);
3364
+ }
3365
+ return `boundary="${marker}"`;
3366
+ }
3367
+ );
3368
+ if (normalized2 !== line) {
3369
+ replace(lineStart, lineEnd, encoder4.encode(normalized2));
3370
+ }
3371
+ }
3372
+ } else if (raw2[lineStart] === 45 && raw2[lineStart + 1] === 45) {
3373
+ const tokenStart = lineStart + 2;
3374
+ const token = isBoundaryToken(raw2, tokenStart, lineEnd) ? decoder3.decode(raw2.subarray(tokenStart, lineEnd)) : void 0;
3375
+ const openingMarker = token === void 0 ? void 0 : boundaries.get(token);
3376
+ if (openingMarker !== void 0) {
3377
+ replace(lineStart, lineEnd, encoder4.encode(`--${openingMarker}`));
3378
+ inHeaders = true;
3379
+ } else if (lineEnd - tokenStart >= 2 && raw2[lineEnd - 2] === 45 && raw2[lineEnd - 1] === 45 && isBoundaryToken(raw2, tokenStart, lineEnd - 2)) {
3380
+ const closingToken = decoder3.decode(raw2.subarray(tokenStart, lineEnd - 2));
3381
+ const closingMarker = boundaries.get(closingToken);
3382
+ if (closingMarker !== void 0) {
3383
+ replace(lineStart, lineEnd, encoder4.encode(`--${closingMarker}--`));
3384
+ }
3385
+ }
3386
+ }
3387
+ lineStart = nextLine;
3388
+ }
3389
+ if (!changed) {
3390
+ return raw2;
3391
+ }
3392
+ append(raw2.subarray(copyStart));
3393
+ const normalized = new Uint8Array(outputBytes);
3394
+ let offset = 0;
3395
+ for (const chunk of output) {
3396
+ normalized.set(chunk, offset);
3397
+ offset += chunk.byteLength;
3398
+ }
3399
+ return normalized;
3400
+ }
3401
+ __name(canonicalizeVoidMime, "canonicalizeVoidMime");
3402
+ function findCrlfCrlf(raw2) {
3403
+ for (let i = 0; i + 3 < raw2.byteLength; i++) {
3404
+ if (raw2[i] === 13 && raw2[i + 1] === 10 && raw2[i + 2] === 13 && raw2[i + 3] === 10) {
3405
+ return i;
3406
+ }
3407
+ }
3408
+ return -1;
3409
+ }
3410
+ __name(findCrlfCrlf, "findCrlfCrlf");
3411
+ function findCrlf(raw2, start) {
3412
+ let cursor = start;
3413
+ while (cursor < raw2.byteLength) {
3414
+ const cr = raw2.indexOf(13, cursor);
3415
+ if (cr === -1) {
3416
+ return raw2.byteLength;
3417
+ }
3418
+ if (raw2[cr + 1] === 10) {
3419
+ return cr;
3420
+ }
3421
+ cursor = cr + 1;
3422
+ }
3423
+ return raw2.byteLength;
3424
+ }
3425
+ __name(findCrlf, "findCrlf");
3426
+ function asciiPrefixEquals(raw2, start, end, expected) {
3427
+ if (end - start < expected.length) {
3428
+ return false;
3429
+ }
3430
+ for (let i = 0; i < expected.length; i++) {
3431
+ const actual = raw2[start + i];
3432
+ const wanted = expected.charCodeAt(i);
3433
+ if ((actual >= 65 && actual <= 90 ? actual + 32 : actual) !== (wanted | 32)) {
3434
+ return false;
3435
+ }
3436
+ }
3437
+ return true;
3438
+ }
3439
+ __name(asciiPrefixEquals, "asciiPrefixEquals");
3440
+ function isMultipartContentType(raw2, start, end) {
3441
+ const field = "Content-Type:";
3442
+ if (!asciiPrefixEquals(raw2, start, end, field)) {
3443
+ return false;
3444
+ }
3445
+ let cursor = start + field.length;
3446
+ while (cursor < end && (raw2[cursor] === 32 || raw2[cursor] === 9 || raw2[cursor] === 11 || raw2[cursor] === 12)) {
3447
+ cursor++;
3448
+ }
3449
+ return asciiPrefixEquals(raw2, cursor, end, "multipart/");
3450
+ }
3451
+ __name(isMultipartContentType, "isMultipartContentType");
3452
+ function isBoundaryToken(raw2, start, end) {
3453
+ if (end <= start) {
3454
+ return false;
3455
+ }
3456
+ for (let i = start; i < end; i++) {
3457
+ const byte = raw2[i];
3458
+ if (!(byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57 || byte === 95 || byte === 45 || byte === 61)) {
3459
+ return false;
3460
+ }
3461
+ }
3462
+ return true;
3463
+ }
3464
+ __name(isBoundaryToken, "isBoundaryToken");
3465
+ async function emailPayloadDigest(kind, from, recipients, raw2) {
3466
+ const canonical = canonicalizeVoidMime(raw2);
3467
+ const prefix = encoder4.encode(
3468
+ JSON.stringify({
3469
+ kind,
3470
+ from: from.toLowerCase(),
3471
+ recipients: [...new Set(recipients.map((r) => r.toLowerCase()))].sort()
3472
+ })
3473
+ );
3474
+ const input = new Uint8Array(prefix.length + 1 + canonical.length);
3475
+ input.set(prefix);
3476
+ input[prefix.length] = 0;
3477
+ input.set(canonical, prefix.length + 1);
3478
+ return [...new Uint8Array(await crypto.subtle.digest("SHA-256", input))].map((b) => b.toString(16).padStart(2, "0")).join("");
3479
+ }
3480
+ __name(emailPayloadDigest, "emailPayloadDigest");
3481
+ function admitEmail(env, body, deadlineMs = EMAIL_ADMISSION_DEADLINE_MS) {
3482
+ return call(env, "/admit", body, Math.min(deadlineMs, EMAIL_ADMISSION_DEADLINE_MS));
3483
+ }
3484
+ __name(admitEmail, "admitEmail");
3485
+ function startEmailAttempt(env, operationId, recipients, deadlineMs = EMAIL_ACTION_DEADLINE_MS) {
3486
+ return call(
3487
+ env,
3488
+ `/${encodeURIComponent(operationId)}/start`,
3489
+ { recipients },
3490
+ Math.min(deadlineMs, EMAIL_ACTION_DEADLINE_MS)
3491
+ );
3492
+ }
3493
+ __name(startEmailAttempt, "startEmailAttempt");
3494
+ function finishEmailAttempt(env, operationId, recipients, state, details = {}, deadlineMs = EMAIL_ACTION_DEADLINE_MS) {
3495
+ return call(
3496
+ env,
3497
+ `/${encodeURIComponent(operationId)}/finish`,
3498
+ { recipients, state, ...details },
3499
+ Math.min(deadlineMs, EMAIL_ACTION_DEADLINE_MS)
3500
+ );
3501
+ }
3502
+ __name(finishEmailAttempt, "finishEmailAttempt");
3503
+
3504
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/email-lease.ts
3505
+ var LEASE_DEADLINE_MS = 5e3;
3506
+ var MAX_LEASE_RESPONSE_BYTES = 4 * 1024;
3507
+ function leaseBearer(lease) {
3508
+ return lease.token;
3509
+ }
3510
+ __name(leaseBearer, "leaseBearer");
3511
+ async function leaseEmailDomainCredential(env, domain, projectId, deadlineMs = LEASE_DEADLINE_MS) {
3512
+ if (!env.EMAIL_SIGNING_SECRET) {
3513
+ return null;
3514
+ }
3515
+ const controller = new AbortController();
3516
+ const deadline = Date.now() + Math.min(deadlineMs, LEASE_DEADLINE_MS);
3517
+ const beforeDeadline2 = /* @__PURE__ */ __name(async (run) => {
3518
+ const remaining = deadline - Date.now();
3519
+ if (remaining <= 0) {
3520
+ controller.abort();
3521
+ throw new Error("email credential lease timed out");
3522
+ }
3523
+ const promise = run();
3524
+ let timer;
3525
+ try {
3526
+ return await Promise.race([
3527
+ promise,
3528
+ new Promise((_, reject) => {
3529
+ timer = setTimeout(() => {
3530
+ controller.abort();
3531
+ reject(new Error("email credential lease timed out"));
3532
+ }, remaining);
3533
+ })
3534
+ ]);
3535
+ } finally {
3536
+ if (timer !== void 0) {
3537
+ clearTimeout(timer);
3538
+ }
3539
+ }
3540
+ }, "beforeDeadline");
3541
+ let res;
3542
+ try {
3543
+ const token = await hmacSign3(
3544
+ `email-lease-v1
3545
+ ${projectId}
3546
+ ${domain.toLowerCase()}`,
3547
+ env.EMAIL_SIGNING_SECRET
3548
+ );
3549
+ res = await beforeDeadline2(
3550
+ () => env.API.fetch("https://api/internal/email/domain-credential", {
3551
+ method: "POST",
3552
+ headers: {
3553
+ "content-type": "application/json",
3554
+ "x-void-internal": token
3555
+ },
3556
+ body: JSON.stringify({ domain, projectId }),
3557
+ signal: controller.signal
3558
+ })
3559
+ );
3560
+ } catch {
3561
+ return null;
3562
+ }
3563
+ if (!res.ok) {
3564
+ void res.body?.cancel().catch(() => {
3565
+ });
3566
+ return null;
3567
+ }
3568
+ const declared = res.headers.get("content-length");
3569
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > MAX_LEASE_RESPONSE_BYTES)) {
3570
+ void res.body?.cancel().catch(() => {
3571
+ });
3572
+ return null;
3573
+ }
3574
+ const reader = res.body?.getReader();
3575
+ if (!reader) {
3576
+ return null;
3577
+ }
3578
+ const bytes = new Uint8Array(MAX_LEASE_RESPONSE_BYTES);
3579
+ let size = 0;
3580
+ try {
3581
+ for (; ; ) {
3582
+ const chunk = await beforeDeadline2(() => reader.read());
3583
+ if (chunk.done) {
3584
+ break;
3585
+ }
3586
+ if (size + chunk.value.byteLength > bytes.byteLength) {
3587
+ throw new Error("email credential lease response too large");
3588
+ }
3589
+ bytes.set(chunk.value, size);
3590
+ size += chunk.value.byteLength;
3591
+ }
3592
+ if (declared !== null && size !== Number(declared)) {
3593
+ return null;
3594
+ }
3595
+ const data = JSON.parse(
3596
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes.subarray(0, size))
3597
+ );
3598
+ return parseLease(data);
3599
+ } catch {
3600
+ return null;
3601
+ } finally {
3602
+ void reader.cancel().catch(() => {
3603
+ });
3604
+ try {
3605
+ reader.releaseLock();
3606
+ } catch {
3607
+ }
3608
+ }
3609
+ }
3610
+ __name(leaseEmailDomainCredential, "leaseEmailDomainCredential");
3611
+ function parseLease(data) {
3612
+ if (typeof data !== "object" || data === null) {
3613
+ return null;
3330
3614
  }
3331
3615
  const kind = data.kind;
3616
+ const accountId = data.accountId;
3617
+ if (typeof accountId !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(accountId)) {
3618
+ return null;
3619
+ }
3332
3620
  if (kind === "api_token") {
3333
3621
  const token = data.token;
3334
- return typeof token === "string" && token.length > 0 ? { kind, token } : null;
3335
- }
3336
- if (kind === "oauth_grant") {
3337
- const accessToken = data.accessToken;
3338
- return typeof accessToken === "string" && accessToken.length > 0 ? { kind, accessToken } : null;
3622
+ return typeof token === "string" && token.length > 0 ? { kind, token, accountId } : null;
3339
3623
  }
3340
3624
  return null;
3341
3625
  }
3342
3626
  __name(parseLease, "parseLease");
3343
- var encoder3 = new TextEncoder();
3627
+ var encoder5 = new TextEncoder();
3344
3628
  var cachedSignKey3;
3345
3629
  var cachedKeySecret3;
3346
3630
  async function getSignKey3(secret) {
@@ -3349,7 +3633,7 @@ async function getSignKey3(secret) {
3349
3633
  }
3350
3634
  cachedSignKey3 = await crypto.subtle.importKey(
3351
3635
  "raw",
3352
- encoder3.encode(secret),
3636
+ encoder5.encode(secret),
3353
3637
  { name: "HMAC", hash: "SHA-256" },
3354
3638
  false,
3355
3639
  ["sign"]
@@ -3360,18 +3644,111 @@ async function getSignKey3(secret) {
3360
3644
  __name(getSignKey3, "getSignKey");
3361
3645
  async function hmacSign3(data, secret) {
3362
3646
  const key = await getSignKey3(secret);
3363
- const sig = await crypto.subtle.sign("HMAC", key, encoder3.encode(data));
3647
+ const sig = await crypto.subtle.sign("HMAC", key, encoder5.encode(data));
3364
3648
  const binary = String.fromCharCode(...new Uint8Array(sig));
3365
3649
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3366
3650
  }
3367
3651
  __name(hmacSign3, "hmacSign");
3368
3652
 
3653
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/email-provider-deadline.ts
3654
+ var EMAIL_PROVIDER_DEADLINE_MS = 3e4;
3655
+ var EMAIL_PROVIDER_BATCH_DEADLINE_MS = 6e4;
3656
+ var EmailProviderTimeoutError = class extends Error {
3657
+ static {
3658
+ __name(this, "EmailProviderTimeoutError");
3659
+ }
3660
+ constructor() {
3661
+ super("email provider outcome unknown after timeout");
3662
+ this.name = "EmailProviderTimeoutError";
3663
+ }
3664
+ };
3665
+ function createEmailProviderDeadline(totalMs = EMAIL_PROVIDER_BATCH_DEADLINE_MS) {
3666
+ const deadline = Date.now() + totalMs;
3667
+ return {
3668
+ remainingMs(reserveMs = 0) {
3669
+ return Math.max(0, deadline - Date.now() - reserveMs);
3670
+ },
3671
+ expired(reserveMs = 0) {
3672
+ return Date.now() >= deadline - reserveMs;
3673
+ },
3674
+ async run(submit, reserveMs = 0) {
3675
+ const remaining = Math.min(EMAIL_PROVIDER_DEADLINE_MS, deadline - Date.now() - reserveMs);
3676
+ if (remaining <= 0) {
3677
+ throw new EmailProviderTimeoutError();
3678
+ }
3679
+ const work = submit();
3680
+ let timer;
3681
+ try {
3682
+ return await Promise.race([
3683
+ work,
3684
+ new Promise((_, reject) => {
3685
+ timer = setTimeout(() => reject(new EmailProviderTimeoutError()), remaining);
3686
+ })
3687
+ ]);
3688
+ } finally {
3689
+ if (timer !== void 0) {
3690
+ clearTimeout(timer);
3691
+ }
3692
+ }
3693
+ }
3694
+ };
3695
+ }
3696
+ __name(createEmailProviderDeadline, "createEmailProviderDeadline");
3697
+
3369
3698
  // ../../home/runner/work/void/void/platform/packages/proxy/src/email.ts
3370
- init_performance2();
3371
- import { EmailMessage } from "cloudflare:email";
3372
3699
  var app = new Hono2();
3373
- var MAX_MIME_BYTES = 10 * 1024 * 1024;
3374
- var MAX_RECIPIENTS = 100;
3700
+ var EMAIL_SEND_DEADLINE_MS = 5e4;
3701
+ var EMAIL_SEND_FINALIZATION_MARGIN_MS = 5e3;
3702
+ var MAX_MIME_BYTES = 5 * 1024 * 1024;
3703
+ var MAX_CUSTOM_HEADER_BYTES = 16 * 1024;
3704
+ var BUILTIN_MIME_HEADERS = /* @__PURE__ */ new Set([
3705
+ "from",
3706
+ "to",
3707
+ "cc",
3708
+ "bcc",
3709
+ "subject",
3710
+ "reply-to",
3711
+ "date",
3712
+ "message-id",
3713
+ "in-reply-to",
3714
+ "references",
3715
+ "mime-version",
3716
+ "content-type",
3717
+ "content-transfer-encoding",
3718
+ "content-disposition"
3719
+ ]);
3720
+ function customMimeHeaderBytesWithinLimit(raw2) {
3721
+ let used = 0;
3722
+ let custom = false;
3723
+ for (let start = 0; start < raw2.byteLength; ) {
3724
+ let end = start;
3725
+ while (end < raw2.byteLength && raw2[end] !== 10) {
3726
+ end++;
3727
+ }
3728
+ const lineEnd = end > start && raw2[end - 1] === 13 ? end - 1 : end;
3729
+ if (lineEnd === start) {
3730
+ return true;
3731
+ }
3732
+ if (raw2[start] !== 32 && raw2[start] !== 9) {
3733
+ let colon = start;
3734
+ while (colon < lineEnd && raw2[colon] !== 58) {
3735
+ colon++;
3736
+ }
3737
+ const name = new TextDecoder().decode(raw2.subarray(start, colon)).toLowerCase();
3738
+ custom = colon < lineEnd && !BUILTIN_MIME_HEADERS.has(name);
3739
+ }
3740
+ if (custom) {
3741
+ used += lineEnd - start + 2;
3742
+ if (used > MAX_CUSTOM_HEADER_BYTES) {
3743
+ return false;
3744
+ }
3745
+ }
3746
+ start = end + 1;
3747
+ }
3748
+ return true;
3749
+ }
3750
+ __name(customMimeHeaderBytesWithinLimit, "customMimeHeaderBytesWithinLimit");
3751
+ var MAX_RECIPIENTS = 50;
3375
3752
  var MAX_RECIPIENT_LENGTH = 320;
3376
3753
  var MAX_SENDER_LENGTH = 320;
3377
3754
  function readSenderDomain(env) {
@@ -3394,12 +3771,11 @@ function strictFromRe(domain) {
3394
3771
  if (cached) {
3395
3772
  return cached;
3396
3773
  }
3397
- const re = new RegExp(`^[a-z0-9._-]+(?:\\+[a-z0-9._=/-]+)?@${escapeRegExp(domain)}$`);
3774
+ const re = new RegExp(`^[a-z0-9._-]+(?:\\+[a-z0-9._=/-]+)*@${escapeRegExp(domain)}$`);
3398
3775
  strictFromCache.set(domain, re);
3399
3776
  return re;
3400
3777
  }
3401
3778
  __name(strictFromRe, "strictFromRe");
3402
- var SLUG_RE = /^[a-z0-9._-]+$/;
3403
3779
  var CONTROL_CHARS_RE = new RegExp("[\\u0000-\\u001f\\u007f]+", "g");
3404
3780
  function sanitizeForLog(value, maxLen = 200) {
3405
3781
  return value.slice(0, maxLen).replace(CONTROL_CHARS_RE, " ");
@@ -3579,37 +3955,22 @@ function singleMailboxAddrSpec(value) {
3579
3955
  return /^[\x21-\x7e]+$/.test(addrSpec) ? addrSpec : null;
3580
3956
  }
3581
3957
  __name(singleMailboxAddrSpec, "singleMailboxAddrSpec");
3582
- async function readRoutingSuspension(kv2, slug) {
3583
- const raw2 = await kv2.get(slug);
3584
- if (!raw2) {
3585
- return { kind: "absent" };
3586
- }
3587
- let parsed;
3588
- try {
3589
- parsed = JSON.parse(raw2);
3590
- } catch {
3591
- return { kind: "corrupt" };
3592
- }
3593
- if (typeof parsed !== "object" || parsed === null) {
3594
- return { kind: "corrupt" };
3595
- }
3596
- const suspended = parsed.suspended;
3597
- if (!suspended) {
3598
- return { kind: "clear" };
3599
- }
3600
- if (typeof suspended !== "string") {
3601
- return { kind: "corrupt" };
3602
- }
3603
- return { kind: "suspended", reason: suspended };
3604
- }
3605
- __name(readRoutingSuspension, "readRoutingSuspension");
3606
3958
  app.post("/send", async (context) => {
3959
+ const sendDeadline = createEmailProviderDeadline(EMAIL_SEND_DEADLINE_MS);
3607
3960
  const projectId = context.get("projectId");
3608
3961
  const body = context.get("body");
3609
3962
  const from = asString(body.from);
3610
3963
  const mimeBase64 = asString(body.mimeBase64);
3611
3964
  const requestIdRaw = asString(body.requestId);
3612
3965
  const requestId = requestIdRaw === null ? "unknown" : sanitizeForLog(requestIdRaw, MAX_REQUEST_ID_LENGTH);
3966
+ const callerKey = asString(body.idempotencyKey);
3967
+ if (callerKey !== null && (callerKey.length === 0 || callerKey.length > 128 || !/^[\x20-\x7e]+$/.test(callerKey))) {
3968
+ return context.json(
3969
+ { ok: false, error: { code: "INVALID_TO", message: "invalid idempotency key" } },
3970
+ 400
3971
+ );
3972
+ }
3973
+ const operationKey = callerKey ?? (requestIdRaw && requestIdRaw.length <= 128 && /^[\x20-\x7e]+$/.test(requestIdRaw) ? requestIdRaw : crypto.randomUUID());
3613
3974
  const recipientsRaw = Array.isArray(body.recipients) ? body.recipients : null;
3614
3975
  if (!from || !mimeBase64 || !recipientsRaw) {
3615
3976
  return context.json(
@@ -3654,19 +4015,6 @@ app.post("/send", async (context) => {
3654
4015
  502
3655
4016
  );
3656
4017
  }
3657
- const sendBinding = context.env.SEND_EMAIL;
3658
- if (typeof sendBinding?.send !== "function") {
3659
- return context.json(
3660
- {
3661
- ok: false,
3662
- error: {
3663
- code: "UPSTREAM_ERROR",
3664
- message: "email sending is not configured"
3665
- }
3666
- },
3667
- 502
3668
- );
3669
- }
3670
4018
  const recipients = [];
3671
4019
  const seen = /* @__PURE__ */ new Set();
3672
4020
  for (const r of recipientsRaw) {
@@ -3731,6 +4079,15 @@ app.post("/send", async (context) => {
3731
4079
  413
3732
4080
  );
3733
4081
  }
4082
+ if (!customMimeHeaderBytesWithinLimit(raw2)) {
4083
+ return context.json(
4084
+ {
4085
+ ok: false,
4086
+ error: { code: "MIME_ERROR", message: "custom headers exceed the 16 KiB limit" }
4087
+ },
4088
+ 413
4089
+ );
4090
+ }
3734
4091
  const fromAt = from.indexOf("@");
3735
4092
  const fromDomain = fromAt === -1 ? "" : from.slice(fromAt + 1).toLowerCase();
3736
4093
  if (fromDomain !== "" && fromDomain !== senderDomain) {
@@ -3742,49 +4099,25 @@ app.post("/send", async (context) => {
3742
4099
  senderDomain,
3743
4100
  recipients,
3744
4101
  raw2,
3745
- requestId
3746
- );
3747
- }
3748
- const expectedSlug = await context.env.ROUTING.get(`email-slug:${projectId}`);
3749
- if (!expectedSlug) {
3750
- return context.json(
3751
- {
3752
- ok: false,
3753
- error: {
3754
- code: "UPSTREAM_ERROR",
3755
- message: "project slug unknown \u2014 refusing to send"
3756
- }
3757
- },
3758
- 502
4102
+ requestId,
4103
+ operationKey,
4104
+ sendDeadline
3759
4105
  );
3760
4106
  }
3761
- if (!SLUG_RE.test(expectedSlug)) {
3762
- console.error(
3763
- `[email-send] project ${projectId} slug malformed in KV: ${sanitizeForLog(expectedSlug, 64)}`
3764
- );
4107
+ const sendBinding = context.env.SEND_EMAIL;
4108
+ if (typeof sendBinding?.send !== "function") {
3765
4109
  return context.json(
3766
4110
  {
3767
4111
  ok: false,
3768
4112
  error: {
3769
4113
  code: "UPSTREAM_ERROR",
3770
- message: "project slug malformed in routing"
4114
+ message: "email sending is not configured"
3771
4115
  }
3772
4116
  },
3773
4117
  502
3774
4118
  );
3775
4119
  }
3776
- if (isReservedEmailSlug(expectedSlug)) {
3777
- return context.json(
3778
- {
3779
- ok: false,
3780
- error: {
3781
- code: "INVALID_FROM",
3782
- message: `project slug "${expectedSlug}" is a reserved email identity on ${senderDomain} \u2014 refusing to send`
3783
- }
3784
- },
3785
- 403
3786
- );
3787
- }
4120
+ const expectedSlug = from.split("@")[0]?.split("+")[0] ?? "";
3788
4121
  if (!/^[\x21-\x7e]+$/.test(from)) {
3789
4122
  return context.json(
3790
4123
  {
@@ -3835,119 +4168,160 @@ app.post("/send", async (context) => {
3835
4168
  403
3836
4169
  );
3837
4170
  }
3838
- const platformSuspension = await readRoutingSuspension(context.env.ROUTING, expectedSlug);
3839
- if (platformSuspension.kind === "corrupt") {
3840
- console.error(
3841
- `[email-send] project ${projectId} routing blob malformed in KV for slug ${sanitizeForLog(expectedSlug, 64)}`
3842
- );
3843
- return context.json(
4171
+ let admission;
4172
+ try {
4173
+ admission = await admitEmail(
4174
+ context.env,
3844
4175
  {
3845
- ok: false,
3846
- error: {
3847
- code: "UPSTREAM_ERROR",
3848
- message: "project routing entry malformed"
3849
- }
4176
+ projectId,
4177
+ kind: "direct",
4178
+ key: operationKey,
4179
+ digest: await emailPayloadDigest("direct", from, recipients, raw2),
4180
+ from,
4181
+ recipients,
4182
+ transport: "binding"
3850
4183
  },
3851
- 502
4184
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
3852
4185
  );
3853
- }
3854
- if (platformSuspension.kind === "suspended") {
4186
+ } catch (error) {
4187
+ const conflict = error instanceof Error && error.message === "IDEMPOTENCY_CONFLICT";
3855
4188
  return context.json(
3856
4189
  {
3857
4190
  ok: false,
3858
4191
  error: {
3859
- code: "QUOTA_EXCEEDED",
3860
- message: `project is suspended (${sanitizeForLog(platformSuspension.reason, 64)})`
4192
+ code: conflict ? "IDEMPOTENCY_CONFLICT" : "UPSTREAM_ERROR",
4193
+ message: conflict ? "idempotency key has different content" : "email admission unavailable"
3861
4194
  }
3862
4195
  },
3863
- 200
3864
- );
3865
- }
3866
- if (await context.env.ROUTING.get(`email-suspended:${projectId}`)) {
3867
- return context.json(
3868
- { ok: false, error: { code: "QUOTA_EXCEEDED", message: "project email is suspended" } },
3869
- 200
4196
+ conflict ? 409 : 503
3870
4197
  );
3871
4198
  }
3872
- const allowlist = await readAllowlist(context.env.ROUTING, projectId);
3873
- const allowed = [];
3874
- const deliveries = [];
3875
- for (const r of recipients) {
3876
- if (allowlist.has(r)) {
3877
- allowed.push(r);
3878
- } else {
4199
+ const deliveries = admission.recipients.filter((r) => r.state !== "reserved").map((r) => ({
4200
+ recipient: r.recipient,
4201
+ ok: r.state === "provider_accepted",
4202
+ state: r.state,
4203
+ messageId: r.providerReference ?? void 0,
4204
+ error: r.state === "provider_accepted" ? void 0 : {
4205
+ code: r.errorCode ?? (r.state === "outcome_unknown" || r.state === "attempt_started" ? "OUTCOME_UNKNOWN" : r.state === "cancelled" ? "UPSTREAM_ERROR" : "UNVERIFIED_DESTINATION"),
4206
+ message: r.state === "outcome_unknown" || r.state === "attempt_started" ? "provider outcome unknown; inspect email logs before retrying" : r.state === "cancelled" ? "email attempt was cancelled before provider submission" : "recipient was not admitted"
4207
+ }
4208
+ }));
4209
+ if (admission.replay) {
4210
+ for (const row of admission.recipients.filter((r) => r.state === "reserved")) {
3879
4211
  deliveries.push({
3880
- recipient: r,
4212
+ recipient: row.recipient,
3881
4213
  ok: false,
4214
+ state: "reserved",
3882
4215
  error: {
3883
- code: "UNVERIFIED_DESTINATION",
3884
- message: `recipient '${r}' is not a verified destination for this project`
4216
+ code: "OUTCOME_UNKNOWN",
4217
+ message: "email action remains reserved; inspect email logs before retrying"
3885
4218
  }
3886
4219
  });
3887
4220
  }
3888
- }
3889
- if (allowed.length === 0) {
3890
- return context.json({ ok: false, deliveries }, 200);
3891
- }
3892
- const now = /* @__PURE__ */ new Date();
3893
- const burstKey = `email-burst:${projectId}:${minuteBucket(now)}`;
3894
- const burstCurrent = readCounter(await context.env.ROUTING.get(burstKey));
3895
- const burstLimit = await getBurstLimit(context.env.ROUTING, projectId);
3896
- if (burstCurrent + allowed.length > burstLimit) {
3897
- return context.json(
3898
- {
3899
- ok: false,
3900
- error: {
3901
- code: "QUOTA_EXCEEDED",
3902
- message: `outbound burst limit ${burstLimit}/min exceeded`
3903
- }
3904
- },
3905
- 429
3906
- );
3907
- }
3908
- const counterKey = `email-sent:${projectId}:${monthBucket(now)}`;
3909
- const monthCurrent = readCounter(await context.env.ROUTING.get(counterKey));
3910
- const monthLimit = await getMonthlyLimit(context.env.ROUTING, projectId);
3911
- if (monthCurrent + allowed.length > monthLimit) {
3912
- return context.json(
3913
- {
4221
+ return emailActionResponse(context, admission.operationId, deliveries);
4222
+ }
4223
+ const allowed = admission.recipients.filter((r) => r.state === "reserved").map((r) => r.recipient);
4224
+ for (let index = 0; index < allowed.length; index++) {
4225
+ const r = allowed[index];
4226
+ if (sendDeadline.expired(EMAIL_SEND_FINALIZATION_MARGIN_MS)) {
4227
+ const unstarted = allowed.slice(index);
4228
+ const cancelled = await cancelUnstartedRecipients(
4229
+ context.env,
4230
+ admission.operationId,
4231
+ unstarted,
4232
+ sendDeadline.remainingMs()
4233
+ );
4234
+ for (const recipient of unstarted) {
4235
+ deliveries.push({
4236
+ recipient,
4237
+ ok: false,
4238
+ state: cancelled ? "cancelled" : "outcome_unknown",
4239
+ error: cancelled ? {
4240
+ code: "UPSTREAM_ERROR",
4241
+ message: "email batch deadline elapsed before provider submission"
4242
+ } : { code: "OUTCOME_UNKNOWN", message: "could not cancel unstarted email attempt" }
4243
+ });
4244
+ }
4245
+ break;
4246
+ }
4247
+ try {
4248
+ await startEmailAttempt(
4249
+ context.env,
4250
+ admission.operationId,
4251
+ [r],
4252
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
4253
+ );
4254
+ } catch {
4255
+ const cancelled = await cancelUnstartedRecipients(
4256
+ context.env,
4257
+ admission.operationId,
4258
+ [r],
4259
+ sendDeadline.remainingMs()
4260
+ );
4261
+ deliveries.push({
4262
+ recipient: r,
3914
4263
  ok: false,
3915
- error: {
3916
- code: "QUOTA_EXCEEDED",
3917
- message: `monthly outbound limit ${monthLimit} exceeded`
3918
- }
3919
- },
3920
- 429
3921
- );
3922
- }
3923
- await Promise.all([
3924
- context.env.ROUTING.put(counterKey, String(monthCurrent + allowed.length), {
3925
- expirationTtl: monthTtlSeconds(now)
3926
- }).catch((e) => {
3927
- console.error(`[email-send] monthly counter put failed for ${projectId}:`, e);
3928
- }),
3929
- context.env.ROUTING.put(burstKey, String(burstCurrent + allowed.length), {
3930
- expirationTtl: 5 * 60
3931
- }).catch((e) => {
3932
- console.error(`[email-send] burst counter put failed for ${projectId}:`, e);
3933
- })
3934
- ]);
3935
- for (const r of allowed) {
4264
+ state: cancelled ? "cancelled" : "outcome_unknown",
4265
+ error: cancelled ? {
4266
+ code: "UPSTREAM_ERROR",
4267
+ message: "email attempt did not start; reservation cancelled"
4268
+ } : { code: "OUTCOME_UNKNOWN", message: "could not confirm email attempt status" }
4269
+ });
4270
+ continue;
4271
+ }
3936
4272
  try {
3937
4273
  const msg = new EmailMessage(from, r, bytesToStream(raw2));
3938
- const result = await sendBinding.send(msg);
4274
+ const result = await sendDeadline.run(
4275
+ () => sendBinding.send(msg),
4276
+ EMAIL_SEND_FINALIZATION_MARGIN_MS
4277
+ );
4278
+ const providerReference = typeof result?.messageId === "string" ? result.messageId : "";
4279
+ try {
4280
+ await finishEmailAttempt(
4281
+ context.env,
4282
+ admission.operationId,
4283
+ [r],
4284
+ "provider_accepted",
4285
+ { providerReference },
4286
+ sendDeadline.remainingMs()
4287
+ );
4288
+ } catch {
4289
+ deliveries.push({
4290
+ recipient: r,
4291
+ ok: false,
4292
+ state: "outcome_unknown",
4293
+ error: {
4294
+ code: "OUTCOME_UNKNOWN",
4295
+ message: "provider accepted email but outcome could not be recorded"
4296
+ }
4297
+ });
4298
+ continue;
4299
+ }
3939
4300
  deliveries.push({
3940
4301
  recipient: r,
3941
4302
  ok: true,
3942
- messageId: typeof result?.messageId === "string" ? result.messageId : ""
4303
+ state: "provider_accepted",
4304
+ messageId: providerReference
3943
4305
  });
3944
- } catch (e) {
3945
- deliveries.push({
3946
- recipient: r,
3947
- ok: false,
3948
- error: {
3949
- code: classifyUpstream(e, projectId),
3950
- message: e instanceof Error ? e.message : String(e)
4306
+ } catch {
4307
+ try {
4308
+ await finishEmailAttempt(
4309
+ context.env,
4310
+ admission.operationId,
4311
+ [r],
4312
+ "outcome_unknown",
4313
+ { errorCode: "OUTCOME_UNKNOWN" },
4314
+ sendDeadline.remainingMs()
4315
+ );
4316
+ } catch {
4317
+ }
4318
+ deliveries.push({
4319
+ recipient: r,
4320
+ ok: false,
4321
+ state: "outcome_unknown",
4322
+ error: {
4323
+ code: "OUTCOME_UNKNOWN",
4324
+ message: "provider outcome unknown; inspect email logs before retrying"
3951
4325
  }
3952
4326
  });
3953
4327
  }
@@ -3955,29 +4329,166 @@ app.post("/send", async (context) => {
3955
4329
  logSend(projectId, requestId, deliveries);
3956
4330
  const anyFailed = deliveries.some((d) => !d.ok);
3957
4331
  if (anyFailed) {
3958
- return context.json({ ok: false, deliveries }, 200);
4332
+ return context.json({ ok: false, operationId: admission.operationId, deliveries }, 200);
3959
4333
  }
3960
4334
  return context.json({
3961
4335
  ok: true,
4336
+ operationId: admission.operationId,
3962
4337
  ids: deliveries.map((d) => ({
3963
4338
  recipient: d.recipient,
3964
- messageId: d.messageId
4339
+ messageId: d.messageId,
4340
+ state: "provider_accepted"
3965
4341
  }))
3966
4342
  });
3967
4343
  });
4344
+ var MAX_SEND_RAW_RESPONSE_BYTES = 64 * 1024;
4345
+ async function requestSendRaw(input, deadlineMs = EMAIL_PROVIDER_DEADLINE_MS) {
4346
+ const controller = new AbortController();
4347
+ const deadline = Date.now() + Math.min(EMAIL_PROVIDER_DEADLINE_MS, deadlineMs);
4348
+ const requestBody = JSON.stringify({
4349
+ from: input.from,
4350
+ recipients: input.recipients,
4351
+ mime_message: input.mimeMessage
4352
+ });
4353
+ const beforeDeadline2 = /* @__PURE__ */ __name(async (run) => {
4354
+ const remaining = deadline - Date.now();
4355
+ if (remaining <= 0) {
4356
+ controller.abort();
4357
+ throw new Error("email sending request timed out");
4358
+ }
4359
+ const promise = run();
4360
+ let timer;
4361
+ try {
4362
+ return await Promise.race([
4363
+ promise,
4364
+ new Promise((_, reject) => {
4365
+ timer = setTimeout(() => {
4366
+ controller.abort();
4367
+ reject(new Error("email sending request timed out"));
4368
+ }, remaining);
4369
+ })
4370
+ ]);
4371
+ } finally {
4372
+ if (timer !== void 0) {
4373
+ clearTimeout(timer);
4374
+ }
4375
+ }
4376
+ }, "beforeDeadline");
4377
+ const response = await beforeDeadline2(
4378
+ () => fetch(
4379
+ `https://api.cloudflare.com/client/v4/accounts/${input.accountId}/email/sending/send_raw`,
4380
+ {
4381
+ method: "POST",
4382
+ headers: {
4383
+ authorization: `Bearer ${input.bearer}`,
4384
+ "content-type": "application/json"
4385
+ },
4386
+ body: requestBody,
4387
+ signal: controller.signal
4388
+ }
4389
+ )
4390
+ );
4391
+ if (!response.ok) {
4392
+ void response.body?.cancel().catch(() => {
4393
+ });
4394
+ return { ok: false, status: response.status };
4395
+ }
4396
+ const declared = response.headers.get("content-length");
4397
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > MAX_SEND_RAW_RESPONSE_BYTES)) {
4398
+ void response.body?.cancel().catch(() => {
4399
+ });
4400
+ throw new Error("email sending API response too large");
4401
+ }
4402
+ const reader = response.body?.getReader();
4403
+ if (!reader) {
4404
+ throw new Error("email sending API response unreadable");
4405
+ }
4406
+ const bytes = new Uint8Array(MAX_SEND_RAW_RESPONSE_BYTES);
4407
+ let size = 0;
4408
+ try {
4409
+ for (; ; ) {
4410
+ const chunk = await beforeDeadline2(() => reader.read());
4411
+ if (chunk.done) {
4412
+ break;
4413
+ }
4414
+ if (size + chunk.value.byteLength > bytes.byteLength) {
4415
+ throw new Error("email sending API response too large");
4416
+ }
4417
+ bytes.set(chunk.value, size);
4418
+ size += chunk.value.byteLength;
4419
+ }
4420
+ if (declared !== null && size !== Number(declared)) {
4421
+ throw new Error("email sending API response length mismatch");
4422
+ }
4423
+ return {
4424
+ ok: true,
4425
+ data: JSON.parse(
4426
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes.subarray(0, size))
4427
+ )
4428
+ };
4429
+ } finally {
4430
+ void reader.cancel().catch(() => {
4431
+ });
4432
+ try {
4433
+ reader.releaseLock();
4434
+ } catch {
4435
+ }
4436
+ }
4437
+ }
4438
+ __name(requestSendRaw, "requestSendRaw");
3968
4439
  function readSendRawAddressList(value) {
4440
+ if (!Array.isArray(value)) {
4441
+ return null;
4442
+ }
3969
4443
  const out = /* @__PURE__ */ new Set();
3970
- if (Array.isArray(value)) {
3971
- for (const entry of value) {
3972
- if (typeof entry === "string") {
3973
- out.add(entry.toLowerCase());
3974
- }
4444
+ for (const entry of value) {
4445
+ if (typeof entry !== "string") {
4446
+ return null;
3975
4447
  }
4448
+ out.add(entry.toLowerCase());
3976
4449
  }
3977
4450
  return out;
3978
4451
  }
3979
4452
  __name(readSendRawAddressList, "readSendRawAddressList");
3980
- async function sendViaCustomDomain(context, projectId, from, fromDomain, senderDomain, recipients, raw2, requestId) {
4453
+ function parseSendRawResult(value) {
4454
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4455
+ return null;
4456
+ }
4457
+ const result = value;
4458
+ const delivered = readSendRawAddressList(result.delivered);
4459
+ const permanentBounces = readSendRawAddressList(result.permanent_bounces);
4460
+ const queued = readSendRawAddressList(result.queued);
4461
+ const suppressedRecipients = readSendRawAddressList(result.suppressed_recipients);
4462
+ if (!delivered || !permanentBounces || !queued || !suppressedRecipients || typeof result.message_id !== "string" || result.message_id === "") {
4463
+ return null;
4464
+ }
4465
+ return {
4466
+ delivered,
4467
+ messageId: result.message_id,
4468
+ permanentBounces,
4469
+ queued,
4470
+ suppressedRecipients
4471
+ };
4472
+ }
4473
+ __name(parseSendRawResult, "parseSendRawResult");
4474
+ function classifySendRawRecipient(result, recipient) {
4475
+ const target = recipient.toLowerCase();
4476
+ const classifications = [
4477
+ result.delivered.has(target),
4478
+ result.queued.has(target),
4479
+ result.permanentBounces.has(target),
4480
+ result.suppressedRecipients.has(target)
4481
+ ];
4482
+ if (classifications.filter(Boolean).length !== 1) {
4483
+ return "outcome_unknown";
4484
+ }
4485
+ if (classifications[0] || classifications[1]) {
4486
+ return "provider_accepted";
4487
+ }
4488
+ return classifications[2] ? "permanent_bounce" : "suppressed";
4489
+ }
4490
+ __name(classifySendRawRecipient, "classifySendRawRecipient");
4491
+ async function sendViaCustomDomain(context, projectId, from, fromDomain, senderDomain, recipients, raw2, requestId, operationKey, sendDeadline) {
3981
4492
  if (!/^[\x21-\x7e]+$/.test(from)) {
3982
4493
  return context.json(
3983
4494
  {
@@ -3988,28 +4499,6 @@ async function sendViaCustomDomain(context, projectId, from, fromDomain, senderD
3988
4499
  );
3989
4500
  }
3990
4501
  const fromLower = from.toLowerCase();
3991
- const mirror = await readEmailDomainMirror(context.env.ROUTING, fromDomain, { cacheTtl: 60 });
3992
- if (!mirror || mirror.projectId !== projectId || mirror.status !== "active") {
3993
- return context.json(
3994
- {
3995
- ok: false,
3996
- error: {
3997
- code: "INVALID_FROM",
3998
- message: `from address must be on the configured sender domain (${senderDomain}) or a domain registered to this project`
3999
- }
4000
- },
4001
- 403
4002
- );
4003
- }
4004
- if (!mirror.accountId) {
4005
- return context.json(
4006
- {
4007
- ok: false,
4008
- error: { code: "UPSTREAM_ERROR", message: "email domain registration is incomplete" }
4009
- },
4010
- 502
4011
- );
4012
- }
4013
4502
  const mimeFrom = readMimeFromAddrSpec(raw2);
4014
4503
  if (!mimeFrom.ok || mimeFrom.addrSpec.toLowerCase() !== fromLower) {
4015
4504
  const detail = mimeFrom.ok ? "the From header address does not match the envelope sender" : MIME_FROM_REFUSALS[mimeFrom.reason];
@@ -4024,1482 +4513,428 @@ async function sendViaCustomDomain(context, projectId, from, fromDomain, senderD
4024
4513
  403
4025
4514
  );
4026
4515
  }
4027
- const slug = await context.env.ROUTING.get(`email-slug:${projectId}`);
4028
- if (!slug) {
4516
+ let mimeMessage;
4517
+ try {
4518
+ mimeMessage = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(raw2);
4519
+ } catch {
4029
4520
  return context.json(
4030
- {
4031
- ok: false,
4032
- error: {
4033
- code: "UPSTREAM_ERROR",
4034
- message: "project slug unknown \u2014 refusing to send"
4035
- }
4036
- },
4037
- 502
4521
+ { ok: false, error: { code: "MIME_ERROR", message: "mime must be valid UTF-8" } },
4522
+ 400
4038
4523
  );
4039
4524
  }
4040
- if (!SLUG_RE.test(slug)) {
4041
- console.error(
4042
- `[email-send] project ${projectId} slug malformed in KV: ${sanitizeForLog(slug, 64)}`
4043
- );
4044
- return context.json(
4525
+ let admission;
4526
+ try {
4527
+ admission = await admitEmail(
4528
+ context.env,
4045
4529
  {
4046
- ok: false,
4047
- error: {
4048
- code: "UPSTREAM_ERROR",
4049
- message: "project slug malformed in routing"
4050
- }
4530
+ projectId,
4531
+ kind: "direct",
4532
+ key: operationKey,
4533
+ digest: await emailPayloadDigest("direct", from, recipients, raw2),
4534
+ from,
4535
+ recipients,
4536
+ transport: "send_raw"
4051
4537
  },
4052
- 502
4053
- );
4054
- }
4055
- const platformSuspension = await readRoutingSuspension(context.env.ROUTING, slug);
4056
- if (platformSuspension.kind === "corrupt") {
4057
- console.error(
4058
- `[email-send] project ${projectId} routing blob malformed in KV for slug ${sanitizeForLog(slug, 64)}`
4538
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
4059
4539
  );
4540
+ } catch (error) {
4541
+ const conflict = error instanceof Error && error.message === "IDEMPOTENCY_CONFLICT";
4060
4542
  return context.json(
4061
4543
  {
4062
4544
  ok: false,
4063
4545
  error: {
4064
- code: "UPSTREAM_ERROR",
4065
- message: "project routing entry malformed"
4546
+ code: conflict ? "IDEMPOTENCY_CONFLICT" : "UPSTREAM_ERROR",
4547
+ message: conflict ? "idempotency key has different content" : "email admission unavailable"
4066
4548
  }
4067
4549
  },
4068
- 502
4550
+ conflict ? 409 : 503
4069
4551
  );
4070
4552
  }
4071
- if (platformSuspension.kind === "suspended") {
4072
- return context.json(
4073
- {
4553
+ const deliveries = admission.recipients.filter((r) => r.state !== "reserved").map((r) => ({
4554
+ recipient: r.recipient,
4555
+ ok: r.state === "provider_accepted",
4556
+ state: r.state,
4557
+ messageId: r.providerReference ?? void 0,
4558
+ error: r.state === "provider_accepted" ? void 0 : {
4559
+ code: r.errorCode ?? (r.state === "outcome_unknown" || r.state === "attempt_started" ? "OUTCOME_UNKNOWN" : r.state === "cancelled" ? "UPSTREAM_ERROR" : "UNVERIFIED_DESTINATION"),
4560
+ message: r.state === "outcome_unknown" || r.state === "attempt_started" ? "provider outcome unknown; inspect email logs before retrying" : r.state === "cancelled" ? "email attempt was cancelled before provider submission" : "recipient was not admitted"
4561
+ }
4562
+ }));
4563
+ if (admission.replay) {
4564
+ for (const row of admission.recipients.filter((r) => r.state === "reserved")) {
4565
+ deliveries.push({
4566
+ recipient: row.recipient,
4074
4567
  ok: false,
4568
+ state: "reserved",
4075
4569
  error: {
4076
- code: "QUOTA_EXCEEDED",
4077
- message: `project is suspended (${sanitizeForLog(platformSuspension.reason, 64)})`
4570
+ code: "OUTCOME_UNKNOWN",
4571
+ message: "email action remains reserved; inspect email logs before retrying"
4078
4572
  }
4079
- },
4080
- 200
4081
- );
4082
- }
4083
- if (await context.env.ROUTING.get(`email-suspended:${projectId}`)) {
4084
- return context.json(
4085
- { ok: false, error: { code: "QUOTA_EXCEEDED", message: "project email is suspended" } },
4086
- 200
4087
- );
4573
+ });
4574
+ }
4575
+ return emailActionResponse(context, admission.operationId, deliveries);
4088
4576
  }
4089
- const now = /* @__PURE__ */ new Date();
4090
- const burstKey = `email-burst:${projectId}:${minuteBucket(now)}`;
4091
- const burstCurrent = readCounter(await context.env.ROUTING.get(burstKey));
4092
- const burstLimit = await getBurstLimit(context.env.ROUTING, projectId);
4093
- if (burstCurrent + recipients.length > burstLimit) {
4094
- return context.json(
4095
- {
4096
- ok: false,
4097
- error: {
4098
- code: "QUOTA_EXCEEDED",
4099
- message: `outbound burst limit ${burstLimit}/min exceeded`
4100
- }
4101
- },
4102
- 429
4103
- );
4577
+ recipients = admission.recipients.filter((r) => r.state === "reserved").map((r) => r.recipient);
4578
+ if (recipients.length === 0) {
4579
+ return emailActionResponse(context, admission.operationId, deliveries);
4104
4580
  }
4105
- const counterKey = `email-sent:${projectId}:${monthBucket(now)}`;
4106
- const monthCurrent = readCounter(await context.env.ROUTING.get(counterKey));
4107
- const monthLimit = await getMonthlyLimit(context.env.ROUTING, projectId);
4108
- if (monthCurrent + recipients.length > monthLimit) {
4581
+ const lease = await leaseEmailDomainCredential(
4582
+ context.env,
4583
+ fromDomain,
4584
+ projectId,
4585
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
4586
+ );
4587
+ if (!lease) {
4588
+ await finishEmailAttempt(
4589
+ context.env,
4590
+ admission.operationId,
4591
+ recipients,
4592
+ "cancelled",
4593
+ {},
4594
+ sendDeadline.remainingMs()
4595
+ ).catch(() => {
4596
+ });
4109
4597
  return context.json(
4110
4598
  {
4111
4599
  ok: false,
4112
- error: {
4113
- code: "QUOTA_EXCEEDED",
4114
- message: `monthly outbound limit ${monthLimit} exceeded`
4115
- }
4600
+ operationId: admission.operationId,
4601
+ error: { code: "UPSTREAM_ERROR", message: "could not lease the domain credential" }
4116
4602
  },
4117
- 429
4118
- );
4119
- }
4120
- let mimeMessage;
4121
- try {
4122
- mimeMessage = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(raw2);
4123
- } catch {
4124
- return context.json(
4125
- { ok: false, error: { code: "MIME_ERROR", message: "mime must be valid UTF-8" } },
4126
- 400
4603
+ 502
4127
4604
  );
4128
4605
  }
4129
- const lease = await leaseEmailDomainCredential(context.env, fromDomain);
4130
- if (!lease) {
4606
+ if (admission.accountId !== lease.accountId) {
4607
+ await finishEmailAttempt(
4608
+ context.env,
4609
+ admission.operationId,
4610
+ recipients,
4611
+ "cancelled",
4612
+ {},
4613
+ sendDeadline.remainingMs()
4614
+ ).catch(() => {
4615
+ });
4131
4616
  return context.json(
4132
4617
  {
4133
4618
  ok: false,
4134
- error: { code: "UPSTREAM_ERROR", message: "could not lease the domain credential" }
4619
+ operationId: admission.operationId,
4620
+ error: { code: "UPSTREAM_ERROR", message: "email domain changed during admission" }
4135
4621
  },
4136
- 502
4622
+ 503
4137
4623
  );
4138
4624
  }
4139
- await Promise.all([
4140
- context.env.ROUTING.put(counterKey, String(monthCurrent + recipients.length), {
4141
- expirationTtl: monthTtlSeconds(now)
4142
- }).catch((e) => {
4143
- console.error(`[email-send] monthly counter put failed for ${projectId}:`, e);
4144
- }),
4145
- context.env.ROUTING.put(burstKey, String(burstCurrent + recipients.length), {
4146
- expirationTtl: 5 * 60
4147
- }).catch((e) => {
4148
- console.error(`[email-send] burst counter put failed for ${projectId}:`, e);
4149
- })
4150
- ]);
4151
- let failure = null;
4152
- let messageId = "";
4153
- let bounced = /* @__PURE__ */ new Set();
4154
- let suppressed = /* @__PURE__ */ new Set();
4155
- let cfRes = null;
4156
4625
  try {
4157
- cfRes = await fetch(
4158
- `https://api.cloudflare.com/client/v4/accounts/${mirror.accountId}/email/sending/send_raw`,
4159
- {
4160
- method: "POST",
4161
- headers: {
4162
- authorization: `Bearer ${leaseBearer(lease)}`,
4163
- "content-type": "application/json"
4164
- },
4165
- body: JSON.stringify({ from, recipients, mime_message: mimeMessage })
4166
- }
4626
+ await startEmailAttempt(
4627
+ context.env,
4628
+ admission.operationId,
4629
+ recipients,
4630
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
4167
4631
  );
4168
4632
  } catch {
4169
- failure = { code: "UPSTREAM_ERROR", message: "email sending request failed" };
4170
- }
4171
- if (cfRes) {
4172
- if (!cfRes.ok) {
4173
- if (cfRes.status === 401 || cfRes.status === 403) {
4174
- dropEmailDomainLease(fromDomain);
4175
- }
4176
- failure = {
4177
- code: cfRes.status === 429 ? "QUOTA_EXCEEDED" : "UPSTREAM_ERROR",
4178
- message: `email sending API returned ${cfRes.status}`
4179
- };
4180
- } else {
4181
- let data = null;
4182
- try {
4183
- data = await cfRes.json();
4184
- } catch {
4185
- data = null;
4186
- }
4187
- const success = typeof data === "object" && data !== null && data.success === true;
4188
- if (!success) {
4189
- failure = { code: "UPSTREAM_ERROR", message: "email sending API refused the message" };
4190
- } else {
4191
- const result = data.result;
4192
- const id = result?.message_id;
4193
- messageId = typeof id === "string" ? id : "";
4194
- bounced = readSendRawAddressList(result?.permanent_bounces);
4195
- suppressed = readSendRawAddressList(result?.suppressed_recipients);
4196
- }
4197
- }
4198
- }
4199
- const deliveries = [];
4200
- for (const r of recipients) {
4201
- if (failure) {
4202
- deliveries.push({ recipient: r, ok: false, error: failure });
4203
- } else if (bounced.has(r)) {
4204
- deliveries.push({
4205
- recipient: r,
4206
- ok: false,
4207
- error: {
4208
- code: "UNVERIFIED_DESTINATION",
4209
- message: `recipient '${r}' permanently bounced`
4210
- }
4211
- });
4212
- } else if (suppressed.has(r)) {
4633
+ const cancelled = await cancelUnstartedRecipients(
4634
+ context.env,
4635
+ admission.operationId,
4636
+ recipients,
4637
+ sendDeadline.remainingMs()
4638
+ );
4639
+ for (const recipient of recipients) {
4213
4640
  deliveries.push({
4214
- recipient: r,
4641
+ recipient,
4215
4642
  ok: false,
4216
- error: {
4217
- code: "UNVERIFIED_DESTINATION",
4218
- message: `recipient '${r}' is on the suppression list`
4219
- }
4643
+ state: cancelled ? "cancelled" : "outcome_unknown",
4644
+ error: cancelled ? {
4645
+ code: "UPSTREAM_ERROR",
4646
+ message: "email attempt did not start; reservation cancelled"
4647
+ } : { code: "OUTCOME_UNKNOWN", message: "could not confirm email attempt status" }
4220
4648
  });
4221
- } else {
4222
- deliveries.push({ recipient: r, ok: true, messageId });
4223
- }
4224
- }
4225
- logSend(projectId, requestId, deliveries);
4226
- if (deliveries.some((d) => !d.ok)) {
4227
- return context.json({ ok: false, deliveries }, 200);
4228
- }
4229
- return context.json({
4230
- ok: true,
4231
- ids: deliveries.map((d) => ({
4232
- recipient: d.recipient,
4233
- messageId: d.messageId
4234
- }))
4235
- });
4236
- }
4237
- __name(sendViaCustomDomain, "sendViaCustomDomain");
4238
- async function recordInboundDelivery(env, projectId, info) {
4239
- const now = /* @__PURE__ */ new Date();
4240
- const counterKey = `email-recv:${projectId}:${monthBucket(now)}`;
4241
- const current = readCounter(await env.ROUTING.get(counterKey));
4242
- await env.ROUTING.put(counterKey, String(current + 1), {
4243
- expirationTtl: monthTtlSeconds(now)
4244
- });
4245
- console.log(
4246
- `[email-recv] ${projectId} ${sanitizeForLog(info.from, 320)} -> ${sanitizeForLog(info.to, 320)} ${info.status} (${info.byteSize}B) @${Date.now()} subject="${sanitizeForLog(info.subject)}"`
4247
- );
4248
- }
4249
- __name(recordInboundDelivery, "recordInboundDelivery");
4250
- var UNVERIFIED_DESTINATION_RE = /^[^\n]{0,200}destination address[^\n]{0,200}not (?:yet )?verified/i;
4251
- function classifyUpstream(e, projectId) {
4252
- const message = e instanceof Error ? e.message : String(e);
4253
- const code = typeof e?.code === "string" ? e.code : "";
4254
- switch (code) {
4255
- case "E_RECIPIENT_NOT_ALLOWED":
4256
- return "UNVERIFIED_DESTINATION";
4257
- case "E_RATE_LIMIT_EXCEEDED":
4258
- case "E_DAILY_LIMIT_EXCEEDED":
4259
- return "QUOTA_EXCEEDED";
4260
- case "E_SENDER_NOT_VERIFIED":
4261
- case "E_SENDER_DOMAIN_NOT_AVAILABLE":
4262
- console.error(
4263
- `[email-send] project ${projectId} send binding refused the platform sender (${code}): ${sanitizeForLog(message)}`
4264
- );
4265
- return "UPSTREAM_ERROR";
4266
- default:
4267
- break;
4268
- }
4269
- if (UNVERIFIED_DESTINATION_RE.test(message)) {
4270
- return "UNVERIFIED_DESTINATION";
4271
- }
4272
- return "UPSTREAM_ERROR";
4273
- }
4274
- __name(classifyUpstream, "classifyUpstream");
4275
- function logSend(projectId, requestId, deliveries) {
4276
- for (const d of deliveries) {
4277
- const status = d.ok ? "sent" : d.error?.code ?? "unknown_error";
4278
- const id = d.messageId ?? "";
4279
- console.log(
4280
- `[email-log] ${projectId} ${requestId} ${sanitizeForLog(d.recipient, 320)} ${status} ${id} @${Date.now()}`
4281
- );
4282
- }
4283
- }
4284
- __name(logSend, "logSend");
4285
- async function readAllowlist(kv2, projectId) {
4286
- const raw2 = await kv2.get(`email-allowlist:${projectId}`, { cacheTtl: 60 });
4287
- if (!raw2) {
4288
- return /* @__PURE__ */ new Set();
4289
- }
4290
- try {
4291
- const arr = JSON.parse(raw2);
4292
- if (!Array.isArray(arr)) {
4293
- return /* @__PURE__ */ new Set();
4294
- }
4295
- const out = /* @__PURE__ */ new Set();
4296
- for (const v of arr) {
4297
- if (typeof v === "string") {
4298
- out.add(v.toLowerCase());
4299
- }
4300
4649
  }
4301
- return out;
4302
- } catch {
4303
- return /* @__PURE__ */ new Set();
4304
- }
4305
- }
4306
- __name(readAllowlist, "readAllowlist");
4307
- async function readEmailDomainMirror(kv2, domain, options) {
4308
- const raw2 = await kv2.get(`email-domain:${domain}`, options);
4309
- if (!raw2) {
4310
- return null;
4650
+ return emailActionResponse(context, admission.operationId, deliveries);
4311
4651
  }
4312
- let parsed;
4652
+ let failure = null;
4653
+ let failureState = "failed";
4654
+ let sendRawResult = null;
4655
+ let providerResponse = null;
4313
4656
  try {
4314
- parsed = JSON.parse(raw2);
4657
+ providerResponse = await requestSendRaw(
4658
+ {
4659
+ accountId: lease.accountId,
4660
+ bearer: leaseBearer(lease),
4661
+ from,
4662
+ recipients,
4663
+ mimeMessage
4664
+ },
4665
+ sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
4666
+ );
4315
4667
  } catch {
4316
- return null;
4317
- }
4318
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4319
- return null;
4320
- }
4321
- const entry = parsed;
4322
- if (typeof entry.projectId !== "string" || entry.projectId === "" || typeof entry.status !== "string" || typeof entry.relaySecretSha256 !== "string") {
4323
- return null;
4324
- }
4325
- return {
4326
- projectId: entry.projectId,
4327
- status: entry.status,
4328
- relaySecretSha256: entry.relaySecretSha256,
4329
- relayWorkerName: typeof entry.relayWorkerName === "string" ? entry.relayWorkerName : null,
4330
- accountId: typeof entry.accountId === "string" ? entry.accountId : null,
4331
- zoneId: typeof entry.zoneId === "string" ? entry.zoneId : null,
4332
- mode: typeof entry.mode === "string" ? entry.mode : null
4333
- };
4334
- }
4335
- __name(readEmailDomainMirror, "readEmailDomainMirror");
4336
- function readCounter(raw2) {
4337
- if (!raw2) {
4338
- return 0;
4339
- }
4340
- const n = parseInt(raw2, 10);
4341
- return Number.isFinite(n) && n >= 0 ? n : 0;
4342
- }
4343
- __name(readCounter, "readCounter");
4344
- function base64Decode(b64) {
4345
- const binary = atob(b64);
4346
- const out = new Uint8Array(binary.length);
4347
- for (let i = 0; i < binary.length; i++) {
4348
- out[i] = binary.charCodeAt(i);
4349
- }
4350
- return out;
4351
- }
4352
- __name(base64Decode, "base64Decode");
4353
- function monthBucket(now) {
4354
- return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
4355
- }
4356
- __name(monthBucket, "monthBucket");
4357
- function minuteBucket(now) {
4358
- return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(
4359
- now.getUTCDate()
4360
- ).padStart(2, "0")}T${String(now.getUTCHours()).padStart(2, "0")}${String(
4361
- now.getUTCMinutes()
4362
- ).padStart(2, "0")}`;
4363
- }
4364
- __name(minuteBucket, "minuteBucket");
4365
- function monthTtlSeconds(now) {
4366
- const endOfMonth = Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0);
4367
- const seconds = Math.ceil((endOfMonth - now.getTime()) / 1e3) + 60 * 60;
4368
- return Math.max(60, seconds);
4369
- }
4370
- __name(monthTtlSeconds, "monthTtlSeconds");
4371
- async function getBurstLimit(kv2, projectId) {
4372
- const raw2 = await kv2.get(`email-burst-limit:${projectId}`, { cacheTtl: 60 });
4373
- if (raw2) {
4374
- const n = parseInt(raw2, 10);
4375
- if (Number.isFinite(n) && n > 0) {
4376
- return n;
4377
- }
4378
- }
4379
- return 10;
4380
- }
4381
- __name(getBurstLimit, "getBurstLimit");
4382
- async function getMonthlyLimit(kv2, projectId) {
4383
- const raw2 = await kv2.get(`email-limit:${projectId}`, { cacheTtl: 60 });
4384
- if (raw2) {
4385
- const n = parseInt(raw2, 10);
4386
- if (Number.isFinite(n) && n > 0) {
4387
- return n;
4388
- }
4389
- }
4390
- return 200;
4391
- }
4392
- __name(getMonthlyLimit, "getMonthlyLimit");
4393
- var INTERNAL_FROM_LOCAL_PART = "noreply";
4394
- app.post("/internal/send", async (context) => {
4395
- const body = context.get("body");
4396
- const senderDomain = readSenderDomain(context.env);
4397
- if (!senderDomain) {
4398
- return context.json({ ok: false, error: "email sender domain is not configured" }, 502);
4399
- }
4400
- const from = asString(body.from);
4401
- const to = asString(body.to);
4402
- const mimeBase64 = asString(body.mimeBase64);
4403
- if (!from || !to || !mimeBase64) {
4404
- return context.json({ ok: false, error: "invalid body" }, 400);
4405
- }
4406
- if (!/^[\x21-\x7e]+$/.test(from)) {
4407
- return context.json({ ok: false, error: "from not allowed for internal send" }, 403);
4408
- }
4409
- if (from.toLowerCase() !== `${INTERNAL_FROM_LOCAL_PART}@${senderDomain}`) {
4410
- return context.json({ ok: false, error: "from not allowed for internal send" }, 403);
4411
- }
4412
- const sendBinding = context.env.SEND_EMAIL;
4413
- if (typeof sendBinding?.send !== "function") {
4414
- return context.json({ ok: false, error: "email sending is not configured" }, 502);
4415
- }
4416
- let raw2;
4417
- try {
4418
- raw2 = base64Decode(mimeBase64);
4419
- } catch {
4420
- return context.json({ ok: false, error: "invalid base64 mime" }, 400);
4421
- }
4422
- if (raw2.byteLength > MAX_MIME_BYTES) {
4423
- return context.json({ ok: false, error: "too large" }, 413);
4424
- }
4425
- try {
4426
- const msg = new EmailMessage(from, to, bytesToStream(raw2));
4427
- await sendBinding.send(msg);
4428
- return context.json({ ok: true });
4429
- } catch (err) {
4430
- const message = err instanceof Error ? err.message : String(err);
4431
- console.error(
4432
- `[email-internal] send failed from=${sanitizeForLog(from, 320)} to=${sanitizeForLog(to, 320)}: ${sanitizeForLog(message, 320)}`
4433
- );
4434
- return context.json({ ok: false, error: "upstream error" }, 502);
4435
- }
4436
- });
4437
-
4438
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-router.ts
4439
- var CONTROL_CHARS_RE2 = new RegExp("[\\u0000-\\u001f\\u007f]+", "g");
4440
- function sanitize(value, maxLen) {
4441
- return value.slice(0, maxLen).replace(CONTROL_CHARS_RE2, " ");
4442
- }
4443
- __name(sanitize, "sanitize");
4444
- function describeUntrusted(value, maxLen) {
4445
- switch (typeof value) {
4446
- case "string":
4447
- return sanitize(value, maxLen);
4448
- case "number":
4449
- case "boolean":
4450
- case "undefined":
4451
- return String(value);
4452
- case "object":
4453
- return value === null ? "null" : sanitize(Object.prototype.toString.call(value), 64);
4454
- default:
4455
- return `[${typeof value}]`;
4456
- }
4457
- }
4458
- __name(describeUntrusted, "describeUntrusted");
4459
- var MAX_INBOUND_BYTES = 10 * 1024 * 1024;
4460
- var MAX_INBOUND_HEADERS = 1024;
4461
- var MAX_REJECT_REASON_LENGTH = 1e3;
4462
- var MAX_ACTIONS = 100;
4463
- var MAX_ACTION_ADDRESS_LENGTH = 320;
4464
- var MAX_FORWARD_HEADERS = 32;
4465
- var MAX_FORWARD_HEADER_LENGTH = 1024;
4466
- var MAX_REPLY_BYTES = MAX_MIME_BYTES;
4467
- var MAX_REPLY_BYTES_TOTAL = 2 * MAX_REPLY_BYTES;
4468
- var RPC_METHOD_MISSING = /does not implement the method|receiver does not.*method/i;
4469
- var InboundTooLargeError = class extends Error {
4470
- constructor(size) {
4471
- super(`inbound message exceeds ${MAX_INBOUND_BYTES} bytes (got ${size})`);
4472
- this.size = size;
4473
- this.name = "InboundTooLargeError";
4474
- }
4475
- size;
4476
- static {
4477
- __name(this, "InboundTooLargeError");
4478
- }
4479
- };
4480
- var InboundTooManyHeadersError = class extends Error {
4481
- static {
4482
- __name(this, "InboundTooManyHeadersError");
4483
- }
4484
- constructor() {
4485
- super(`inbound message carries more than ${MAX_INBOUND_HEADERS} headers`);
4486
- this.name = "InboundTooManyHeadersError";
4487
- }
4488
- };
4489
- function isValidEmailAddress(s) {
4490
- return typeof s === "string" && s.length <= MAX_ACTION_ADDRESS_LENGTH && (0, import_email_validator.validate)(s);
4491
- }
4492
- __name(isValidEmailAddress, "isValidEmailAddress");
4493
- function logAddress(value) {
4494
- return describeUntrusted(value, MAX_ACTION_ADDRESS_LENGTH);
4495
- }
4496
- __name(logAddress, "logAddress");
4497
- function forwardHeadersWithinBounds(headers) {
4498
- if (typeof headers !== "object" || headers === null) {
4499
- console.error("[email-router] action list dropped: forward headers is not a plain object");
4500
- return false;
4501
- }
4502
- const proto = Object.getPrototypeOf(headers);
4503
- if (proto !== Object.prototype && proto !== null) {
4504
- console.error("[email-router] action list dropped: forward headers is not a plain object");
4505
- return false;
4506
- }
4507
- const names = Object.keys(headers);
4508
- if (names.length > MAX_FORWARD_HEADERS) {
4509
- console.error(
4510
- `[email-router] action list dropped: forward has ${names.length} headers (max ${MAX_FORWARD_HEADERS})`
4511
- );
4512
- return false;
4513
- }
4514
- for (const name of names) {
4515
- const value = headers[name];
4516
- if (name.length > MAX_FORWARD_HEADER_LENGTH) {
4517
- console.error(
4518
- `[email-router] action list dropped: forward header name exceeds ${MAX_FORWARD_HEADER_LENGTH} characters`
4519
- );
4520
- return false;
4521
- }
4522
- if (typeof value !== "string") {
4523
- console.error(
4524
- `[email-router] action list dropped: forward header "${sanitize(name, 64)}" value is not a string`
4525
- );
4526
- return false;
4527
- }
4528
- if (value.length > MAX_FORWARD_HEADER_LENGTH) {
4529
- console.error(
4530
- `[email-router] action list dropped: forward header "${sanitize(name, 64)}" value exceeds ${MAX_FORWARD_HEADER_LENGTH} characters`
4531
- );
4532
- return false;
4533
- }
4534
- }
4535
- return true;
4536
- }
4537
- __name(forwardHeadersWithinBounds, "forwardHeadersWithinBounds");
4538
- async function isPermittedReplySender(from, pin) {
4539
- if (!/^[\x21-\x7e]+$/.test(from)) {
4540
- return false;
4541
- }
4542
- const lower = from.toLowerCase();
4543
- const at = lower.indexOf("@");
4544
- if (at <= 0) {
4545
- return false;
4546
- }
4547
- const fromDomain = lower.slice(at + 1);
4548
- if (fromDomain === pin.domain) {
4549
- const localPrefix = lower.slice(0, at).split("+")[0] ?? "";
4550
- if (localPrefix.length > 0 && localPrefix === pin.slug) {
4551
- return true;
4552
- }
4553
- }
4554
- return isRegisteredDomainSender(fromDomain, pin);
4555
- }
4556
- __name(isPermittedReplySender, "isPermittedReplySender");
4557
- async function isRegisteredDomainSender(fromDomain, pin) {
4558
- if (!pin.projectId) {
4559
- return false;
4560
- }
4561
- const cached = pin.registeredDomains.get(fromDomain);
4562
- if (cached !== void 0) {
4563
- return cached;
4564
- }
4565
- const mirror = await readEmailDomainMirror(pin.kv, fromDomain, { cacheTtl: 60 }).catch(
4566
- () => null
4567
- );
4568
- const registered = mirror !== null && mirror.projectId === pin.projectId && mirror.status === "active";
4569
- pin.registeredDomains.set(fromDomain, registered);
4570
- return registered;
4571
- }
4572
- __name(isRegisteredDomainSender, "isRegisteredDomainSender");
4573
- function readCounter2(raw2) {
4574
- if (!raw2) {
4575
- return 0;
4576
- }
4577
- const n = parseInt(raw2, 10);
4578
- return Number.isFinite(n) && n >= 0 ? n : 0;
4579
- }
4580
- __name(readCounter2, "readCounter");
4581
- function monthBucket2(now) {
4582
- return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
4583
- }
4584
- __name(monthBucket2, "monthBucket");
4585
- function minuteBucket2(now) {
4586
- return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(
4587
- now.getUTCDate()
4588
- ).padStart(2, "0")}T${String(now.getUTCHours()).padStart(2, "0")}${String(
4589
- now.getUTCMinutes()
4590
- ).padStart(2, "0")}`;
4591
- }
4592
- __name(minuteBucket2, "minuteBucket");
4593
- function monthTtlSeconds2(now) {
4594
- const endOfMonth = Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0);
4595
- const seconds = Math.ceil((endOfMonth - now.getTime()) / 1e3) + 60 * 60;
4596
- return Math.max(60, seconds);
4597
- }
4598
- __name(monthTtlSeconds2, "monthTtlSeconds");
4599
- async function readAllowlist2(kv2, projectId) {
4600
- const raw2 = await kv2.get(`email-allowlist:${projectId}`, { cacheTtl: 60 });
4601
- if (!raw2) {
4602
- return /* @__PURE__ */ new Set();
4603
- }
4604
- try {
4605
- const arr = JSON.parse(raw2);
4606
- if (!Array.isArray(arr)) {
4607
- return /* @__PURE__ */ new Set();
4608
- }
4609
- const out = /* @__PURE__ */ new Set();
4610
- for (const v of arr) {
4611
- if (typeof v === "string") {
4612
- out.add(v.toLowerCase());
4613
- }
4614
- }
4615
- return out;
4616
- } catch {
4617
- return /* @__PURE__ */ new Set();
4618
- }
4619
- }
4620
- __name(readAllowlist2, "readAllowlist");
4621
- async function getBurstLimit2(kv2, projectId) {
4622
- const raw2 = await kv2.get(`email-burst-limit:${projectId}`, { cacheTtl: 60 });
4623
- if (raw2) {
4624
- const n = parseInt(raw2, 10);
4625
- if (Number.isFinite(n) && n > 0) {
4626
- return n;
4627
- }
4628
- }
4629
- return 10;
4630
- }
4631
- __name(getBurstLimit2, "getBurstLimit");
4632
- async function getMonthlyLimit2(kv2, projectId) {
4633
- const raw2 = await kv2.get(`email-limit:${projectId}`, { cacheTtl: 60 });
4634
- if (raw2) {
4635
- const n = parseInt(raw2, 10);
4636
- if (Number.isFinite(n) && n > 0) {
4637
- return n;
4638
- }
4639
- }
4640
- return 200;
4641
- }
4642
- __name(getMonthlyLimit2, "getMonthlyLimit");
4643
- async function routeInbound(message, env, ctx) {
4644
- const label = readSenderDomain(env) ?? "void email";
4645
- const to = String(message.to ?? "").toLowerCase();
4646
- const local = to.split("@")[0] ?? "";
4647
- const plus = local.indexOf("+");
4648
- const slug = plus === -1 ? local : local.slice(0, plus);
4649
- const atIdx = to.indexOf("@");
4650
- const receivedDomain = atIdx === -1 ? "" : to.slice(atIdx + 1);
4651
- if (!slug) {
4652
- message.setReject(`${label}: missing project slug in recipient`);
4653
- return;
4654
- }
4655
- if (isReservedEmailSlug(slug)) {
4656
- message.setReject(`${label}: "${sanitize(slug, 64)}" is a reserved platform address`);
4657
- return;
4658
- }
4659
- const routingJson = await env.ROUTING.get(slug);
4660
- if (!routingJson) {
4661
- message.setReject(`${label}: no project found for slug "${sanitize(slug, 64)}"`);
4662
- return;
4663
- }
4664
- let parsedRouting;
4665
- try {
4666
- parsedRouting = JSON.parse(routingJson);
4667
- } catch {
4668
- message.setReject(`${label}: routing data corrupt`);
4669
- return;
4670
- }
4671
- if (typeof parsedRouting !== "object" || parsedRouting === null) {
4672
- message.setReject(`${label}: routing data corrupt`);
4673
- return;
4674
- }
4675
- const routing = parsedRouting;
4676
- if (routing.suspended) {
4677
- const reason = typeof routing.suspended === "string" ? routing.suspended : "suspended";
4678
- message.setReject(
4679
- `${label}: project "${sanitize(slug, 64)}" is suspended (${sanitize(reason, 64)})`
4680
- );
4681
- return;
4682
- }
4683
- const workerName = routing.workerName;
4684
- if (!workerName) {
4685
- message.setReject(`${label}: project "${sanitize(slug, 64)}" has no worker (static deploy)`);
4686
- return;
4687
- }
4688
- if (typeof workerName !== "string") {
4689
- message.setReject(`${label}: routing data corrupt`);
4690
- return;
4691
- }
4692
- if (typeof routing.projectId !== "string" || routing.projectId === "") {
4693
- message.setReject(`${label}: routing data corrupt`);
4694
- return;
4695
- }
4696
- if (await env.ROUTING.get(`email-suspended:${routing.projectId}`)) {
4697
- message.setReject(`${label}: project "${sanitize(slug, 64)}" inbound is suspended`);
4698
- return;
4699
- }
4700
- let stub;
4701
- try {
4702
- stub = routing.limits ? env.DISPATCH.get(workerName, {}, { limits: routing.limits }) : env.DISPATCH.get(workerName);
4703
- } catch (e) {
4704
- console.error(`[email-router] DISPATCH.get(${sanitize(workerName, 320)}) failed:`, e);
4705
- message.setReject(`${label}: project worker unavailable`);
4706
- return;
4707
- }
4708
- if (typeof message.rawSize === "number" && message.rawSize > MAX_INBOUND_BYTES) {
4709
- message.setReject(`${label}: message too large (max ${MAX_INBOUND_BYTES} bytes)`);
4710
- return;
4711
- }
4712
- let envelope;
4713
- try {
4714
- envelope = await buildEnvelope(message);
4715
- } catch (e) {
4716
- if (e instanceof InboundTooLargeError) {
4717
- message.setReject(`${label}: message too large (max ${MAX_INBOUND_BYTES} bytes)`);
4718
- return;
4719
- }
4720
- if (e instanceof InboundTooManyHeadersError) {
4721
- message.setReject(`${label}: too many headers (max ${MAX_INBOUND_HEADERS})`);
4722
- return;
4723
- }
4724
- console.error(`[email-router] failed to read message body for ${sanitize(slug, 64)}:`, e);
4725
- message.setReject(`${label}: could not read inbound message body`);
4726
- return;
4727
- }
4728
- let gate;
4729
- try {
4730
- gate = await loadOutboundGate(env, routing.projectId);
4731
- } catch (e) {
4732
- console.error(
4733
- `[email-router] failed to load outbound gate for ${sanitize(routing.projectId ?? "", 64)}:`,
4734
- e
4735
- );
4736
- gate = {
4737
- projectId: routing.projectId,
4738
- skipAllowlist: false,
4739
- allowlist: /* @__PURE__ */ new Set(),
4740
- burstKey: "",
4741
- burstLimit: 0,
4742
- burstCurrent: 0,
4743
- monthKey: "",
4744
- monthLimit: 0,
4745
- monthCurrent: 0,
4746
- quotaExhausted: true
4747
- };
4748
- }
4749
- let actions;
4750
- try {
4751
- const result = await stub.email(envelope);
4752
- if (result === void 0 || result === null) {
4753
- actions = [];
4754
- } else if (Array.isArray(result)) {
4755
- actions = result;
4756
- } else {
4757
- console.warn(
4758
- `[email-router] ${sanitize(workerName, 320)} email handler returned non-array; treating as no-op`
4759
- );
4760
- actions = [];
4761
- }
4762
- } catch (e) {
4763
- const msg = e instanceof Error ? e.message.slice(0, 320) : describeUntrusted(e, 320);
4764
- if (RPC_METHOD_MISSING.test(msg)) {
4765
- console.error(
4766
- `[email-router] ${sanitize(workerName, 320)} has no .email() (needs redeploy):`,
4767
- sanitize(msg, 320)
4768
- );
4769
- message.setReject(`${label}: project has no email handler`);
4770
- ctx.waitUntil(
4771
- logRecv(env, routing.projectId, message, [{ type: "reject", reason: "no-handler" }])
4772
- );
4773
- return;
4774
- }
4775
- console.error(`[email-router] dispatch to ${sanitize(workerName, 320)} failed:`, msg);
4776
- message.setReject(`${label}: project handler error`);
4777
- ctx.waitUntil(
4778
- logRecv(env, routing.projectId, message, [{ type: "reject", reason: "dispatch-error" }])
4779
- );
4780
- return;
4781
- }
4782
- await enactActions(env, message, actions, gate, {
4783
- slug,
4784
- domain: receivedDomain,
4785
- projectId: routing.projectId,
4786
- kv: env.ROUTING,
4787
- registeredDomains: /* @__PURE__ */ new Map()
4788
- });
4789
- ctx.waitUntil(logRecv(env, routing.projectId, message, actions));
4790
- }
4791
- __name(routeInbound, "routeInbound");
4792
- async function routeRelayed(env, ctx, input) {
4793
- const { projectId, domain, envelope } = input;
4794
- const slug = await env.ROUTING.get(`email-slug:${projectId}`);
4795
- if (!slug) {
4796
- console.error(`[email-relay] no slug mirror for project ${sanitize(projectId, 64)}`);
4797
- return { reject: true };
4798
- }
4799
- const routingJson = await env.ROUTING.get(slug);
4800
- if (!routingJson) {
4801
- console.error(
4802
- `[email-relay] no routing entry for slug ${sanitize(slug, 64)} (project ${sanitize(projectId, 64)})`
4803
- );
4804
- return { reject: true };
4805
- }
4806
- let parsedRouting;
4807
- try {
4808
- parsedRouting = JSON.parse(routingJson);
4809
- } catch {
4810
- console.error(`[email-relay] routing data corrupt for slug ${sanitize(slug, 64)}`);
4811
- return { reject: true };
4812
- }
4813
- if (typeof parsedRouting !== "object" || parsedRouting === null) {
4814
- console.error(`[email-relay] routing data corrupt for slug ${sanitize(slug, 64)}`);
4815
- return { reject: true };
4816
- }
4817
- const routing = parsedRouting;
4818
- if (routing.projectId !== projectId) {
4819
- console.error(
4820
- `[email-relay] routing blob names another project for slug ${sanitize(slug, 64)}`
4821
- );
4822
- return { reject: true };
4823
- }
4824
- if (routing.suspended) {
4825
- console.error(`[email-relay] project ${sanitize(projectId, 64)} is suspended`);
4826
- return { reject: true };
4827
- }
4828
- const workerName = routing.workerName;
4829
- if (!workerName || typeof workerName !== "string") {
4830
- console.error(`[email-relay] project ${sanitize(projectId, 64)} has no worker (static deploy)`);
4831
- return { reject: true };
4832
- }
4833
- if (await env.ROUTING.get(`email-suspended:${projectId}`)) {
4834
- console.error(`[email-relay] project ${sanitize(projectId, 64)} inbound is suspended`);
4835
- return { reject: true };
4836
- }
4837
- let stub;
4838
- try {
4839
- stub = routing.limits ? env.DISPATCH.get(workerName, {}, { limits: routing.limits }) : env.DISPATCH.get(workerName);
4840
- } catch (e) {
4841
- console.error(`[email-relay] DISPATCH.get(${sanitize(workerName, 320)}) failed:`, e);
4842
- return { reject: true };
4843
- }
4844
- const relayed = createRelayedMessage(env, envelope, { domain, projectId });
4845
- let gate;
4846
- try {
4847
- gate = await loadOutboundGate(env, projectId, { skipAllowlist: true });
4848
- } catch (e) {
4849
- console.error(`[email-relay] failed to load outbound gate for ${sanitize(projectId, 64)}:`, e);
4850
- gate = {
4851
- projectId,
4852
- skipAllowlist: true,
4853
- allowlist: /* @__PURE__ */ new Set(),
4854
- burstKey: "",
4855
- burstLimit: 0,
4856
- burstCurrent: 0,
4857
- monthKey: "",
4858
- monthLimit: 0,
4859
- monthCurrent: 0,
4860
- quotaExhausted: true
4861
- };
4862
- }
4863
- let actions;
4864
- try {
4865
- const result = await stub.email(envelope);
4866
- if (result === void 0 || result === null) {
4867
- actions = [];
4868
- } else if (Array.isArray(result)) {
4869
- actions = result;
4870
- } else {
4871
- console.warn(
4872
- `[email-relay] ${sanitize(workerName, 320)} email handler returned non-array; treating as no-op`
4873
- );
4874
- actions = [];
4875
- }
4876
- } catch (e) {
4877
- const msg = e instanceof Error ? e.message.slice(0, 320) : describeUntrusted(e, 320);
4878
- if (RPC_METHOD_MISSING.test(msg)) {
4879
- console.error(
4880
- `[email-relay] ${sanitize(workerName, 320)} has no .email() (needs redeploy):`,
4881
- sanitize(msg, 320)
4882
- );
4883
- ctx.waitUntil(
4884
- logRecv(env, projectId, relayed.message, [{ type: "reject", reason: "no-handler" }])
4885
- );
4886
- return { reject: true };
4887
- }
4888
- console.error(`[email-relay] dispatch to ${sanitize(workerName, 320)} failed:`, msg);
4889
- ctx.waitUntil(
4890
- logRecv(env, projectId, relayed.message, [{ type: "reject", reason: "dispatch-error" }])
4891
- );
4892
- return { reject: true };
4893
- }
4894
- await enactActions(env, relayed.message, actions, gate, {
4895
- slug: slug.toLowerCase(),
4896
- domain,
4897
- projectId,
4898
- kv: env.ROUTING,
4899
- registeredDomains: /* @__PURE__ */ new Map()
4900
- });
4901
- ctx.waitUntil(logRecv(env, projectId, relayed.message, actions));
4902
- return { reject: relayed.rejectedReason() !== null };
4903
- }
4904
- __name(routeRelayed, "routeRelayed");
4905
- function createRelayedMessage(env, envelope, relay) {
4906
- let reason = null;
4907
- async function sendRaw(from, to, raw2) {
4908
- const senderDomain = from.slice(from.indexOf("@") + 1).toLowerCase();
4909
- const mirror = await readEmailDomainMirror(env.ROUTING, senderDomain);
4910
- if (!mirror || mirror.projectId !== relay.projectId || mirror.status !== "active") {
4911
- throw new Error("email domain registration is no longer active");
4912
- }
4913
- if (!mirror.accountId) {
4914
- throw new Error("email domain registration is incomplete");
4915
- }
4916
- const lease = await leaseEmailDomainCredential(env, senderDomain);
4917
- if (!lease) {
4918
- throw new Error("could not lease the domain credential");
4919
- }
4920
- const mimeMessage = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(raw2);
4921
- let res;
4922
- try {
4923
- res = await fetch(
4924
- `https://api.cloudflare.com/client/v4/accounts/${mirror.accountId}/email/sending/send_raw`,
4925
- {
4926
- method: "POST",
4927
- headers: {
4928
- authorization: `Bearer ${leaseBearer(lease)}`,
4929
- "content-type": "application/json"
4930
- },
4931
- body: JSON.stringify({ from, recipients: [to], mime_message: mimeMessage })
4932
- }
4933
- );
4934
- } catch {
4935
- throw new Error("email sending request failed");
4936
- }
4937
- if (res.status === 401 || res.status === 403) {
4938
- dropEmailDomainLease(senderDomain);
4939
- }
4940
- if (!res.ok) {
4941
- throw new Error(`email sending API returned ${res.status}`);
4942
- }
4943
- let data = null;
4944
- try {
4945
- data = await res.json();
4946
- } catch {
4947
- data = null;
4948
- }
4949
- const success = typeof data === "object" && data !== null && data.success === true;
4950
- if (!success) {
4951
- throw new Error("email sending API refused the message");
4952
- }
4953
- const result = data.result;
4954
- const target = to.toLowerCase();
4955
- if (readSendRawAddressList(result?.permanent_bounces).has(target)) {
4956
- throw new Error("recipient permanently bounced");
4957
- }
4958
- if (readSendRawAddressList(result?.suppressed_recipients).has(target)) {
4959
- throw new Error("recipient is on the suppression list");
4960
- }
4961
- }
4962
- __name(sendRaw, "sendRaw");
4963
- const message = {
4964
- from: envelope.from,
4965
- to: envelope.to,
4966
- rawSize: envelope.rawSize,
4967
- headers: new Headers(envelope.headers),
4968
- raw: envelopeRawStream(envelope.raw),
4969
- setReject(r) {
4970
- reason = r;
4971
- },
4972
- async forward(to, _headers) {
4973
- await sendRaw(envelope.to, to, envelope.raw);
4974
- },
4975
- // The reply channel `enactSanitized` prefers when present. A native
4976
- // reply needs both the CF email event this lane never had and a genuine
4977
- // EmailMessage — whose class exposes no body accessor, so the recorded
4978
- // reply bytes could not cross it anyway; they arrive on the action.
4979
- async __voidRelayReply(from, to, raw2) {
4980
- await sendRaw(from, to, raw2);
4981
- }
4982
- };
4983
- return {
4984
- message,
4985
- rejectedReason: /* @__PURE__ */ __name(() => reason, "rejectedReason")
4986
- };
4987
- }
4988
- __name(createRelayedMessage, "createRelayedMessage");
4989
- function envelopeRawStream(bytes) {
4990
- return new ReadableStream({
4991
- start(controller) {
4992
- controller.enqueue(bytes);
4993
- controller.close();
4994
- }
4995
- });
4996
- }
4997
- __name(envelopeRawStream, "envelopeRawStream");
4998
- async function loadOutboundGate(env, projectId, opts) {
4999
- const skipAllowlist = opts?.skipAllowlist ?? false;
5000
- if (!projectId) {
5001
- return {
5002
- projectId: void 0,
5003
- skipAllowlist,
5004
- allowlist: /* @__PURE__ */ new Set(),
5005
- burstKey: "",
5006
- burstLimit: 0,
5007
- burstCurrent: 0,
5008
- monthKey: "",
5009
- monthLimit: 0,
5010
- monthCurrent: 0,
5011
- quotaExhausted: true
5012
- };
5013
- }
5014
- const allowlist = skipAllowlist ? /* @__PURE__ */ new Set() : await readAllowlist2(env.ROUTING, projectId);
5015
- const now = /* @__PURE__ */ new Date();
5016
- const burstKey = `email-burst:${projectId}:${minuteBucket2(now)}`;
5017
- const monthKey = `email-sent:${projectId}:${monthBucket2(now)}`;
5018
- const [burstLimit, monthLimit, burstRaw, monthRaw] = await Promise.all([
5019
- getBurstLimit2(env.ROUTING, projectId),
5020
- getMonthlyLimit2(env.ROUTING, projectId),
5021
- env.ROUTING.get(burstKey),
5022
- env.ROUTING.get(monthKey)
5023
- ]);
5024
- return {
5025
- projectId,
5026
- skipAllowlist,
5027
- allowlist,
5028
- burstKey,
5029
- burstLimit,
5030
- burstCurrent: readCounter2(burstRaw),
5031
- monthKey,
5032
- monthLimit,
5033
- monthCurrent: readCounter2(monthRaw),
5034
- quotaExhausted: false
5035
- };
5036
- }
5037
- __name(loadOutboundGate, "loadOutboundGate");
5038
- async function commitGateCounters(env, gate) {
5039
- if (!gate.projectId) {
5040
- return;
5041
- }
5042
- const now = /* @__PURE__ */ new Date();
5043
- await Promise.all([
5044
- env.ROUTING.put(gate.monthKey, String(gate.monthCurrent), {
5045
- expirationTtl: monthTtlSeconds2(now)
5046
- }).catch((e) => {
5047
- console.error(
5048
- `[email-router] monthly counter put failed for ${sanitize(gate.projectId ?? "", 64)}:`,
5049
- e
5050
- );
5051
- }),
5052
- env.ROUTING.put(gate.burstKey, String(gate.burstCurrent), {
5053
- expirationTtl: 5 * 60
5054
- }).catch((e) => {
5055
- console.error(
5056
- `[email-router] burst counter put failed for ${sanitize(gate.projectId ?? "", 64)}:`,
5057
- e
5058
- );
5059
- })
5060
- ]);
5061
- }
5062
- __name(commitGateCounters, "commitGateCounters");
5063
- async function logRecv(env, projectId, message, actions) {
5064
- if (!projectId) {
5065
- console.error(
5066
- `[email-router] cannot log inbound delivery: routing entry has no projectId (to=${sanitize(String(message.to ?? ""), 320)})`
5067
- );
5068
- return;
5069
- }
5070
- const status = actions.some((a) => a && typeof a === "object" && a.type === "reject") ? "rejected" : actions.length === 0 ? "accepted" : "handled";
5071
- try {
5072
- await recordInboundDelivery(env, projectId, {
5073
- from: String(message.from ?? ""),
5074
- to: String(message.to ?? ""),
5075
- // sanitize() in email.ts handles control-char stripping before logging;
5076
- // don't pre-truncate here so the full subject is available if logging
5077
- // ever moves to structured storage.
5078
- subject: message.headers.get("Subject") ?? "",
5079
- byteSize: message.rawSize ?? 0,
5080
- status
5081
- });
5082
- } catch (e) {
5083
- console.error("[email-router] log-recv failed:", e);
5084
- }
5085
- }
5086
- __name(logRecv, "logRecv");
5087
- async function buildEnvelope(message) {
5088
- const entries = [];
5089
- for (const entry of message.headers) {
5090
- if (entries.length >= MAX_INBOUND_HEADERS) {
5091
- throw new InboundTooManyHeadersError();
5092
- }
5093
- entries.push(entry);
5094
- }
5095
- const raw2 = await readStreamToBytes(message.raw);
5096
- const headers = Object.fromEntries(entries);
5097
- return {
5098
- from: String(message.from ?? ""),
5099
- to: String(message.to ?? ""),
5100
- headers,
5101
- raw: raw2,
5102
- rawSize: message.rawSize ?? raw2.byteLength
5103
- };
5104
- }
5105
- __name(buildEnvelope, "buildEnvelope");
5106
- async function readStreamToBytes(stream) {
5107
- const reader = stream.getReader();
5108
- const chunks = [];
5109
- let total = 0;
5110
- for (; ; ) {
5111
- const { value, done } = await reader.read();
5112
- if (done) {
5113
- break;
5114
- }
5115
- total += value.byteLength;
5116
- if (total > MAX_INBOUND_BYTES) {
5117
- try {
5118
- await reader.cancel();
5119
- } catch {
5120
- }
5121
- throw new InboundTooLargeError(total);
5122
- }
5123
- chunks.push(value);
5124
- }
5125
- const out = new Uint8Array(total);
5126
- let offset = 0;
5127
- for (const c of chunks) {
5128
- out.set(c, offset);
5129
- offset += c.byteLength;
5130
- }
5131
- return out;
5132
- }
5133
- __name(readStreamToBytes, "readStreamToBytes");
5134
- async function validateActions(actions, replySender) {
5135
- if (actions.length > MAX_ACTIONS) {
5136
- console.error(
5137
- `[email-router] action list dropped: ${actions.length} actions returned (max ${MAX_ACTIONS})`
5138
- );
5139
- return null;
5140
- }
5141
- const out = [];
5142
- let replyBytesUsed = 0;
5143
- for (const action of actions) {
5144
- if (!action || typeof action !== "object") {
5145
- console.error(`[email-router] dropping non-object action: ${describeUntrusted(action, 320)}`);
5146
- continue;
5147
- }
5148
- if (action.type === "reject") {
5149
- const raw2 = typeof action.reason === "string" ? action.reason : "";
5150
- const reason = raw2.length > 0 ? sanitize(raw2, MAX_REJECT_REASON_LENGTH) : "rejected by handler";
5151
- out.push({ type: "reject", reason });
5152
- return out;
5153
- }
5154
- if (action.type === "forward") {
5155
- if (!isValidEmailAddress(action.to)) {
5156
- console.error(
5157
- `[email-router] forward dropped: invalid "to" address ${logAddress(action.to)}`
5158
- );
5159
- continue;
5160
- }
5161
- let headers;
5162
- if (action.headers) {
5163
- if (!forwardHeadersWithinBounds(action.headers)) {
5164
- return null;
5165
- }
5166
- try {
5167
- headers = new Headers(action.headers);
5168
- } catch (e) {
5169
- console.error(
5170
- `[email-router] action list dropped: forward headers invalid:`,
5171
- e instanceof Error ? sanitize(e.message, 320) : sanitize(String(e), 320)
5172
- );
5173
- return null;
5174
- }
5175
- }
5176
- out.push({ type: "forward", to: action.to, headers });
5177
- } else if (action.type === "reply") {
5178
- if (!isValidEmailAddress(action.from) || !isValidEmailAddress(action.to)) {
5179
- console.error(
5180
- `[email-router] reply dropped: invalid address (from=${logAddress(action.from)} to=${logAddress(action.to)})`
5181
- );
5182
- continue;
5183
- }
5184
- if (!await isPermittedReplySender(action.from, replySender)) {
5185
- console.error(
5186
- `[email-router] reply dropped: sender not owned by project "${sanitize(replySender.slug, 64)}" (from=${logAddress(action.from)})`
5187
- );
5188
- continue;
5189
- }
5190
- if (!(action.raw instanceof Uint8Array)) {
5191
- console.error(
5192
- `[email-router] reply dropped: raw is not a Uint8Array (from=${logAddress(action.from)})`
5193
- );
5194
- continue;
5195
- }
5196
- const replyBytes = action.raw.byteLength;
5197
- if (replyBytes > MAX_REPLY_BYTES) {
5198
- console.error(
5199
- `[email-router] reply dropped: raw is ${replyBytes} bytes (max ${MAX_REPLY_BYTES}) (from=${logAddress(action.from)})`
5200
- );
5201
- continue;
5202
- }
5203
- if (replyBytesUsed + replyBytes > MAX_REPLY_BYTES_TOTAL) {
5204
- console.error(
5205
- `[email-router] reply dropped: cumulative reply bytes ${replyBytesUsed + replyBytes} exceed this delivery's budget of ${MAX_REPLY_BYTES_TOTAL} (from=${logAddress(action.from)})`
5206
- );
5207
- continue;
5208
- }
5209
- const mimeFrom = readMimeFromAddrSpec(action.raw);
5210
- if (!mimeFrom.ok || mimeFrom.addrSpec.toLowerCase() !== action.from.toLowerCase()) {
5211
- console.error(
5212
- `[email-router] reply dropped: MIME From is not the envelope sender (from=${logAddress(action.from)}, ${mimeFrom.ok ? "header names another address" : mimeFrom.reason})`
5213
- );
5214
- continue;
5215
- }
5216
- replyBytesUsed += replyBytes;
5217
- out.push({ type: "reply", from: action.from, to: action.to, raw: action.raw });
4668
+ failure = { code: "UPSTREAM_ERROR", message: "email sending request failed" };
4669
+ failureState = "outcome_unknown";
4670
+ }
4671
+ if (providerResponse) {
4672
+ if (!providerResponse.ok) {
4673
+ failure = {
4674
+ code: providerResponse.status === 429 ? "QUOTA_EXCEEDED" : "UPSTREAM_ERROR",
4675
+ message: `email sending API returned ${providerResponse.status}`
4676
+ };
4677
+ failureState = providerResponse.status >= 500 || providerResponse.status === 408 ? "outcome_unknown" : "failed";
5218
4678
  } else {
5219
- console.warn(
5220
- `[email-router] dropping unknown action type: ${describeUntrusted(action.type, 320)}`
5221
- );
4679
+ const data = providerResponse.data;
4680
+ const success = typeof data === "object" && data !== null ? data.success : void 0;
4681
+ if (success === false) {
4682
+ failure = { code: "UPSTREAM_ERROR", message: "email sending API refused the message" };
4683
+ } else if (success !== true) {
4684
+ failure = { code: "OUTCOME_UNKNOWN", message: "email sending API response invalid" };
4685
+ failureState = "outcome_unknown";
4686
+ } else {
4687
+ sendRawResult = parseSendRawResult(data.result);
4688
+ if (!sendRawResult) {
4689
+ failure = { code: "OUTCOME_UNKNOWN", message: "email sending API response invalid" };
4690
+ failureState = "outcome_unknown";
4691
+ }
4692
+ }
5222
4693
  }
5223
4694
  }
5224
- return out;
5225
- }
5226
- __name(validateActions, "validateActions");
5227
- async function enactActions(env, message, actions, gate, replySender) {
5228
- const sanitized = await validateActions(actions, replySender);
5229
- if (sanitized === null) {
5230
- return;
5231
- }
5232
- await enactSanitized(env, message, sanitized, gate);
5233
- }
5234
- __name(enactActions, "enactActions");
5235
- async function enactSanitized(env, message, actions, gate) {
5236
- let counterDirty = false;
5237
- for (const action of actions) {
5238
- if (action.type === "reject") {
5239
- message.setReject(action.reason);
5240
- if (counterDirty) {
5241
- await commitGateCounters(env, gate);
5242
- }
5243
- return;
4695
+ for (const r of recipients) {
4696
+ if (failure) {
4697
+ deliveries.push({ recipient: r, ok: false, state: failureState, error: failure });
4698
+ continue;
5244
4699
  }
5245
- if (action.type === "forward") {
5246
- if (!checkOutboundGate(gate, action.to, "forward")) {
5247
- continue;
5248
- }
5249
- try {
5250
- await message.forward(action.to, action.headers);
5251
- gate.burstCurrent += 1;
5252
- gate.monthCurrent += 1;
5253
- counterDirty = true;
5254
- } catch (e) {
5255
- console.error(
5256
- `[email-router] forward to ${sanitize(action.to, 320)} failed:`,
5257
- e instanceof Error ? sanitize(e.message, 320) : sanitize(String(e), 320)
5258
- );
5259
- }
5260
- } else if (action.type === "reply") {
5261
- if (!checkOutboundGate(gate, action.to, "reply")) {
5262
- continue;
5263
- }
5264
- try {
5265
- const relayReply = message.__voidRelayReply;
5266
- if (relayReply !== void 0) {
5267
- await relayReply(action.from, action.to, action.raw);
5268
- } else {
5269
- const stream = new ReadableStream({
5270
- start(controller) {
5271
- controller.enqueue(action.raw);
5272
- controller.close();
5273
- }
5274
- });
5275
- const reply = new EmailMessage2(action.from, action.to, stream);
5276
- await message.reply(reply);
4700
+ const outcome = classifySendRawRecipient(sendRawResult, r);
4701
+ if (outcome === "permanent_bounce") {
4702
+ deliveries.push({
4703
+ recipient: r,
4704
+ ok: false,
4705
+ state: "failed",
4706
+ error: {
4707
+ code: "UNVERIFIED_DESTINATION",
4708
+ message: `recipient '${r}' permanently bounced`
5277
4709
  }
5278
- gate.burstCurrent += 1;
5279
- gate.monthCurrent += 1;
5280
- counterDirty = true;
5281
- } catch (e) {
5282
- console.error(
5283
- `[email-router] reply ${sanitize(action.from, 320)}->${sanitize(action.to, 320)} failed:`,
5284
- e instanceof Error ? sanitize(e.message, 320) : sanitize(String(e), 320)
5285
- );
4710
+ });
4711
+ } else if (outcome === "suppressed") {
4712
+ deliveries.push({
4713
+ recipient: r,
4714
+ ok: false,
4715
+ state: "failed",
4716
+ error: {
4717
+ code: "UNVERIFIED_DESTINATION",
4718
+ message: `recipient '${r}' is on the suppression list`
4719
+ }
4720
+ });
4721
+ } else if (outcome === "outcome_unknown") {
4722
+ deliveries.push({
4723
+ recipient: r,
4724
+ ok: false,
4725
+ state: "outcome_unknown",
4726
+ error: {
4727
+ code: "OUTCOME_UNKNOWN",
4728
+ message: "email sending API did not classify the recipient"
4729
+ }
4730
+ });
4731
+ } else {
4732
+ deliveries.push({
4733
+ recipient: r,
4734
+ ok: true,
4735
+ state: "provider_accepted",
4736
+ messageId: sendRawResult.messageId
4737
+ });
4738
+ }
4739
+ }
4740
+ for (const delivery of deliveries.filter(
4741
+ (d) => admission.recipients.some((r) => r.recipient === d.recipient && r.state === "reserved")
4742
+ )) {
4743
+ try {
4744
+ await finishEmailAttempt(
4745
+ context.env,
4746
+ admission.operationId,
4747
+ [delivery.recipient],
4748
+ delivery.ok ? "provider_accepted" : delivery.state === "outcome_unknown" ? "outcome_unknown" : "failed",
4749
+ {
4750
+ providerReference: delivery.messageId,
4751
+ errorCode: delivery.ok ? void 0 : delivery.state === "outcome_unknown" ? "OUTCOME_UNKNOWN" : failure?.code ?? delivery.error?.code
4752
+ },
4753
+ sendDeadline.remainingMs()
4754
+ );
4755
+ delivery.state = delivery.ok ? "provider_accepted" : delivery.state === "outcome_unknown" ? "outcome_unknown" : "failed";
4756
+ if (delivery.state === "outcome_unknown" && !delivery.ok) {
4757
+ delivery.error = {
4758
+ code: "OUTCOME_UNKNOWN",
4759
+ message: "provider outcome unknown; inspect email logs before retrying"
4760
+ };
5286
4761
  }
4762
+ } catch {
4763
+ delivery.ok = false;
4764
+ delivery.state = "outcome_unknown";
4765
+ delivery.error = {
4766
+ code: "OUTCOME_UNKNOWN",
4767
+ message: "provider outcome could not be recorded"
4768
+ };
5287
4769
  }
5288
4770
  }
5289
- if (counterDirty) {
5290
- await commitGateCounters(env, gate);
4771
+ logSend(projectId, requestId, deliveries);
4772
+ if (deliveries.some((d) => !d.ok)) {
4773
+ return context.json({ ok: false, operationId: admission.operationId, deliveries }, 200);
5291
4774
  }
4775
+ return context.json({
4776
+ ok: true,
4777
+ operationId: admission.operationId,
4778
+ ids: deliveries.map((d) => ({
4779
+ recipient: d.recipient,
4780
+ messageId: d.messageId,
4781
+ state: "provider_accepted"
4782
+ }))
4783
+ });
5292
4784
  }
5293
- __name(enactSanitized, "enactSanitized");
5294
- function checkOutboundGate(gate, target, kind) {
5295
- if (gate.quotaExhausted) {
5296
- console.error(
5297
- `[email-router] ${kind} dropped: project quota exhausted (target=${sanitize(target, 320)})`
4785
+ __name(sendViaCustomDomain, "sendViaCustomDomain");
4786
+ async function cancelUnstartedRecipients(env, operationId, recipients, deadlineMs) {
4787
+ try {
4788
+ const result = await finishEmailAttempt(
4789
+ env,
4790
+ operationId,
4791
+ recipients,
4792
+ "cancelled",
4793
+ {},
4794
+ deadlineMs
5298
4795
  );
5299
- return false;
5300
- }
5301
- if (!gate.projectId) {
5302
- console.error(
5303
- `[email-router] ${kind} dropped: no projectId on routing entry (target=${sanitize(target, 320)})`
4796
+ return recipients.every(
4797
+ (recipient) => result.recipients.some((row) => row.recipient === recipient && row.state === "cancelled")
5304
4798
  );
4799
+ } catch {
5305
4800
  return false;
5306
4801
  }
5307
- const lowered = target.toLowerCase();
5308
- if (!gate.skipAllowlist && !gate.allowlist.has(lowered)) {
5309
- console.error(
5310
- `[email-router] ${kind} dropped: recipient not in project allowlist (target=${sanitize(target, 320)})`
4802
+ }
4803
+ __name(cancelUnstartedRecipients, "cancelUnstartedRecipients");
4804
+ function emailActionResponse(context, operationId, deliveries) {
4805
+ if (deliveries.length === 0) {
4806
+ return context.json(
4807
+ {
4808
+ ok: false,
4809
+ operationId,
4810
+ error: { code: "OUTCOME_UNKNOWN", message: "email action remains reserved" }
4811
+ },
4812
+ 200
5311
4813
  );
5312
- return false;
5313
- }
5314
- if (gate.burstCurrent + 1 > gate.burstLimit) {
5315
- console.error(`[email-router] ${kind} dropped: burst limit ${gate.burstLimit}/min exceeded`);
5316
- gate.quotaExhausted = true;
5317
- return false;
5318
4814
  }
5319
- if (gate.monthCurrent + 1 > gate.monthLimit) {
5320
- console.error(`[email-router] ${kind} dropped: monthly limit ${gate.monthLimit} exceeded`);
5321
- gate.quotaExhausted = true;
5322
- return false;
4815
+ if (deliveries.some((d) => !d.ok)) {
4816
+ return context.json({ ok: false, operationId, deliveries }, 200);
5323
4817
  }
5324
- return true;
5325
- }
5326
- __name(checkOutboundGate, "checkOutboundGate");
5327
-
5328
- // ../../home/runner/work/void/void/platform/packages/proxy/src/email-relay.ts
5329
- var app2 = new Hono2();
5330
- app2.post("/relay", async (context) => {
5331
- const projectId = context.get("projectId");
5332
- const from = context.req.header("X-Void-Email-From") ?? "";
5333
- const to = context.req.header("X-Void-Email-To") ?? "";
5334
- const domain = to.slice(to.lastIndexOf("@") + 1).toLowerCase();
5335
- const body = context.req.raw.body;
5336
- if (!body) {
5337
- return context.json({ error: "mime body required" }, 400);
5338
- }
5339
- const raw2 = await readMimeBody(body);
5340
- if (raw2 === null) {
5341
- return context.json({ error: "mime too large" }, 413);
5342
- }
5343
- const headers = parseRelayedHeaders(raw2);
5344
- if (headers === null) {
5345
- return context.json({ error: "too many headers" }, 413);
5346
- }
5347
- const envelope = {
5348
- from,
5349
- to,
5350
- headers,
5351
- raw: raw2,
5352
- rawSize: raw2.byteLength,
5353
- viaDomain: domain
5354
- };
5355
- const pending = [];
5356
- let ctx = {
5357
- waitUntil(p) {
5358
- pending.push(p);
4818
+ return context.json(
4819
+ {
4820
+ ok: true,
4821
+ operationId,
4822
+ ids: deliveries.map((d) => ({
4823
+ recipient: d.recipient,
4824
+ messageId: d.messageId ?? "",
4825
+ state: d.state
4826
+ }))
5359
4827
  },
5360
- passThroughOnException() {
5361
- }
5362
- };
5363
- try {
5364
- ctx = context.executionCtx;
5365
- } catch {
5366
- }
5367
- const outcome = await routeRelayed(context.env, ctx, { projectId, domain, envelope });
5368
- if (pending.length > 0) {
5369
- await Promise.allSettled(pending);
5370
- }
5371
- if (outcome.reject) {
5372
- return context.json({ error: "message rejected" }, 403);
5373
- }
5374
- return context.json({ ok: true });
5375
- });
5376
- async function readMimeBody(stream) {
5377
- const reader = stream.getReader();
5378
- const chunks = [];
5379
- let total = 0;
5380
- for (; ; ) {
5381
- const { value, done } = await reader.read();
5382
- if (done) {
5383
- break;
5384
- }
5385
- total += value.byteLength;
5386
- if (total > MAX_MIME_BYTES) {
5387
- try {
5388
- await reader.cancel();
5389
- } catch {
5390
- }
5391
- return null;
5392
- }
5393
- chunks.push(value);
4828
+ 200
4829
+ );
4830
+ }
4831
+ __name(emailActionResponse, "emailActionResponse");
4832
+ function logSend(projectId, requestId, deliveries) {
4833
+ for (const d of deliveries) {
4834
+ const status = d.ok ? "sent" : d.error?.code ?? "unknown_error";
4835
+ const id = d.messageId ?? "";
4836
+ console.log(
4837
+ `[email-log] ${projectId} ${requestId} ${sanitizeForLog(d.recipient, 320)} ${status} ${id} @${Date.now()}`
4838
+ );
5394
4839
  }
5395
- const out = new Uint8Array(total);
5396
- let offset = 0;
5397
- for (const c of chunks) {
5398
- out.set(c, offset);
5399
- offset += c.byteLength;
4840
+ }
4841
+ __name(logSend, "logSend");
4842
+ function base64Decode(b64) {
4843
+ const binary = atob(b64);
4844
+ const out = new Uint8Array(binary.length);
4845
+ for (let i = 0; i < binary.length; i++) {
4846
+ out[i] = binary.charCodeAt(i);
5400
4847
  }
5401
4848
  return out;
5402
4849
  }
5403
- __name(readMimeBody, "readMimeBody");
5404
- var LF2 = 10;
5405
- var CR2 = 13;
5406
- var HEADER_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/;
5407
- var MAX_INBOUND_HEADER_LINES = 4096;
5408
- function parseRelayedHeaders(raw2) {
5409
- const block = new TextDecoder().decode(raw2.subarray(0, headerBlockEnd(raw2)));
5410
- const folded = /* @__PURE__ */ new Map();
5411
- let totalPieces = 0;
5412
- let current = null;
5413
- let pos = 0;
5414
- while (pos < block.length) {
5415
- let nl = block.indexOf("\n", pos);
5416
- if (nl === -1) {
5417
- nl = block.length;
5418
- }
5419
- let line = block.slice(pos, nl);
5420
- pos = nl + 1;
5421
- if (line.endsWith("\r")) {
5422
- line = line.slice(0, -1);
5423
- }
5424
- if (line === "") {
5425
- break;
5426
- }
5427
- if (line.startsWith(" ") || line.startsWith(" ")) {
5428
- if (current !== null && headerValueSafe(line)) {
5429
- totalPieces += 1;
5430
- if (totalPieces > MAX_INBOUND_HEADER_LINES) {
5431
- return null;
5432
- }
5433
- const pieces2 = folded.get(current) ?? [];
5434
- pieces2.push(` ${line.trim()}`);
5435
- folded.set(current, pieces2);
5436
- }
5437
- continue;
5438
- }
5439
- const colon = line.indexOf(":");
5440
- if (colon === -1) {
5441
- current = null;
5442
- continue;
5443
- }
5444
- const name = line.slice(0, colon).trim().toLowerCase();
5445
- const value = line.slice(colon + 1).trim();
5446
- if (!HEADER_NAME_RE.test(name) || !headerValueSafe(value)) {
5447
- current = null;
5448
- continue;
4850
+ __name(base64Decode, "base64Decode");
4851
+
4852
+ // ../../home/runner/work/void/void/platform/packages/proxy/src/human-stream.ts
4853
+ function guardHumanStream(source, validate, signal) {
4854
+ const reader = source.getReader();
4855
+ let closed = false;
4856
+ let timer;
4857
+ let controller;
4858
+ const cleanup = /* @__PURE__ */ __name(() => {
4859
+ closed = true;
4860
+ if (timer !== void 0) {
4861
+ clearTimeout(timer);
4862
+ }
4863
+ signal?.removeEventListener("abort", abort);
4864
+ }, "cleanup");
4865
+ const stop = /* @__PURE__ */ __name(() => {
4866
+ if (closed) {
4867
+ return;
5449
4868
  }
5450
- const pieces = folded.get(name);
5451
- totalPieces += 1;
5452
- if (totalPieces > MAX_INBOUND_HEADER_LINES) {
5453
- return null;
4869
+ cleanup();
4870
+ controller.error(new Error("Authentication expired or was revoked. Sign in again."));
4871
+ void reader.cancel().catch(() => {
4872
+ });
4873
+ }, "stop");
4874
+ const abort = /* @__PURE__ */ __name(() => stop(), "abort");
4875
+ const check = /* @__PURE__ */ __name(async () => {
4876
+ if (closed) {
4877
+ return false;
5454
4878
  }
5455
- if (pieces !== void 0) {
5456
- pieces.push(`, ${value}`);
5457
- } else {
5458
- if (folded.size >= MAX_INBOUND_HEADERS) {
5459
- return null;
4879
+ try {
4880
+ if (await validate()) {
4881
+ return !closed;
5460
4882
  }
5461
- folded.set(name, [value]);
4883
+ } catch {
5462
4884
  }
5463
- current = name;
5464
- }
5465
- return Object.fromEntries(
5466
- Array.from(folded, ([name, pieces]) => [name, pieces.join("")])
4885
+ stop();
4886
+ return false;
4887
+ }, "check");
4888
+ const schedule = /* @__PURE__ */ __name(() => {
4889
+ timer = setTimeout(async () => {
4890
+ if (await check()) {
4891
+ schedule();
4892
+ }
4893
+ }, 15e3);
4894
+ }, "schedule");
4895
+ return new ReadableStream(
4896
+ {
4897
+ start(target) {
4898
+ controller = target;
4899
+ signal?.addEventListener("abort", abort, { once: true });
4900
+ if (signal?.aborted) {
4901
+ stop();
4902
+ } else {
4903
+ schedule();
4904
+ }
4905
+ },
4906
+ async pull(target) {
4907
+ try {
4908
+ const chunk = await reader.read();
4909
+ if (closed) {
4910
+ return;
4911
+ }
4912
+ if (chunk.done) {
4913
+ cleanup();
4914
+ target.close();
4915
+ } else if (await check()) {
4916
+ target.enqueue(chunk.value);
4917
+ }
4918
+ } catch (error) {
4919
+ if (!closed) {
4920
+ cleanup();
4921
+ target.error(error);
4922
+ void reader.cancel().catch(() => {
4923
+ });
4924
+ }
4925
+ }
4926
+ },
4927
+ async cancel(reason) {
4928
+ cleanup();
4929
+ await reader.cancel(reason);
4930
+ }
4931
+ },
4932
+ { highWaterMark: 0 }
5467
4933
  );
5468
4934
  }
5469
- __name(parseRelayedHeaders, "parseRelayedHeaders");
5470
- function headerBlockEnd(raw2) {
5471
- let lineStart = 0;
5472
- while (lineStart < raw2.length) {
5473
- const lf = raw2.indexOf(LF2, lineStart);
5474
- const lineEnd = lf === -1 ? raw2.length : lf;
5475
- let contentEnd = lineEnd;
5476
- if (contentEnd > lineStart && raw2[contentEnd - 1] === CR2) {
5477
- contentEnd--;
5478
- }
5479
- if (contentEnd === lineStart) {
5480
- return lineStart;
5481
- }
5482
- lineStart = lf === -1 ? raw2.length : lf + 1;
5483
- }
5484
- return raw2.length;
5485
- }
5486
- __name(headerBlockEnd, "headerBlockEnd");
5487
- function headerValueSafe(value) {
5488
- for (let i = 0; i < value.length; i++) {
5489
- const code = value.charCodeAt(i);
5490
- if (code === 9) {
5491
- continue;
5492
- }
5493
- if (code < 32 || code === 127 || code > 255) {
5494
- return false;
5495
- }
5496
- }
5497
- return true;
5498
- }
5499
- __name(headerValueSafe, "headerValueSafe");
4935
+ __name(guardHumanStream, "guardHumanStream");
5500
4936
 
5501
4937
  // ../../home/runner/work/void/void/platform/packages/proxy/src/kv.ts
5502
- init_performance2();
5503
4938
  var CF_API2 = "https://api.cloudflare.com/client/v4";
5504
4939
  var kv = new Hono2();
5505
4940
  kv.post("/get", async (context) => {
@@ -5681,12 +5116,10 @@ kv.post("/list", async (context) => {
5681
5116
  });
5682
5117
 
5683
5118
  // ../../home/runner/work/void/void/platform/packages/proxy/src/platform-lifecycle.ts
5684
- init_performance2();
5685
5119
  var PLATFORM_DISABLED_KEY = "__void/platform/disabled/v1";
5686
5120
  var PLATFORM_DISABLED_VALUE = "disabled";
5687
5121
 
5688
5122
  // ../../home/runner/work/void/void/platform/packages/proxy/src/r2.ts
5689
- init_performance2();
5690
5123
  var CF_API3 = "https://api.cloudflare.com/client/v4";
5691
5124
  var r2 = new Hono2();
5692
5125
  function cfHeaders(token) {
@@ -5948,9 +5381,8 @@ r2.post("/head", async (context) => {
5948
5381
  });
5949
5382
 
5950
5383
  // ../../home/runner/work/void/void/platform/packages/proxy/src/revalidate.ts
5951
- init_performance2();
5952
- var app3 = new Hono2();
5953
- app3.post("/", async (context) => {
5384
+ var app2 = new Hono2();
5385
+ app2.post("/", async (context) => {
5954
5386
  const projectId = context.get("projectId");
5955
5387
  const body = context.get("body");
5956
5388
  const response = await context.env.API.fetch(`https://api/internal/revalidate/${projectId}`, {
@@ -5971,8 +5403,8 @@ app3.post("/", async (context) => {
5971
5403
  });
5972
5404
 
5973
5405
  // ../../home/runner/work/void/void/platform/packages/proxy/src/index.ts
5974
- var app4 = new Hono2();
5975
- app4.use("/*", async (context, next) => {
5406
+ var app3 = new Hono2();
5407
+ app3.use("/*", async (context, next) => {
5976
5408
  const healthProbeToken = context.env.PLATFORM_HEALTH_PROBE_TOKEN;
5977
5409
  const installerProbe = healthProbeToken !== void 0 && healthProbeToken !== "" && context.req.path === "/health" && context.req.header("X-Void-Platform-Probe") === healthProbeToken;
5978
5410
  if (!installerProbe && (context.env.PLATFORM_TRAFFIC_STATE === "staging" || context.env.PLATFORM_TRAFFIC_STATE === "sandbox-drain" || context.env.PLATFORM_INSTALLATION_ID && await context.env.ROUTING.get(PLATFORM_DISABLED_KEY) === PLATFORM_DISABLED_VALUE)) {
@@ -5980,7 +5412,7 @@ app4.use("/*", async (context, next) => {
5980
5412
  }
5981
5413
  await next();
5982
5414
  });
5983
- app4.use(
5415
+ app3.use(
5984
5416
  "/*",
5985
5417
  cors({
5986
5418
  origin: "*",
@@ -5988,7 +5420,7 @@ app4.use(
5988
5420
  allowHeaders: ["content-type", "authorization"]
5989
5421
  })
5990
5422
  );
5991
- app4.get(
5423
+ app3.get(
5992
5424
  "/health",
5993
5425
  (context) => context.json({
5994
5426
  ok: true,
@@ -6002,66 +5434,64 @@ app4.get(
6002
5434
  }
6003
5435
  })
6004
5436
  );
6005
- app4.all("/*", async (context, next) => {
5437
+ app3.all("/*", async (context, next) => {
6006
5438
  if (context.req.method !== "POST") {
6007
5439
  return context.json({ error: "method not allowed" }, 405);
6008
5440
  }
6009
5441
  await next();
6010
5442
  });
6011
- var PLATFORM_INTERNAL_EMAIL_SENTINEL = "void-platform-internal-email-v1";
6012
- var MAX_PLATFORM_MIME_BASE64_LENGTH = Math.ceil(10 * 1024 * 1024 * 4 / 3) + 4;
6013
- var PLATFORM_TOKEN_MAX_SKEW_SECONDS = 5 * 60;
6014
- var PLATFORM_TOKEN_LENGTH = 43;
6015
- app4.post("/*", async (context, next) => {
6016
- const path = new URL(context.req.url).pathname;
6017
- if (path === "/email/relay") {
6018
- const relayDomain = context.req.header("X-Void-Email-Domain");
6019
- const relayFrom = context.req.header("X-Void-Email-From");
6020
- const relayTo = context.req.header("X-Void-Email-To");
6021
- const relayAuth = context.req.header("Authorization");
6022
- if (!relayDomain || !relayFrom || !relayTo || !relayAuth) {
6023
- return context.json({ error: "relay envelope required" }, 400);
6024
- }
6025
- if (relayDomain.length > 256 || relayFrom.length > 256 || relayTo.length > 256 || relayAuth.length > 256) {
6026
- return context.json({ error: "relay envelope required" }, 400);
6027
- }
6028
- const contentLengthRaw = context.req.header("content-length");
6029
- if (!contentLengthRaw) {
6030
- return context.json({ error: "content-length required" }, 400);
6031
- }
6032
- const contentLength = Number(contentLengthRaw);
6033
- if (!Number.isInteger(contentLength) || contentLength < 0) {
6034
- return context.json({ error: "content-length required" }, 400);
6035
- }
6036
- if (contentLength > MAX_MIME_BYTES) {
6037
- return context.json({ error: "mime too large" }, 413);
6038
- }
6039
- const relaySecret = relayAuth.startsWith("Bearer ") ? relayAuth.slice(7) : null;
6040
- if (!relaySecret) {
6041
- return context.json({ error: "invalid relay credentials" }, 403);
6042
- }
6043
- const recipientDomain = relayTo.slice(relayTo.lastIndexOf("@") + 1).toLowerCase();
6044
- if (recipientDomain.length === 0 || recipientDomain !== relayDomain.toLowerCase()) {
6045
- return context.json({ error: "relay domain mismatch" }, 403);
6046
- }
6047
- const mirror = await readEmailDomainMirror(context.env.ROUTING, recipientDomain);
6048
- if (!mirror) {
6049
- return context.json({ error: "invalid relay credentials" }, 403);
6050
- }
6051
- const presented = await sha256Hex(relaySecret);
6052
- if (!constantTimeEqual(presented, mirror.relaySecretSha256)) {
6053
- return context.json({ error: "invalid relay credentials" }, 403);
6054
- }
6055
- if (mirror.status !== "active") {
6056
- return context.json({ error: "email domain is not active" }, 403);
6057
- }
6058
- context.set("projectId", mirror.projectId);
6059
- await next();
6060
- return;
5443
+ async function hmacSign4(data, secret) {
5444
+ const key = await crypto.subtle.importKey(
5445
+ "raw",
5446
+ new TextEncoder().encode(secret),
5447
+ { name: "HMAC", hash: "SHA-256" },
5448
+ false,
5449
+ ["sign"]
5450
+ );
5451
+ const bytes = new Uint8Array(
5452
+ await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data))
5453
+ );
5454
+ let binary = "";
5455
+ for (const byte of bytes) {
5456
+ binary += String.fromCharCode(byte);
5457
+ }
5458
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
5459
+ }
5460
+ __name(hmacSign4, "hmacSign");
5461
+ app3.post("/email/verify-receipt", async (context) => {
5462
+ let body;
5463
+ try {
5464
+ body = await readEmailRequestJson(context.req.raw);
5465
+ } catch {
5466
+ return context.json({ valid: false }, 400);
5467
+ }
5468
+ if (!body || typeof body !== "object") {
5469
+ return context.json({ valid: false }, 400);
5470
+ }
5471
+ const input = body;
5472
+ if (typeof input.projectId !== "string" || input.projectId.length > 128 || typeof input.deploymentId !== "string" || input.deploymentId.length > 128 || typeof input.recipient !== "string" || input.recipient.length > 320 || typeof input.receipt !== "string" || input.receipt.length > 1536) {
5473
+ return context.json({ valid: false }, 400);
6061
5474
  }
5475
+ const projectToken = await hmacSign4(input.projectId, context.env.JWT_SECRET);
5476
+ if (context.req.header("x-void-internal") !== projectToken) {
5477
+ return context.json({ valid: false }, 403);
5478
+ }
5479
+ if (!context.env.EMAIL_SIGNING_SECRET) {
5480
+ return context.json({ valid: false }, 503);
5481
+ }
5482
+ const claims = await verifyEmailReceipt(input.receipt, context.env.EMAIL_SIGNING_SECRET, {
5483
+ projectId: input.projectId,
5484
+ deploymentId: input.deploymentId,
5485
+ recipient: input.recipient
5486
+ });
5487
+ return context.json({ valid: claims !== null, receiptId: claims?.receiptId });
5488
+ });
5489
+ app3.post("/email/relay", (context) => context.json({ error: "not found" }, 404));
5490
+ app3.post("/*", async (context, next) => {
5491
+ const path = new URL(context.req.url).pathname;
6062
5492
  let body;
6063
5493
  try {
6064
- const json = await context.req.json();
5494
+ const json = path === "/email" || path.startsWith("/email/") ? await readEmailRequestJson(context.req.raw, 8 * 1024 * 1024) : await context.req.json();
6065
5495
  if (!isProxyBody(json)) {
6066
5496
  return context.json({ error: "invalid JSON body" }, 400);
6067
5497
  }
@@ -6070,45 +5500,19 @@ app4.post("/*", async (context, next) => {
6070
5500
  return context.json({ error: "invalid JSON body" }, 400);
6071
5501
  }
6072
5502
  context.set("body", body);
6073
- if (path.startsWith("/email/internal/")) {
6074
- const platformToken = asString(body.platformToken);
6075
- const platformTimestamp = asNumber(body.platformTimestamp);
6076
- if (!platformToken || platformTimestamp === null) {
6077
- return context.json({ error: "platform token required" }, 403);
6078
- }
6079
- if (platformToken.length !== PLATFORM_TOKEN_LENGTH) {
6080
- return context.json({ error: "invalid platform token" }, 403);
6081
- }
6082
- const nowSec = Math.floor(Date.now() / 1e3);
6083
- if (Math.abs(nowSec - platformTimestamp) > PLATFORM_TOKEN_MAX_SKEW_SECONDS) {
6084
- return context.json({ error: "invalid platform token" }, 403);
6085
- }
6086
- const bindingFrom = asString(body.from);
6087
- const bindingTo = asString(body.to);
6088
- const bindingMime = asString(body.mimeBase64);
6089
- if (!bindingFrom || !bindingTo || !bindingMime) {
6090
- return context.json({ error: "invalid platform token" }, 403);
6091
- }
6092
- if (bindingMime.length > MAX_PLATFORM_MIME_BASE64_LENGTH) {
6093
- return context.json({ error: "invalid platform token" }, 403);
6094
- }
6095
- if (bindingFrom.length > MAX_SENDER_LENGTH || bindingTo.length > MAX_RECIPIENT_LENGTH) {
6096
- return context.json({ error: "invalid platform token" }, 403);
6097
- }
6098
- const contentHash = await sha256Hex(`${bindingFrom}|${bindingTo}|${bindingMime}`);
6099
- const signed = `${PLATFORM_INTERNAL_EMAIL_SENTINEL}.${platformTimestamp}.${contentHash}`;
6100
- if (!await verifyHmac(signed, platformToken, context.env.JWT_SECRET)) {
6101
- return context.json({ error: "invalid platform token" }, 403);
6102
- }
6103
- await next();
6104
- return;
6105
- }
5503
+ let revalidateHuman;
6106
5504
  const authHeader = context.req.header("authorization");
6107
5505
  if (authHeader?.startsWith("Bearer ")) {
6108
5506
  const token = authHeader.slice(7);
6109
5507
  const payload = await verifyJwt(token, context.env.JWT_SECRET);
6110
5508
  if (!payload) {
6111
- return context.json({ error: "invalid or expired token" }, 401);
5509
+ return context.json(
5510
+ {
5511
+ error: "invalid or expired token",
5512
+ message: "This proxy credential is invalid or expired. Sign in to the connected platform again for remote development, or restart the build to obtain a fresh prerender token."
5513
+ },
5514
+ 401
5515
+ );
6112
5516
  }
6113
5517
  const projectId = asString(body.projectId);
6114
5518
  if (!projectId) {
@@ -6116,25 +5520,52 @@ app4.post("/*", async (context, next) => {
6116
5520
  }
6117
5521
  if (payload.aud === "prerender") {
6118
5522
  if (!payload.project || payload.project !== projectId) {
6119
- return context.json({ error: "forbidden" }, 403);
5523
+ return context.json(
5524
+ {
5525
+ error: "forbidden",
5526
+ message: "This prerender token does not match the requested project. Restart the build with a prerender token issued for the selected project."
5527
+ },
5528
+ 403
5529
+ );
6120
5530
  }
6121
- if (payload.build && !await verifyPrerenderBuild(
5531
+ const active = await verifyPrerenderAuthority(
6122
5532
  context.env.API,
6123
5533
  context.env.JWT_SECRET,
6124
5534
  projectId,
6125
- payload.build
6126
- )) {
6127
- return context.json({ error: "prerender token build is no longer active" }, 403);
5535
+ payload.sub,
5536
+ {
5537
+ ...payload.build ? { buildId: payload.build } : {},
5538
+ ...payload.credential ? { credentialId: payload.credential } : {}
5539
+ }
5540
+ );
5541
+ if (!active) {
5542
+ return context.json(
5543
+ {
5544
+ error: payload.credential ? "prerender token credential is no longer active" : payload.build ? "prerender token build is no longer active" : "prerender token authority is no longer active",
5545
+ message: payload.credential ? "The deploy credential used for prerendering is no longer authorized. Ask the project owner for an active deploy credential, then restart the build." : payload.build ? "This build is no longer authorized to prerender. Start a new build to obtain a fresh prerender token." : "This prerender token is no longer authorized. Confirm your project access with a project admin or owner, then restart the build to obtain a fresh token."
5546
+ },
5547
+ 403
5548
+ );
6128
5549
  }
6129
5550
  } else {
6130
- const owns = await verifyProjectOwner(
5551
+ const checkOwner = /* @__PURE__ */ __name(() => verifyProjectDeployAccess(
6131
5552
  context.env.API,
6132
5553
  context.env.JWT_SECRET,
6133
5554
  projectId,
6134
- payload.sub
6135
- );
6136
- if (!owns) {
6137
- return context.json({ error: "forbidden" }, 403);
5555
+ payload.sub,
5556
+ token,
5557
+ context.req.header("Cf-Access-Jwt-Assertion"),
5558
+ new URL(context.req.url).origin
5559
+ ), "checkOwner");
5560
+ revalidateHuman = /* @__PURE__ */ __name(async () => payload.exp > Date.now() / 1e3 && await checkOwner(), "revalidateHuman");
5561
+ if (!await revalidateHuman()) {
5562
+ return context.json(
5563
+ {
5564
+ error: "forbidden",
5565
+ message: "Remote access could not be authorized. Check the selected platform and project, then ask a project admin or owner to confirm your access. If your access is correct, retry shortly."
5566
+ },
5567
+ 403
5568
+ );
6138
5569
  }
6139
5570
  }
6140
5571
  context.set("projectId", projectId);
@@ -6150,14 +5581,20 @@ app4.post("/*", async (context, next) => {
6150
5581
  context.set("projectId", projectId);
6151
5582
  }
6152
5583
  await next();
5584
+ if (revalidateHuman && context.res.body && (context.res.headers.get("content-type")?.includes("text/event-stream") || path.startsWith("/ai/") || path === "/r2/get")) {
5585
+ context.res = new Response(
5586
+ guardHumanStream(context.res.body, revalidateHuman, context.req.raw.signal),
5587
+ context.res
5588
+ );
5589
+ }
6153
5590
  });
6154
- app4.post("/ai/*", async (context, next) => {
5591
+ app3.post("/ai/*", async (context, next) => {
6155
5592
  if (await context.env.ROUTING.get(`ai-block:${context.get("projectId")}`, { cacheTtl: 60 })) {
6156
5593
  return context.json({ error: "AI usage limit exceeded" }, 429);
6157
5594
  }
6158
5595
  await next();
6159
5596
  });
6160
- app4.post(
5597
+ app3.post(
6161
5598
  "/sandbox/*",
6162
5599
  (context) => context.json(
6163
5600
  {
@@ -6168,7 +5605,7 @@ app4.post(
6168
5605
  501
6169
5606
  )
6170
5607
  );
6171
- app4.post("/email/*", async (context, next) => {
5608
+ app3.post("/email/*", async (context, next) => {
6172
5609
  if (!proxyCapabilityEnabled(context.env.PLATFORM_CAPABILITIES, "email")) {
6173
5610
  return context.json(
6174
5611
  {
@@ -6181,25 +5618,15 @@ app4.post("/email/*", async (context, next) => {
6181
5618
  }
6182
5619
  await next();
6183
5620
  });
6184
- app4.route("/ai", ai);
6185
- app4.route("/d1", d1);
6186
- app4.route("/kv", kv);
6187
- app4.route("/r2", r2);
6188
- app4.route("/revalidate", app3);
6189
- app4.route("/email", app);
6190
- app4.route("/email", app2);
6191
- var index_default = {
6192
- fetch: app4.fetch,
6193
- /**
6194
- * Inbound email entry point — bound as the catch-all CF Email Routing rule
6195
- * on the platform mail zone. See `email-router.ts`.
6196
- */
6197
- async email(message, env, ctx) {
6198
- await routeInbound(message, env, ctx);
6199
- }
6200
- };
6201
- var encoder4 = new TextEncoder();
6202
- var decoder = new TextDecoder();
5621
+ app3.route("/ai", ai);
5622
+ app3.route("/d1", d1);
5623
+ app3.route("/kv", kv);
5624
+ app3.route("/r2", r2);
5625
+ app3.route("/revalidate", app2);
5626
+ app3.route("/email", app);
5627
+ var index_default = { fetch: app3.fetch };
5628
+ var encoder6 = new TextEncoder();
5629
+ var decoder4 = new TextDecoder();
6203
5630
  var cachedVerifyKey;
6204
5631
  var cachedKeySecret4;
6205
5632
  async function getVerifyKey(secret) {
@@ -6208,7 +5635,7 @@ async function getVerifyKey(secret) {
6208
5635
  }
6209
5636
  cachedVerifyKey = await crypto.subtle.importKey(
6210
5637
  "raw",
6211
- encoder4.encode(secret),
5638
+ encoder6.encode(secret),
6212
5639
  { name: "HMAC", hash: "SHA-256" },
6213
5640
  false,
6214
5641
  ["verify"]
@@ -6217,32 +5644,11 @@ async function getVerifyKey(secret) {
6217
5644
  return cachedVerifyKey;
6218
5645
  }
6219
5646
  __name(getVerifyKey, "getVerifyKey");
6220
- async function sha256Hex(input) {
6221
- const digest = await crypto.subtle.digest("SHA-256", encoder4.encode(input));
6222
- const bytes = new Uint8Array(digest);
6223
- let out = "";
6224
- for (const b of bytes) {
6225
- out += b.toString(16).padStart(2, "0");
6226
- }
6227
- return out;
6228
- }
6229
- __name(sha256Hex, "sha256Hex");
6230
- function constantTimeEqual(a, b) {
6231
- if (a.length !== b.length) {
6232
- return false;
6233
- }
6234
- let diff = 0;
6235
- for (let i = 0; i < a.length; i++) {
6236
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6237
- }
6238
- return diff === 0;
6239
- }
6240
- __name(constantTimeEqual, "constantTimeEqual");
6241
5647
  async function verifyHmac(data, signature, secret) {
6242
5648
  try {
6243
5649
  const key = await getVerifyKey(secret);
6244
5650
  const sigBytes = base64urlDecode(signature);
6245
- return crypto.subtle.verify("HMAC", key, sigBytes.buffer, encoder4.encode(data));
5651
+ return crypto.subtle.verify("HMAC", key, sigBytes.buffer, encoder6.encode(data));
6246
5652
  } catch {
6247
5653
  return false;
6248
5654
  }
@@ -6267,12 +5673,12 @@ async function verifyJwt(token, secret) {
6267
5673
  "HMAC",
6268
5674
  key,
6269
5675
  sigBytes.buffer,
6270
- encoder4.encode(`${header}.${body}`)
5676
+ encoder6.encode(`${header}.${body}`)
6271
5677
  );
6272
5678
  if (!valid) {
6273
5679
  return null;
6274
5680
  }
6275
- const payload = JSON.parse(decoder.decode(base64urlDecode(body)));
5681
+ const payload = JSON.parse(decoder4.decode(base64urlDecode(body)));
6276
5682
  const now = Math.floor(Date.now() / 1e3);
6277
5683
  if (payload.exp <= now) {
6278
5684
  return null;