@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.
- package/README.md +10 -1
- package/dist/email-protocol.d.mts +48 -0
- package/dist/email-protocol.mjs +278 -0
- package/dist/index.d.mts +154 -15
- package/dist/index.mjs +349 -22
- package/dist/installer.d.mts +5 -3
- package/dist/installer.mjs +14 -5
- package/dist/runtime/api/README.md +1 -1
- package/dist/runtime/api/index.js +79311 -65332
- package/dist/runtime/api/index.js.map +4 -4
- package/dist/runtime/dispatch/README.md +1 -1
- package/dist/runtime/dispatch/index.js +35 -13
- package/dist/runtime/dispatch/index.js.map +2 -2
- package/dist/runtime/drizzle/0041_military_beyonder.sql +119 -0
- package/dist/runtime/drizzle/0042_dark_the_anarchist.sql +1 -0
- package/dist/runtime/drizzle/0043_nostalgic_meggan.sql +21 -0
- package/dist/runtime/drizzle/0044_smart_mentor.sql +15 -0
- package/dist/runtime/drizzle/0045_platform_settings.sql +5 -0
- package/dist/runtime/drizzle/0046_email_domains_platform_credential.sql +8 -0
- package/dist/runtime/drizzle/0047_email_gateway.sql +226 -0
- package/dist/runtime/drizzle/0048_project_teams.sql +133 -0
- package/dist/runtime/drizzle/compatibility.json +4 -0
- package/dist/runtime/drizzle/meta/_journal.json +56 -0
- package/dist/runtime/emailGateway/README.md +1 -0
- package/dist/runtime/emailGateway/index.js +5893 -0
- package/dist/runtime/emailGateway/index.js.map +8 -0
- package/dist/runtime/manifest.json +71 -12
- package/dist/runtime/proxy/README.md +1 -1
- package/dist/runtime/proxy/index.js +1646 -2240
- package/dist/runtime/proxy/index.js.map +4 -4
- package/dist/runtime/tail/README.md +1 -1
- 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
|
-
|
|
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
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
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/
|
|
323
|
-
var
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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,
|
|
519
|
+
var tryDecode = /* @__PURE__ */ __name((str, decoder5) => {
|
|
595
520
|
try {
|
|
596
|
-
return
|
|
521
|
+
return decoder5(str);
|
|
597
522
|
} catch {
|
|
598
523
|
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
|
|
599
524
|
try {
|
|
600
|
-
return
|
|
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,
|
|
1552
|
+
route(path, app4) {
|
|
1631
1553
|
const subApp = this.basePath(path);
|
|
1632
|
-
|
|
1554
|
+
app4.routes.map((r) => {
|
|
1633
1555
|
let handler;
|
|
1634
|
-
if (
|
|
1556
|
+
if (app4.errorHandler === errorHandler) {
|
|
1635
1557
|
handler = r.handler;
|
|
1636
1558
|
} else {
|
|
1637
|
-
handler = /* @__PURE__ */ __name(async (c, next) => (await compose([],
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
3005
|
+
async function verifyProjectDeployAccess(api, jwtSecret, projectId, userId, jwt, accessAssertion, proxyOrigin) {
|
|
3123
3006
|
try {
|
|
3124
|
-
|
|
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({
|
|
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(
|
|
3139
|
-
|
|
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({
|
|
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(
|
|
3156
|
-
var
|
|
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
|
-
|
|
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,
|
|
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-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
])
|
|
3270
|
-
|
|
3271
|
-
|
|
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(
|
|
3217
|
+
__name(readEmailRequestJson, "readEmailRequestJson");
|
|
3274
3218
|
|
|
3275
|
-
// ../../home/runner/work/void/void/platform/packages/proxy/src/email
|
|
3276
|
-
|
|
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-
|
|
3280
|
-
|
|
3281
|
-
var
|
|
3282
|
-
var
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
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(
|
|
3291
|
-
async function
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
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":
|
|
3278
|
+
"x-void-internal": proof
|
|
3304
3279
|
},
|
|
3305
|
-
body:
|
|
3280
|
+
body: raw2,
|
|
3281
|
+
signal: controller.signal
|
|
3282
|
+
})
|
|
3283
|
+
);
|
|
3284
|
+
if (!response.ok) {
|
|
3285
|
+
void response.body?.cancel().catch(() => {
|
|
3306
3286
|
});
|
|
3307
|
-
|
|
3308
|
-
|
|
3287
|
+
if (response.status === 409) {
|
|
3288
|
+
throw new Error("IDEMPOTENCY_CONFLICT");
|
|
3289
|
+
}
|
|
3290
|
+
throw new Error(`EMAIL_ADMISSION_${response.status}`);
|
|
3309
3291
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3292
|
+
const reader = response.body?.getReader();
|
|
3293
|
+
if (!reader) {
|
|
3294
|
+
throw new Error("EMAIL_ADMISSION_EMPTY_RESPONSE");
|
|
3312
3295
|
}
|
|
3313
|
-
|
|
3296
|
+
const bytes = new Uint8Array(MAX_ADMISSION_RESPONSE_BYTES);
|
|
3297
|
+
let size = 0;
|
|
3314
3298
|
try {
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
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(
|
|
3327
|
-
function
|
|
3328
|
-
if (
|
|
3329
|
-
return
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
3374
|
-
var
|
|
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._=/-]+)
|
|
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
|
-
|
|
3762
|
-
|
|
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: "
|
|
4114
|
+
message: "email sending is not configured"
|
|
3771
4115
|
}
|
|
3772
4116
|
},
|
|
3773
4117
|
502
|
|
3774
4118
|
);
|
|
3775
4119
|
}
|
|
3776
|
-
|
|
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
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
);
|
|
3843
|
-
return context.json(
|
|
4171
|
+
let admission;
|
|
4172
|
+
try {
|
|
4173
|
+
admission = await admitEmail(
|
|
4174
|
+
context.env,
|
|
3844
4175
|
{
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
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
|
-
|
|
4184
|
+
sendDeadline.remainingMs(EMAIL_SEND_FINALIZATION_MARGIN_MS)
|
|
3852
4185
|
);
|
|
3853
|
-
}
|
|
3854
|
-
|
|
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: "
|
|
3860
|
-
message:
|
|
4192
|
+
code: conflict ? "IDEMPOTENCY_CONFLICT" : "UPSTREAM_ERROR",
|
|
4193
|
+
message: conflict ? "idempotency key has different content" : "email admission unavailable"
|
|
3861
4194
|
}
|
|
3862
4195
|
},
|
|
3863
|
-
|
|
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
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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:
|
|
4212
|
+
recipient: row.recipient,
|
|
3881
4213
|
ok: false,
|
|
4214
|
+
state: "reserved",
|
|
3882
4215
|
error: {
|
|
3883
|
-
code: "
|
|
3884
|
-
message:
|
|
4216
|
+
code: "OUTCOME_UNKNOWN",
|
|
4217
|
+
message: "email action remains reserved; inspect email logs before retrying"
|
|
3885
4218
|
}
|
|
3886
4219
|
});
|
|
3887
4220
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
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
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
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
|
|
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
|
-
|
|
4303
|
+
state: "provider_accepted",
|
|
4304
|
+
messageId: providerReference
|
|
3943
4305
|
});
|
|
3944
|
-
} catch
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4028
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
return context.json(
|
|
4525
|
+
let admission;
|
|
4526
|
+
try {
|
|
4527
|
+
admission = await admitEmail(
|
|
4528
|
+
context.env,
|
|
4045
4529
|
{
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
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
|
-
|
|
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: "
|
|
4546
|
+
code: conflict ? "IDEMPOTENCY_CONFLICT" : "UPSTREAM_ERROR",
|
|
4547
|
+
message: conflict ? "idempotency key has different content" : "email admission unavailable"
|
|
4066
4548
|
}
|
|
4067
4549
|
},
|
|
4068
|
-
|
|
4550
|
+
conflict ? 409 : 503
|
|
4069
4551
|
);
|
|
4070
4552
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
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: "
|
|
4077
|
-
message:
|
|
4570
|
+
code: "OUTCOME_UNKNOWN",
|
|
4571
|
+
message: "email action remains reserved; inspect email logs before retrying"
|
|
4078
4572
|
}
|
|
4079
|
-
}
|
|
4080
|
-
|
|
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
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
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
|
-
|
|
4113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4130
|
-
|
|
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
|
-
|
|
4619
|
+
operationId: admission.operationId,
|
|
4620
|
+
error: { code: "UPSTREAM_ERROR", message: "email domain changed during admission" }
|
|
4135
4621
|
},
|
|
4136
|
-
|
|
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
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
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
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
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
|
|
4641
|
+
recipient,
|
|
4215
4642
|
ok: false,
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
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
|
|
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
|
|
4652
|
+
let failure = null;
|
|
4653
|
+
let failureState = "failed";
|
|
4654
|
+
let sendRawResult = null;
|
|
4655
|
+
let providerResponse = null;
|
|
4313
4656
|
try {
|
|
4314
|
-
|
|
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
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
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
|
-
|
|
5220
|
-
|
|
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
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
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
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
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
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
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
|
-
|
|
5290
|
-
|
|
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(
|
|
5294
|
-
function
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
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
|
|
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
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
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 (
|
|
5320
|
-
|
|
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
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
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
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
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
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
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(
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
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
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
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
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
if (folded.size >= MAX_INBOUND_HEADERS) {
|
|
5459
|
-
return null;
|
|
4879
|
+
try {
|
|
4880
|
+
if (await validate()) {
|
|
4881
|
+
return !closed;
|
|
5460
4882
|
}
|
|
5461
|
-
|
|
4883
|
+
} catch {
|
|
5462
4884
|
}
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
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(
|
|
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
|
-
|
|
5952
|
-
|
|
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
|
|
5975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5531
|
+
const active = await verifyPrerenderAuthority(
|
|
6122
5532
|
context.env.API,
|
|
6123
5533
|
context.env.JWT_SECRET,
|
|
6124
5534
|
projectId,
|
|
6125
|
-
payload.
|
|
6126
|
-
|
|
6127
|
-
|
|
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
|
|
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
|
-
|
|
6137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
var
|
|
6192
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
5676
|
+
encoder6.encode(`${header}.${body}`)
|
|
6271
5677
|
);
|
|
6272
5678
|
if (!valid) {
|
|
6273
5679
|
return null;
|
|
6274
5680
|
}
|
|
6275
|
-
const payload = JSON.parse(
|
|
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;
|