arcane-os 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/mail-server.mjs
CHANGED
|
@@ -7,32 +7,27 @@ export const RESEND_MAIL_PATH='/v1/mail';
|
|
|
7
7
|
const RESEND_EMAIL_ENDPOINT='https://api.resend.com/emails';
|
|
8
8
|
const APP_ID_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,62})$/u;
|
|
9
9
|
const EMAIL_PATTERN=/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/iu;
|
|
10
|
-
const IDEMPOTENCY_KEY_PATTERN=/^[a-zA-Z0-9._:-]
|
|
11
|
-
const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]
|
|
12
|
-
const PROVIDER_CODE_PATTERN=/^[a-z0-9_]
|
|
13
|
-
const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]
|
|
10
|
+
const IDEMPOTENCY_KEY_PATTERN=/^[a-zA-Z0-9._:-]+$/u;
|
|
11
|
+
const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]+$/u;
|
|
12
|
+
const PROVIDER_CODE_PATTERN=/^[a-z0-9_]+$/u;
|
|
13
|
+
const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]+$/u;
|
|
14
14
|
const JSON_CONTENT_TYPE_PATTERN=/^application\/json(?:\s*;\s*charset\s*=\s*"?utf-8"?)?$/iu;
|
|
15
15
|
const HEADER_NAME_PATTERN=/^[!#$%&'*+.^_`|~0-9a-z-]+$/iu;
|
|
16
16
|
const MAIL_TYPES=new Set(['error','report','crisis_detected']);
|
|
17
|
-
const REPORT_KEYS=new Set(['html','subject','text','to','type']);
|
|
18
17
|
const PREFLIGHT_HEADERS=new Set([
|
|
19
18
|
'content-type','idempotency-key','x-mail-app','x-mail-key'
|
|
20
19
|
]);
|
|
21
20
|
const PERMANENT_RATE_CODES=new Set(['daily_quota_exceeded','monthly_quota_exceeded']);
|
|
22
21
|
const RETRYABLE_PROVIDER_STATUSES=new Set([408,425,429,500,502,503,504]);
|
|
23
|
-
const DEFAULT_MAX_MESSAGE_BYTES=25*1024*1024;
|
|
24
|
-
const DEFAULT_MAX_REQUEST_BYTES=52*1024*1024;
|
|
25
|
-
const DEFAULT_MAX_PROVIDER_RESPONSE_BYTES=64*1024;
|
|
26
|
-
const DEFAULT_MAX_QUEUED_MESSAGE_BYTES=64*1024*1024;
|
|
27
|
-
const MAX_RETRY_AFTER_MS=24*60*60*1000;
|
|
28
22
|
|
|
29
23
|
class MailGatewayFault extends Error {
|
|
30
|
-
constructor(code,{retryable=false,retryAfterMs=0,statusCode=400,uncertain=false}={}){
|
|
24
|
+
constructor(code,{details=null,retryable=false,retryAfterMs=0,statusCode=400,uncertain=false}={}){
|
|
31
25
|
super(code);
|
|
32
26
|
this.name='MailGatewayFault';
|
|
33
27
|
this.code=code;
|
|
28
|
+
this.details=details;
|
|
34
29
|
this.retryable=Boolean(retryable);
|
|
35
|
-
this.retryAfterMs=
|
|
30
|
+
this.retryAfterMs=normalizeRetryAfter(retryAfterMs);
|
|
36
31
|
this.statusCode=statusCode;
|
|
37
32
|
this.uncertain=Boolean(uncertain);
|
|
38
33
|
}
|
|
@@ -44,18 +39,43 @@ function configurationError(message){
|
|
|
44
39
|
return error;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
function
|
|
42
|
+
function completeErrorDetails(error){
|
|
43
|
+
if(!error||typeof error!=='object'){
|
|
44
|
+
return {message:String(error??''),name:'Error'};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...error,
|
|
48
|
+
...(typeof error.code==='string'?{code:error.code}:{}),
|
|
49
|
+
message:typeof error.message==='string'?error.message:String(error),
|
|
50
|
+
name:typeof error.name==='string'?error.name:'Error',
|
|
51
|
+
...(typeof error.stack==='string'?{stack:error.stack}:{})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function positiveInteger(value,fallback,{label,allowZero=false}={}){
|
|
48
56
|
const resolved=value===undefined?fallback:value;
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const minimum=allowZero?0:1;
|
|
58
|
+
if(!Number.isSafeInteger(resolved)||resolved<minimum){
|
|
59
|
+
throw configurationError(`${label} must be ${allowZero?'a nonnegative':'a positive'} integer.`);
|
|
51
60
|
}
|
|
52
61
|
return resolved;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
function optionalPositiveInteger(value,label){
|
|
65
|
+
if(value===undefined||value===null) return null;
|
|
66
|
+
return positiveInteger(value,null,{label});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeRetryAfter(value){
|
|
70
|
+
return Number.isSafeInteger(value)&&value>0?value:0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function portNumber(value,fallback){
|
|
74
|
+
const resolved=value===undefined?fallback:value;
|
|
75
|
+
if(!Number.isSafeInteger(resolved)||resolved<0||resolved>65_535){
|
|
76
|
+
throw configurationError('port must be an integer between 0 and 65535.');
|
|
77
|
+
}
|
|
78
|
+
return resolved;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
function validateSignal(signal){
|
|
@@ -66,8 +86,7 @@ function validateSignal(signal){
|
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
function validateApiKey(value){
|
|
69
|
-
if(typeof value!=='string'||value.length<1
|
|
70
|
-
||!/^[\x21-\x7e]+$/u.test(value)){
|
|
89
|
+
if(typeof value!=='string'||value.length<1||!/^[\x21-\x7e]+$/u.test(value)){
|
|
71
90
|
throw configurationError('Resend API key must be a nonempty printable ASCII string.');
|
|
72
91
|
}
|
|
73
92
|
return value;
|
|
@@ -95,21 +114,20 @@ function normalizeCallerAuthentication(options){
|
|
|
95
114
|
'appKey must be omitted when allowUnauthenticatedCaller is true.'
|
|
96
115
|
);
|
|
97
116
|
}
|
|
98
|
-
return
|
|
117
|
+
return {
|
|
99
118
|
appKeyDigest:null,
|
|
100
119
|
callerAuthentication:'origin-app-id-only'
|
|
101
|
-
}
|
|
120
|
+
};
|
|
102
121
|
}
|
|
103
|
-
if(typeof options.appKey!=='string'
|
|
104
|
-
||options.appKey.length>512||!/^[\x21-\x7e]+$/u.test(options.appKey)){
|
|
122
|
+
if(typeof options.appKey!=='string'||!/^[\x21-\x7e]+$/u.test(options.appKey)){
|
|
105
123
|
throw configurationError(
|
|
106
|
-
'appKey must
|
|
124
|
+
'appKey must be a nonempty printable ASCII string unless unauthenticated caller mode is explicitly enabled.'
|
|
107
125
|
);
|
|
108
126
|
}
|
|
109
|
-
return
|
|
127
|
+
return {
|
|
110
128
|
appKeyDigest:appKeyDigest(options.appKey),
|
|
111
129
|
callerAuthentication:'app-key'
|
|
112
|
-
}
|
|
130
|
+
};
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
function normalizedEmail(value,label){
|
|
@@ -140,17 +158,12 @@ function validateFrom(value){
|
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
function normalizeEmailList(value,label,{allowEmpty=false}={}){
|
|
143
|
-
if(!Array.isArray(value)||
|
|
144
|
-
throw configurationError(`${label} must contain ${allowEmpty?'zero
|
|
161
|
+
if(!Array.isArray(value)||(!allowEmpty&&value.length===0)){
|
|
162
|
+
throw configurationError(`${label} must contain ${allowEmpty?'zero or more':'one or more'} addresses.`);
|
|
145
163
|
}
|
|
146
164
|
const result=[];
|
|
147
|
-
const seen=new Set();
|
|
148
165
|
for(const entry of value){
|
|
149
166
|
const address=normalizedEmail(entry,label);
|
|
150
|
-
if(seen.has(address)){
|
|
151
|
-
throw configurationError(`${label} must not contain duplicate addresses.`);
|
|
152
|
-
}
|
|
153
|
-
seen.add(address);
|
|
154
167
|
result.push(address);
|
|
155
168
|
}
|
|
156
169
|
return result;
|
|
@@ -174,8 +187,8 @@ function normalizeOrigin(value){
|
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
function normalizeOrigins(value){
|
|
177
|
-
if(!Array.isArray(value)||value.length===0
|
|
178
|
-
throw configurationError('allowedOrigins must contain one
|
|
190
|
+
if(!Array.isArray(value)||value.length===0){
|
|
191
|
+
throw configurationError('allowedOrigins must contain one or more exact origins.');
|
|
179
192
|
}
|
|
180
193
|
const origins=[];
|
|
181
194
|
const seen=new Set();
|
|
@@ -203,8 +216,9 @@ function normalizeConfiguration(options={}){
|
|
|
203
216
|
}
|
|
204
217
|
const callerAuthentication=normalizeCallerAuthentication(options);
|
|
205
218
|
const recipientAllowlist=normalizeEmailList(
|
|
206
|
-
options.recipientAllowlist,
|
|
207
|
-
'recipientAllowlist'
|
|
219
|
+
options.recipientAllowlist??[],
|
|
220
|
+
'recipientAllowlist',
|
|
221
|
+
{allowEmpty:true}
|
|
208
222
|
);
|
|
209
223
|
const errorRecipients=normalizeEmailList(
|
|
210
224
|
options.errorRecipients??[],
|
|
@@ -212,24 +226,11 @@ function normalizeConfiguration(options={}){
|
|
|
212
226
|
{allowEmpty:true}
|
|
213
227
|
);
|
|
214
228
|
const allowedRecipients=new Set(recipientAllowlist);
|
|
215
|
-
if(errorRecipients.some(function errorRecipientIsNotAllowed(address){
|
|
229
|
+
if(recipientAllowlist.length>0&&errorRecipients.some(function errorRecipientIsNotAllowed(address){
|
|
216
230
|
return !allowedRecipients.has(address);
|
|
217
231
|
})){
|
|
218
232
|
throw configurationError('Every error recipient must also be in recipientAllowlist.');
|
|
219
233
|
}
|
|
220
|
-
const maxMessageBytes=boundedInteger(
|
|
221
|
-
options.maxMessageBytes,
|
|
222
|
-
DEFAULT_MAX_MESSAGE_BYTES,
|
|
223
|
-
{label:'maxMessageBytes',min:1,max:DEFAULT_MAX_MESSAGE_BYTES}
|
|
224
|
-
);
|
|
225
|
-
const maxRequestBytes=boundedInteger(
|
|
226
|
-
options.maxRequestBytes,
|
|
227
|
-
DEFAULT_MAX_REQUEST_BYTES,
|
|
228
|
-
{label:'maxRequestBytes',min:256,max:64*1024*1024}
|
|
229
|
-
);
|
|
230
|
-
if(maxRequestBytes<maxMessageBytes+256){
|
|
231
|
-
throw configurationError('maxRequestBytes must leave at least 256 bytes beyond maxMessageBytes.');
|
|
232
|
-
}
|
|
233
234
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
234
235
|
if(typeof fetchImpl!=='function'){
|
|
235
236
|
throw configurationError('A fetch implementation is required for Resend delivery.');
|
|
@@ -240,85 +241,30 @@ function normalizeConfiguration(options={}){
|
|
|
240
241
|
if(options.requestIdFactory!==undefined&&typeof options.requestIdFactory!=='function'){
|
|
241
242
|
throw configurationError('requestIdFactory must be a function when supplied.');
|
|
242
243
|
}
|
|
243
|
-
return
|
|
244
|
-
allowAnyRecipient:
|
|
244
|
+
return {
|
|
245
|
+
allowAnyRecipient:recipientAllowlist.length===0,
|
|
245
246
|
allowedOrigins:normalizeOrigins(options.allowedOrigins),
|
|
246
247
|
allowedRecipients,
|
|
247
248
|
apiKey:validateApiKey(options.apiKey),
|
|
248
249
|
appKeyDigest:callerAuthentication.appKeyDigest,
|
|
249
250
|
appId:validateAppId(options.appId),
|
|
250
|
-
|
|
251
|
-
options.bodyQueueTimeoutMs,
|
|
252
|
-
5_000,
|
|
253
|
-
{label:'bodyQueueTimeoutMs',min:1,max:60_000}
|
|
254
|
-
),
|
|
255
|
-
bodyTimeoutMs:boundedInteger(
|
|
256
|
-
options.bodyTimeoutMs,
|
|
257
|
-
10_000,
|
|
258
|
-
{label:'bodyTimeoutMs',min:100,max:120_000}
|
|
259
|
-
),
|
|
251
|
+
bodyTimeoutMs:optionalPositiveInteger(options.bodyTimeoutMs,'bodyTimeoutMs'),
|
|
260
252
|
errorRecipients,
|
|
261
253
|
fetchImpl,
|
|
262
254
|
from:validateFrom(options.from),
|
|
263
255
|
host:validateLoopbackHost(options.host??'127.0.0.1'),
|
|
264
256
|
callerAuthentication:callerAuthentication.callerAuthentication,
|
|
265
|
-
maxConcurrentBodyReads:boundedInteger(
|
|
266
|
-
options.maxConcurrentBodyReads,
|
|
267
|
-
8,
|
|
268
|
-
{label:'maxConcurrentBodyReads',min:1,max:64}
|
|
269
|
-
),
|
|
270
|
-
maxConcurrentSends:boundedInteger(
|
|
271
|
-
options.maxConcurrentSends,
|
|
272
|
-
2,
|
|
273
|
-
{label:'maxConcurrentSends',min:1,max:16}
|
|
274
|
-
),
|
|
275
|
-
maxMessageBytes,
|
|
276
|
-
maxProviderResponseBytes:boundedInteger(
|
|
277
|
-
options.maxProviderResponseBytes,
|
|
278
|
-
DEFAULT_MAX_PROVIDER_RESPONSE_BYTES,
|
|
279
|
-
{label:'maxProviderResponseBytes',min:64,max:1024*1024}
|
|
280
|
-
),
|
|
281
|
-
maxQueuedBodyReads:boundedInteger(
|
|
282
|
-
options.maxQueuedBodyReads,
|
|
283
|
-
64,
|
|
284
|
-
{label:'maxQueuedBodyReads',min:0,max:1024}
|
|
285
|
-
),
|
|
286
|
-
maxQueuedSends:boundedInteger(
|
|
287
|
-
options.maxQueuedSends,
|
|
288
|
-
32,
|
|
289
|
-
{label:'maxQueuedSends',min:0,max:1024}
|
|
290
|
-
),
|
|
291
|
-
maxQueuedMessageBytes:boundedInteger(
|
|
292
|
-
options.maxQueuedMessageBytes,
|
|
293
|
-
DEFAULT_MAX_QUEUED_MESSAGE_BYTES,
|
|
294
|
-
{label:'maxQueuedMessageBytes',min:0,max:512*1024*1024}
|
|
295
|
-
),
|
|
296
|
-
maxRequestBytes,
|
|
297
257
|
onEvent:options.onEvent,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
1_000,
|
|
301
|
-
{label:'observerDrainTimeoutMs',min:1,max:10_000}
|
|
302
|
-
),
|
|
303
|
-
port:boundedInteger(options.port,8025,{label:'port',min:0,max:65_535}),
|
|
304
|
-
providerTimeoutMs:boundedInteger(
|
|
305
|
-
options.providerTimeoutMs,
|
|
306
|
-
120_000,
|
|
307
|
-
{label:'providerTimeoutMs',min:100,max:600_000}
|
|
308
|
-
),
|
|
258
|
+
port:portNumber(options.port,8025),
|
|
259
|
+
providerTimeoutMs:optionalPositiveInteger(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
309
260
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
310
|
-
retryableDelayMs:
|
|
261
|
+
retryableDelayMs:positiveInteger(
|
|
311
262
|
options.retryableDelayMs,
|
|
312
263
|
1_000,
|
|
313
|
-
{label:'retryableDelayMs'
|
|
314
|
-
),
|
|
315
|
-
sendQueueTimeoutMs:boundedInteger(
|
|
316
|
-
options.sendQueueTimeoutMs,
|
|
317
|
-
10_000,
|
|
318
|
-
{label:'sendQueueTimeoutMs',min:1,max:120_000}
|
|
264
|
+
{label:'retryableDelayMs'}
|
|
319
265
|
),
|
|
320
266
|
signal:validateSignal(options.signal)
|
|
321
|
-
}
|
|
267
|
+
};
|
|
322
268
|
}
|
|
323
269
|
|
|
324
270
|
function createRequestId(factory){
|
|
@@ -328,7 +274,7 @@ function createRequestId(factory){
|
|
|
328
274
|
return candidate;
|
|
329
275
|
}
|
|
330
276
|
}catch{
|
|
331
|
-
// A diagnostic identifier must never prevent
|
|
277
|
+
// A diagnostic identifier must never prevent an error response.
|
|
332
278
|
}
|
|
333
279
|
return randomUUID();
|
|
334
280
|
}
|
|
@@ -410,8 +356,7 @@ function authenticateLocalCaller(request,configuration){
|
|
|
410
356
|
return;
|
|
411
357
|
}
|
|
412
358
|
const candidate=values.length===1?values[0]:'';
|
|
413
|
-
const candidateIsValid
|
|
414
|
-
&&/^[\x21-\x7e]+$/u.test(candidate);
|
|
359
|
+
const candidateIsValid=/^[\x21-\x7e]+$/u.test(candidate);
|
|
415
360
|
const digest=appKeyDigest(candidateIsValid?candidate:'');
|
|
416
361
|
const authenticated=timingSafeEqual(configuration.appKeyDigest,digest);
|
|
417
362
|
if(values.length!==1||!candidateIsValid||!authenticated){
|
|
@@ -435,14 +380,7 @@ function corsHeaders(origin,{allowPrivateNetwork=false}={}){
|
|
|
435
380
|
}
|
|
436
381
|
|
|
437
382
|
function baseResponseHeaders(origin){
|
|
438
|
-
return
|
|
439
|
-
'cache-control':'no-store',
|
|
440
|
-
'content-security-policy':"default-src 'none'; frame-ancestors 'none'; base-uri 'none'",
|
|
441
|
-
'cross-origin-resource-policy':'cross-origin',
|
|
442
|
-
'referrer-policy':'no-referrer',
|
|
443
|
-
'x-content-type-options':'nosniff',
|
|
444
|
-
...corsHeaders(origin)
|
|
445
|
-
};
|
|
383
|
+
return corsHeaders(origin);
|
|
446
384
|
}
|
|
447
385
|
|
|
448
386
|
function writeJson(response,statusCode,value,{origin='',retryAfterMs=0}={}){
|
|
@@ -455,9 +393,9 @@ function writeJson(response,statusCode,value,{origin='',retryAfterMs=0}={}){
|
|
|
455
393
|
'content-length':String(Buffer.byteLength(body,'utf8')),
|
|
456
394
|
'content-type':'application/json; charset=utf-8'
|
|
457
395
|
};
|
|
458
|
-
const
|
|
459
|
-
if(
|
|
460
|
-
headers['retry-after']=String(Math.max(1,Math.ceil(
|
|
396
|
+
const delay=normalizeRetryAfter(retryAfterMs);
|
|
397
|
+
if(delay){
|
|
398
|
+
headers['retry-after']=String(Math.max(1,Math.ceil(delay/1000)));
|
|
461
399
|
}
|
|
462
400
|
response.writeHead(statusCode,headers);
|
|
463
401
|
response.end(body);
|
|
@@ -478,11 +416,13 @@ function writePreflight(response,origin,{allowPrivateNetwork=false}={}){
|
|
|
478
416
|
}
|
|
479
417
|
|
|
480
418
|
function writeFault(response,requestId,fault,origin=''){
|
|
481
|
-
const retryAfterMs=
|
|
419
|
+
const retryAfterMs=normalizeRetryAfter(fault.retryAfterMs);
|
|
482
420
|
return writeJson(response,fault.statusCode,{
|
|
483
421
|
requestId,
|
|
484
422
|
error:{
|
|
485
423
|
code:PROVIDER_CODE_PATTERN.test(fault.code)?fault.code:'mail_gateway_error',
|
|
424
|
+
message:fault.message,
|
|
425
|
+
details:fault.details,
|
|
486
426
|
retryable:Boolean(fault.retryable),
|
|
487
427
|
uncertain:Boolean(fault.uncertain),
|
|
488
428
|
...(retryAfterMs?{retryAfterMs}:{})
|
|
@@ -494,7 +434,10 @@ function normalizeFault(error){
|
|
|
494
434
|
if(error instanceof MailGatewayFault){
|
|
495
435
|
return error;
|
|
496
436
|
}
|
|
497
|
-
return new MailGatewayFault('mail_gateway_error',{
|
|
437
|
+
return new MailGatewayFault('mail_gateway_error',{
|
|
438
|
+
details:completeErrorDetails(error),
|
|
439
|
+
statusCode:500
|
|
440
|
+
});
|
|
498
441
|
}
|
|
499
442
|
|
|
500
443
|
function validatePreflight(request){
|
|
@@ -521,7 +464,7 @@ function validatePreflight(request){
|
|
|
521
464
|
return {allowPrivateNetwork:privateNetwork==='true'};
|
|
522
465
|
}
|
|
523
466
|
|
|
524
|
-
function createObserver(onEvent
|
|
467
|
+
function createObserver(onEvent){
|
|
525
468
|
const pending=new Set();
|
|
526
469
|
function observe(event){
|
|
527
470
|
if(!onEvent){
|
|
@@ -529,7 +472,7 @@ function createObserver(onEvent,drainTimeoutMs){
|
|
|
529
472
|
}
|
|
530
473
|
let result;
|
|
531
474
|
try{
|
|
532
|
-
result=onEvent(
|
|
475
|
+
result=onEvent({...event});
|
|
533
476
|
}catch{
|
|
534
477
|
return;
|
|
535
478
|
}
|
|
@@ -542,242 +485,27 @@ function createObserver(onEvent,drainTimeoutMs){
|
|
|
542
485
|
.finally(function releaseObserverTask(){pending.delete(task);});
|
|
543
486
|
}
|
|
544
487
|
async function drain(){
|
|
545
|
-
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
let timer;
|
|
549
|
-
const timeout=new Promise(function boundObserverDrain(resolve){
|
|
550
|
-
timer=setTimeout(resolve,drainTimeoutMs);
|
|
551
|
-
});
|
|
552
|
-
await Promise.race([Promise.allSettled([...pending]),timeout]);
|
|
553
|
-
clearTimeout(timer);
|
|
554
|
-
pending.clear();
|
|
488
|
+
await Promise.allSettled([...pending]);
|
|
555
489
|
}
|
|
556
490
|
return {drain,observe};
|
|
557
491
|
}
|
|
558
492
|
|
|
559
|
-
function
|
|
560
|
-
maxQueuedWeight=Number.MAX_SAFE_INTEGER,weightCode=code}){
|
|
561
|
-
let active=0;
|
|
562
|
-
let closed=false;
|
|
563
|
-
let queuedWeight=0;
|
|
564
|
-
const pending=[];
|
|
565
|
-
const idleWaiters=[];
|
|
566
|
-
|
|
567
|
-
function notifyIdle(){
|
|
568
|
-
if(active!==0||pending.length!==0){
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
while(idleWaiters.length){
|
|
572
|
-
idleWaiters.shift()();
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function cleanupItem(item){
|
|
577
|
-
if(item.timeout){
|
|
578
|
-
clearTimeout(item.timeout);
|
|
579
|
-
}
|
|
580
|
-
item.signal?.removeEventListener('abort',item.onAbort);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function releaseQueuedWeight(item){
|
|
584
|
-
if(!item.queued){
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
item.queued=false;
|
|
588
|
-
queuedWeight-=item.weight;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function removePending(item){
|
|
592
|
-
const index=pending.indexOf(item);
|
|
593
|
-
if(index>=0){
|
|
594
|
-
pending.splice(index,1);
|
|
595
|
-
releaseQueuedWeight(item);
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function rejectQueuedItem(item,fault){
|
|
602
|
-
if(!removePending(item)){
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
cleanupItem(item);
|
|
606
|
-
item.reject(fault);
|
|
607
|
-
notifyIdle();
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function dispatch(){
|
|
611
|
-
while(!closed&&active<concurrency&&pending.length){
|
|
612
|
-
const item=pending.shift();
|
|
613
|
-
startItem(item);
|
|
614
|
-
}
|
|
615
|
-
notifyIdle();
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async function executeItem(item){
|
|
619
|
-
try{
|
|
620
|
-
item.resolve(await item.work());
|
|
621
|
-
}catch(error){
|
|
622
|
-
item.reject(error);
|
|
623
|
-
}finally{
|
|
624
|
-
active-=1;
|
|
625
|
-
dispatch();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function startItem(item){
|
|
630
|
-
releaseQueuedWeight(item);
|
|
631
|
-
cleanupItem(item);
|
|
632
|
-
if(item.signal?.aborted){
|
|
633
|
-
item.reject(new MailGatewayFault('mail_request_cancelled',{
|
|
634
|
-
retryable:true,
|
|
635
|
-
statusCode:408
|
|
636
|
-
}));
|
|
637
|
-
dispatch();
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
active+=1;
|
|
641
|
-
void executeItem(item);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function schedule(work,{signal,weight=0}={}){
|
|
645
|
-
if(closed){
|
|
646
|
-
return Promise.reject(new MailGatewayFault('mail_server_stopping',{
|
|
647
|
-
retryable:true,
|
|
648
|
-
retryAfterMs,
|
|
649
|
-
statusCode:503
|
|
650
|
-
}));
|
|
651
|
-
}
|
|
652
|
-
if(signal?.aborted){
|
|
653
|
-
return Promise.reject(new MailGatewayFault('mail_request_cancelled',{
|
|
654
|
-
retryable:true,
|
|
655
|
-
statusCode:408
|
|
656
|
-
}));
|
|
657
|
-
}
|
|
658
|
-
if(!Number.isSafeInteger(weight)||weight<0){
|
|
659
|
-
return Promise.reject(new MailGatewayFault('mail_invalid_queue_weight',{
|
|
660
|
-
statusCode:500
|
|
661
|
-
}));
|
|
662
|
-
}
|
|
663
|
-
return new Promise(function createScheduledWork(resolve,reject){
|
|
664
|
-
const item={
|
|
665
|
-
onAbort:null,
|
|
666
|
-
queued:false,
|
|
667
|
-
reject,
|
|
668
|
-
resolve,
|
|
669
|
-
signal,
|
|
670
|
-
timeout:null,
|
|
671
|
-
weight,
|
|
672
|
-
work
|
|
673
|
-
};
|
|
674
|
-
item.onAbort=function cancelQueuedWork(){
|
|
675
|
-
rejectQueuedItem(item,new MailGatewayFault('mail_request_cancelled',{
|
|
676
|
-
retryable:true,
|
|
677
|
-
statusCode:408
|
|
678
|
-
}));
|
|
679
|
-
};
|
|
680
|
-
if(active<concurrency){
|
|
681
|
-
startItem(item);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
if(pending.length>=maxQueued){
|
|
685
|
-
reject(new MailGatewayFault(code,{
|
|
686
|
-
retryable:true,
|
|
687
|
-
retryAfterMs,
|
|
688
|
-
statusCode:503
|
|
689
|
-
}));
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
if(weight>maxQueuedWeight||queuedWeight>maxQueuedWeight-weight){
|
|
693
|
-
reject(new MailGatewayFault(weightCode,{
|
|
694
|
-
retryable:true,
|
|
695
|
-
retryAfterMs,
|
|
696
|
-
statusCode:503
|
|
697
|
-
}));
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
item.queued=true;
|
|
701
|
-
queuedWeight+=weight;
|
|
702
|
-
pending.push(item);
|
|
703
|
-
item.signal?.addEventListener('abort',item.onAbort,{once:true});
|
|
704
|
-
if(item.signal?.aborted){
|
|
705
|
-
item.onAbort();
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
item.timeout=setTimeout(function expireQueuedWork(){
|
|
709
|
-
rejectQueuedItem(item,new MailGatewayFault(`${code}_timeout`,{
|
|
710
|
-
retryable:true,
|
|
711
|
-
retryAfterMs,
|
|
712
|
-
statusCode:503
|
|
713
|
-
}));
|
|
714
|
-
},queueTimeoutMs);
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function close(){
|
|
719
|
-
if(closed){
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
closed=true;
|
|
723
|
-
while(pending.length){
|
|
724
|
-
const item=pending.shift();
|
|
725
|
-
releaseQueuedWeight(item);
|
|
726
|
-
cleanupItem(item);
|
|
727
|
-
item.reject(new MailGatewayFault('mail_server_stopping',{
|
|
728
|
-
retryable:true,
|
|
729
|
-
retryAfterMs,
|
|
730
|
-
statusCode:503
|
|
731
|
-
}));
|
|
732
|
-
}
|
|
733
|
-
notifyIdle();
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function idle(){
|
|
737
|
-
if(active===0&&pending.length===0){
|
|
738
|
-
return Promise.resolve();
|
|
739
|
-
}
|
|
740
|
-
return new Promise(function waitForSchedulerIdle(resolve){
|
|
741
|
-
idleWaiters.push(resolve);
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
return {close,idle,schedule};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function requestContentLength(request,maxRequestBytes){
|
|
749
|
-
const raw=singleHeader(request,'content-length',{required:false});
|
|
750
|
-
if(!raw){
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
753
|
-
if(!/^\d+$/u.test(raw)){
|
|
754
|
-
throw new MailGatewayFault('mail_invalid_content_length',{statusCode:400});
|
|
755
|
-
}
|
|
756
|
-
const length=Number(raw);
|
|
757
|
-
if(!Number.isSafeInteger(length)){
|
|
758
|
-
throw new MailGatewayFault('mail_invalid_content_length',{statusCode:400});
|
|
759
|
-
}
|
|
760
|
-
if(length>maxRequestBytes){
|
|
761
|
-
throw new MailGatewayFault('mail_request_too_large',{statusCode:413});
|
|
762
|
-
}
|
|
763
|
-
return length;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function readRequestBody(request,{maxRequestBytes,timeoutMs,signal}){
|
|
493
|
+
function readRequestBody(request,{timeoutMs,signal}){
|
|
767
494
|
return new Promise(function collectRequestBody(resolve,reject){
|
|
768
495
|
const chunks=[];
|
|
769
|
-
let byteLength=0;
|
|
770
496
|
let settled=false;
|
|
771
|
-
const timer=
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
497
|
+
const timer=timeoutMs==null
|
|
498
|
+
?null
|
|
499
|
+
:setTimeout(function expireRequestBody(){
|
|
500
|
+
finish(new MailGatewayFault('mail_body_timeout',{
|
|
501
|
+
retryable:true,
|
|
502
|
+
statusCode:408
|
|
503
|
+
}));
|
|
504
|
+
request.resume();
|
|
505
|
+
},timeoutMs);
|
|
778
506
|
|
|
779
507
|
function cleanup(){
|
|
780
|
-
clearTimeout(timer);
|
|
508
|
+
if(timer!==null) clearTimeout(timer);
|
|
781
509
|
request.removeListener('data',onData);
|
|
782
510
|
request.removeListener('end',onEnd);
|
|
783
511
|
request.removeListener('error',onError);
|
|
@@ -800,21 +528,16 @@ function readRequestBody(request,{maxRequestBytes,timeoutMs,signal}){
|
|
|
800
528
|
|
|
801
529
|
function onData(chunk){
|
|
802
530
|
const bytes=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
|
|
803
|
-
byteLength+=bytes.length;
|
|
804
|
-
if(byteLength>maxRequestBytes){
|
|
805
|
-
finish(new MailGatewayFault('mail_request_too_large',{statusCode:413}));
|
|
806
|
-
request.resume();
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
531
|
chunks.push(bytes);
|
|
810
532
|
}
|
|
811
533
|
|
|
812
534
|
function onEnd(){
|
|
813
|
-
finish(null,Buffer.concat(chunks
|
|
535
|
+
finish(null,Buffer.concat(chunks).toString('utf8'));
|
|
814
536
|
}
|
|
815
537
|
|
|
816
|
-
function onError(){
|
|
538
|
+
function onError(error){
|
|
817
539
|
finish(new MailGatewayFault('mail_request_stream_failed',{
|
|
540
|
+
details:completeErrorDetails(error),
|
|
818
541
|
retryable:true,
|
|
819
542
|
statusCode:400
|
|
820
543
|
}));
|
|
@@ -846,10 +569,6 @@ function readRequestBody(request,{maxRequestBytes,timeoutMs,signal}){
|
|
|
846
569
|
});
|
|
847
570
|
}
|
|
848
571
|
|
|
849
|
-
function utf8Bytes(value){
|
|
850
|
-
return Buffer.byteLength(value,'utf8');
|
|
851
|
-
}
|
|
852
|
-
|
|
853
572
|
function normalizedReportEmail(value){
|
|
854
573
|
if(typeof value!=='string'){
|
|
855
574
|
throw new MailGatewayFault('mail_invalid_recipient',{statusCode:422});
|
|
@@ -862,20 +581,15 @@ function normalizedReportEmail(value){
|
|
|
862
581
|
}
|
|
863
582
|
|
|
864
583
|
function normalizeReportRecipients(report,configuration){
|
|
865
|
-
if(!Array.isArray(report.to)
|
|
584
|
+
if(!Array.isArray(report.to)){
|
|
866
585
|
throw new MailGatewayFault('mail_invalid_recipients',{statusCode:422});
|
|
867
586
|
}
|
|
868
587
|
const recipients=[];
|
|
869
|
-
const seen=new Set();
|
|
870
588
|
for(const value of report.to){
|
|
871
589
|
const address=normalizedReportEmail(value);
|
|
872
|
-
if(seen.has(address)){
|
|
873
|
-
throw new MailGatewayFault('mail_duplicate_recipient',{statusCode:422});
|
|
874
|
-
}
|
|
875
590
|
if(!configuration.allowAnyRecipient&&!configuration.allowedRecipients.has(address)){
|
|
876
591
|
throw new MailGatewayFault('mail_recipient_not_allowed',{statusCode:403});
|
|
877
592
|
}
|
|
878
|
-
seen.add(address);
|
|
879
593
|
recipients.push(address);
|
|
880
594
|
}
|
|
881
595
|
if(recipients.length===0&&report.type==='error'){
|
|
@@ -891,18 +605,14 @@ function normalizeReport(value,configuration){
|
|
|
891
605
|
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
892
606
|
throw new MailGatewayFault('mail_invalid_report',{statusCode:422});
|
|
893
607
|
}
|
|
894
|
-
|
|
895
|
-
if(keys.some(function reportKeyIsUnknown(key){return !REPORT_KEYS.has(key);})
|
|
896
|
-
||!Object.hasOwn(value,'subject')||!Object.hasOwn(value,'to')
|
|
608
|
+
if(!Object.hasOwn(value,'subject')||!Object.hasOwn(value,'to')
|
|
897
609
|
||!Object.hasOwn(value,'type')){
|
|
898
610
|
throw new MailGatewayFault('mail_invalid_report_shape',{statusCode:422});
|
|
899
611
|
}
|
|
900
612
|
if(typeof value.type!=='string'||!MAIL_TYPES.has(value.type)){
|
|
901
613
|
throw new MailGatewayFault('mail_invalid_type',{statusCode:422});
|
|
902
614
|
}
|
|
903
|
-
if(typeof value.subject!=='string'
|
|
904
|
-
||value.subject.length<1||value.subject.length>160
|
|
905
|
-
||/[\u0000-\u001f\u007f]/u.test(value.subject)){
|
|
615
|
+
if(typeof value.subject!=='string'){
|
|
906
616
|
throw new MailGatewayFault('mail_invalid_subject',{statusCode:422});
|
|
907
617
|
}
|
|
908
618
|
const hasText=Object.hasOwn(value,'text');
|
|
@@ -911,21 +621,11 @@ function normalizeReport(value,configuration){
|
|
|
911
621
|
||(hasHtml&&typeof value.html!=='string')){
|
|
912
622
|
throw new MailGatewayFault('mail_content_required',{statusCode:422});
|
|
913
623
|
}
|
|
914
|
-
const text=hasText?value.text:'';
|
|
915
|
-
const html=hasHtml?value.html:'';
|
|
916
|
-
if(!/\S/u.test(text)&&!/\S/u.test(html)){
|
|
917
|
-
throw new MailGatewayFault('mail_content_required',{statusCode:422});
|
|
918
|
-
}
|
|
919
|
-
const unsupportedControl=/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
920
|
-
if(unsupportedControl.test(text)||unsupportedControl.test(html)){
|
|
921
|
-
throw new MailGatewayFault('mail_unsupported_content',{statusCode:422});
|
|
922
|
-
}
|
|
923
|
-
const messageBytes=utf8Bytes(text)+utf8Bytes(html);
|
|
924
|
-
if(messageBytes>configuration.maxMessageBytes){
|
|
925
|
-
throw new MailGatewayFault('mail_message_too_large',{statusCode:413});
|
|
926
|
-
}
|
|
927
624
|
const recipients=normalizeReportRecipients(value,configuration);
|
|
625
|
+
const providerFields={...value};
|
|
626
|
+
delete providerFields.type;
|
|
928
627
|
const providerBody={
|
|
628
|
+
...providerFields,
|
|
929
629
|
from:configuration.from,
|
|
930
630
|
to:recipients,
|
|
931
631
|
subject:value.subject,
|
|
@@ -933,12 +633,9 @@ function normalizeReport(value,configuration){
|
|
|
933
633
|
...(hasHtml?{html:value.html}:{})
|
|
934
634
|
};
|
|
935
635
|
const serializedProviderBody=JSON.stringify(providerBody);
|
|
936
|
-
if(utf8Bytes(serializedProviderBody)>configuration.maxRequestBytes){
|
|
937
|
-
throw new MailGatewayFault('mail_request_too_large',{statusCode:413});
|
|
938
|
-
}
|
|
939
636
|
return {
|
|
637
|
+
report:{...value},
|
|
940
638
|
providerBody:serializedProviderBody,
|
|
941
|
-
providerBodyBytes:utf8Bytes(serializedProviderBody),
|
|
942
639
|
recipientCount:recipients.length
|
|
943
640
|
};
|
|
944
641
|
}
|
|
@@ -959,11 +656,11 @@ function parseRetryAfter(value,now=Date.now()){
|
|
|
959
656
|
}
|
|
960
657
|
const trimmed=value.trim();
|
|
961
658
|
if(/^\d+(?:\.\d+)?$/u.test(trimmed)){
|
|
962
|
-
return
|
|
659
|
+
return normalizeRetryAfter(Math.ceil(Number(trimmed)*1000));
|
|
963
660
|
}
|
|
964
661
|
const timestamp=Date.parse(trimmed);
|
|
965
662
|
return Number.isFinite(timestamp)
|
|
966
|
-
?
|
|
663
|
+
? normalizeRetryAfter(Math.max(0,timestamp-now))
|
|
967
664
|
: 0;
|
|
968
665
|
}
|
|
969
666
|
|
|
@@ -1029,18 +726,7 @@ function cancelProviderReader(reader){
|
|
|
1029
726
|
}
|
|
1030
727
|
}
|
|
1031
728
|
|
|
1032
|
-
async function readProviderBody(response,
|
|
1033
|
-
const declared=responseHeader(response,'content-length');
|
|
1034
|
-
if(declared){
|
|
1035
|
-
if(!/^\d+$/u.test(declared)||!Number.isSafeInteger(Number(declared))
|
|
1036
|
-
||Number(declared)>maxBytes){
|
|
1037
|
-
cancelProviderBody(response.body);
|
|
1038
|
-
throw new MailGatewayFault('resend_response_too_large',{
|
|
1039
|
-
statusCode:502,
|
|
1040
|
-
uncertain:true
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
729
|
+
async function readProviderBody(response,signal){
|
|
1044
730
|
if(response.body===null||response.body===undefined){
|
|
1045
731
|
return '';
|
|
1046
732
|
}
|
|
@@ -1052,8 +738,8 @@ async function readProviderBody(response,maxBytes,signal){
|
|
|
1052
738
|
});
|
|
1053
739
|
}
|
|
1054
740
|
const reader=response.body.getReader();
|
|
1055
|
-
const
|
|
1056
|
-
let
|
|
741
|
+
const decoder=new TextDecoder();
|
|
742
|
+
let text='';
|
|
1057
743
|
let fullyRead=false;
|
|
1058
744
|
try{
|
|
1059
745
|
while(true){
|
|
@@ -1068,16 +754,9 @@ async function readProviderBody(response,maxBytes,signal){
|
|
|
1068
754
|
uncertain:true
|
|
1069
755
|
});
|
|
1070
756
|
}
|
|
1071
|
-
|
|
1072
|
-
if(byteLength>maxBytes){
|
|
1073
|
-
throw new MailGatewayFault('resend_response_too_large',{
|
|
1074
|
-
statusCode:502,
|
|
1075
|
-
uncertain:true
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
chunks.push(Buffer.from(result.value));
|
|
757
|
+
text+=decoder.decode(result.value,{stream:true});
|
|
1079
758
|
}
|
|
1080
|
-
return
|
|
759
|
+
return text+decoder.decode();
|
|
1081
760
|
}finally{
|
|
1082
761
|
if(!fullyRead){
|
|
1083
762
|
cancelProviderReader(reader);
|
|
@@ -1118,11 +797,12 @@ function providerRejection(statusCode,value,retryAfterMs,defaultRetryAfterMs){
|
|
|
1118
797
|
||(!permanentRateLimit&&code!=='invalid_idempotent_request'
|
|
1119
798
|
&&RETRYABLE_PROVIDER_STATUSES.has(statusCode));
|
|
1120
799
|
const resolvedDelay=retryable
|
|
1121
|
-
?
|
|
800
|
+
? normalizeRetryAfter(retryAfterMs||defaultRetryAfterMs)
|
|
1122
801
|
: 0;
|
|
1123
802
|
return {
|
|
1124
803
|
kind:'rejected',
|
|
1125
804
|
fault:new MailGatewayFault(code,{
|
|
805
|
+
details:value,
|
|
1126
806
|
retryable,
|
|
1127
807
|
retryAfterMs:resolvedDelay,
|
|
1128
808
|
statusCode:retryable?(statusCode===429?429:503):422
|
|
@@ -1131,18 +811,24 @@ function providerRejection(statusCode,value,retryAfterMs,defaultRetryAfterMs){
|
|
|
1131
811
|
};
|
|
1132
812
|
}
|
|
1133
813
|
|
|
1134
|
-
function ambiguousResult(code,retryAfterMs,providerStatus=0){
|
|
814
|
+
function ambiguousResult(code,retryAfterMs,providerStatus=0,details=null){
|
|
1135
815
|
return {
|
|
1136
816
|
code,
|
|
817
|
+
details,
|
|
1137
818
|
kind:'ambiguous',
|
|
1138
819
|
providerStatus,
|
|
1139
|
-
retryAfterMs:
|
|
820
|
+
retryAfterMs:normalizeRetryAfter(retryAfterMs)
|
|
1140
821
|
};
|
|
1141
822
|
}
|
|
1142
823
|
|
|
1143
824
|
async function performResendAttempt(configuration,delivery,idempotencyKey,signal,requestId,observe){
|
|
1144
825
|
const controller=new AbortController();
|
|
826
|
+
let outcome=null;
|
|
1145
827
|
let timedOut=false;
|
|
828
|
+
function completeAttempt(result){
|
|
829
|
+
outcome=result;
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
1146
832
|
function forwardAbort(){
|
|
1147
833
|
controller.abort(signal?.reason??new Error('Mail request cancelled.'));
|
|
1148
834
|
}
|
|
@@ -1150,14 +836,19 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
1150
836
|
if(signal?.aborted){
|
|
1151
837
|
forwardAbort();
|
|
1152
838
|
}
|
|
1153
|
-
const timeout=
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
839
|
+
const timeout=configuration.providerTimeoutMs==null
|
|
840
|
+
?null
|
|
841
|
+
:setTimeout(function expireResendAttempt(){
|
|
842
|
+
timedOut=true;
|
|
843
|
+
controller.abort(new Error('Resend request timed out.'));
|
|
844
|
+
},configuration.providerTimeoutMs);
|
|
1157
845
|
const startedAt=Date.now();
|
|
1158
846
|
observe({
|
|
1159
847
|
type:'mail.provider.started',
|
|
1160
848
|
appId:configuration.appId,
|
|
849
|
+
idempotencyKey,
|
|
850
|
+
providerRequest:JSON.parse(delivery.providerBody),
|
|
851
|
+
report:delivery.report,
|
|
1161
852
|
requestId
|
|
1162
853
|
});
|
|
1163
854
|
let response;
|
|
@@ -1176,65 +867,75 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
1176
867
|
referrerPolicy:'no-referrer',
|
|
1177
868
|
signal:controller.signal
|
|
1178
869
|
}),controller.signal);
|
|
1179
|
-
}catch{
|
|
1180
|
-
return ambiguousResult(
|
|
870
|
+
}catch(error){
|
|
871
|
+
return completeAttempt(ambiguousResult(
|
|
1181
872
|
timedOut?'resend_timeout':'resend_transport_uncertain',
|
|
1182
|
-
configuration.retryableDelayMs
|
|
1183
|
-
|
|
873
|
+
configuration.retryableDelayMs,
|
|
874
|
+
0,
|
|
875
|
+
completeErrorDetails(error)
|
|
876
|
+
));
|
|
1184
877
|
}
|
|
1185
878
|
const statusCode=Number(response?.status);
|
|
1186
879
|
if(!Number.isSafeInteger(statusCode)||statusCode<100||statusCode>599){
|
|
1187
|
-
return ambiguousResult(
|
|
880
|
+
return completeAttempt(ambiguousResult(
|
|
881
|
+
'resend_invalid_response',
|
|
882
|
+
configuration.retryableDelayMs,
|
|
883
|
+
0,
|
|
884
|
+
{status:response?.status??null}
|
|
885
|
+
));
|
|
1188
886
|
}
|
|
1189
887
|
let text='';
|
|
1190
888
|
try{
|
|
1191
|
-
text=await readProviderBody(
|
|
1192
|
-
response,
|
|
1193
|
-
configuration.maxProviderResponseBytes,
|
|
1194
|
-
controller.signal
|
|
1195
|
-
);
|
|
889
|
+
text=await readProviderBody(response,controller.signal);
|
|
1196
890
|
}catch(error){
|
|
1197
891
|
if(statusCode>=200&&statusCode<300||controller.signal.aborted){
|
|
1198
|
-
return ambiguousResult(
|
|
892
|
+
return completeAttempt(ambiguousResult(
|
|
1199
893
|
error instanceof MailGatewayFault?error.code:'resend_transport_uncertain',
|
|
1200
894
|
configuration.retryableDelayMs,
|
|
1201
|
-
statusCode
|
|
1202
|
-
|
|
895
|
+
statusCode,
|
|
896
|
+
completeErrorDetails(error)
|
|
897
|
+
));
|
|
1203
898
|
}
|
|
1204
|
-
return providerRejection(
|
|
899
|
+
return completeAttempt(providerRejection(
|
|
1205
900
|
statusCode,
|
|
1206
901
|
null,
|
|
1207
902
|
parseRetryAfter(responseHeader(response,'retry-after')),
|
|
1208
903
|
configuration.retryableDelayMs
|
|
1209
|
-
);
|
|
904
|
+
));
|
|
1210
905
|
}
|
|
1211
906
|
const value=parseProviderObject(text);
|
|
1212
907
|
if(statusCode>=200&&statusCode<300){
|
|
1213
908
|
if(!value||typeof value.id!=='string'||!PROVIDER_ID_PATTERN.test(value.id)){
|
|
1214
|
-
return ambiguousResult(
|
|
909
|
+
return completeAttempt(ambiguousResult(
|
|
1215
910
|
'resend_invalid_success_response',
|
|
1216
911
|
configuration.retryableDelayMs,
|
|
1217
|
-
statusCode
|
|
1218
|
-
|
|
912
|
+
statusCode,
|
|
913
|
+
value??text
|
|
914
|
+
));
|
|
1219
915
|
}
|
|
1220
|
-
return {
|
|
916
|
+
return completeAttempt({
|
|
1221
917
|
kind:'accepted',
|
|
1222
918
|
providerId:value.id,
|
|
919
|
+
providerResponse:value,
|
|
1223
920
|
providerStatus:statusCode
|
|
1224
|
-
};
|
|
921
|
+
});
|
|
1225
922
|
}
|
|
1226
|
-
return providerRejection(
|
|
923
|
+
return completeAttempt(providerRejection(
|
|
1227
924
|
statusCode,
|
|
1228
925
|
value,
|
|
1229
926
|
parseRetryAfter(responseHeader(response,'retry-after')),
|
|
1230
927
|
configuration.retryableDelayMs
|
|
1231
|
-
);
|
|
928
|
+
));
|
|
1232
929
|
}finally{
|
|
1233
|
-
clearTimeout(timeout);
|
|
930
|
+
if(timeout!==null) clearTimeout(timeout);
|
|
1234
931
|
signal?.removeEventListener('abort',forwardAbort);
|
|
1235
932
|
observe({
|
|
1236
933
|
type:'mail.provider.completed',
|
|
1237
934
|
appId:configuration.appId,
|
|
935
|
+
idempotencyKey,
|
|
936
|
+
outcome,
|
|
937
|
+
providerRequest:JSON.parse(delivery.providerBody),
|
|
938
|
+
report:delivery.report,
|
|
1238
939
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
1239
940
|
requestId,
|
|
1240
941
|
providerStatus:Number.isSafeInteger(Number(response?.status))?Number(response.status):0
|
|
@@ -1247,20 +948,7 @@ function normalizeDirectSendOptions(options){
|
|
|
1247
948
|
throw configurationError('Mail send options must be an object.');
|
|
1248
949
|
}
|
|
1249
950
|
if(typeof options.reportKey!=='string'||!IDEMPOTENCY_KEY_PATTERN.test(options.reportKey)){
|
|
1250
|
-
throw configurationError('reportKey must contain
|
|
1251
|
-
}
|
|
1252
|
-
const maxMessageBytes=boundedInteger(
|
|
1253
|
-
options.maxMessageBytes,
|
|
1254
|
-
DEFAULT_MAX_MESSAGE_BYTES,
|
|
1255
|
-
{label:'maxMessageBytes',min:1,max:DEFAULT_MAX_MESSAGE_BYTES}
|
|
1256
|
-
);
|
|
1257
|
-
const maxRequestBytes=boundedInteger(
|
|
1258
|
-
options.maxRequestBytes,
|
|
1259
|
-
DEFAULT_MAX_REQUEST_BYTES,
|
|
1260
|
-
{label:'maxRequestBytes',min:256,max:64*1024*1024}
|
|
1261
|
-
);
|
|
1262
|
-
if(maxRequestBytes<maxMessageBytes+256){
|
|
1263
|
-
throw configurationError('maxRequestBytes must leave at least 256 bytes beyond maxMessageBytes.');
|
|
951
|
+
throw configurationError('reportKey must contain safe identifier characters.');
|
|
1264
952
|
}
|
|
1265
953
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
1266
954
|
if(typeof fetchImpl!=='function'){
|
|
@@ -1272,7 +960,7 @@ function normalizeDirectSendOptions(options){
|
|
|
1272
960
|
if(options.requestIdFactory!==undefined&&typeof options.requestIdFactory!=='function'){
|
|
1273
961
|
throw configurationError('requestIdFactory must be a function when supplied.');
|
|
1274
962
|
}
|
|
1275
|
-
return
|
|
963
|
+
return {
|
|
1276
964
|
allowAnyRecipient:true,
|
|
1277
965
|
allowedRecipients:null,
|
|
1278
966
|
apiKey:validateApiKey(options.apiKey),
|
|
@@ -1280,38 +968,23 @@ function normalizeDirectSendOptions(options){
|
|
|
1280
968
|
errorRecipients:[],
|
|
1281
969
|
fetchImpl,
|
|
1282
970
|
from:validateFrom(options.from),
|
|
1283
|
-
|
|
1284
|
-
maxProviderResponseBytes:boundedInteger(
|
|
1285
|
-
options.maxProviderResponseBytes,
|
|
1286
|
-
DEFAULT_MAX_PROVIDER_RESPONSE_BYTES,
|
|
1287
|
-
{label:'maxProviderResponseBytes',min:64,max:1024*1024}
|
|
1288
|
-
),
|
|
1289
|
-
maxRequestBytes,
|
|
1290
|
-
observerDrainTimeoutMs:boundedInteger(
|
|
1291
|
-
options.observerDrainTimeoutMs,
|
|
1292
|
-
1_000,
|
|
1293
|
-
{label:'observerDrainTimeoutMs',min:1,max:10_000}
|
|
1294
|
-
),
|
|
1295
|
-
providerTimeoutMs:boundedInteger(
|
|
1296
|
-
options.providerTimeoutMs,
|
|
1297
|
-
120_000,
|
|
1298
|
-
{label:'providerTimeoutMs',min:100,max:600_000}
|
|
1299
|
-
),
|
|
971
|
+
providerTimeoutMs:optionalPositiveInteger(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
1300
972
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
1301
|
-
retryableDelayMs:
|
|
973
|
+
retryableDelayMs:positiveInteger(
|
|
1302
974
|
options.retryableDelayMs,
|
|
1303
975
|
1_000,
|
|
1304
|
-
{label:'retryableDelayMs'
|
|
976
|
+
{label:'retryableDelayMs'}
|
|
1305
977
|
),
|
|
1306
978
|
signal:validateSignal(options.signal),
|
|
1307
979
|
report:options.report,
|
|
1308
980
|
reportKey:options.reportKey,
|
|
1309
981
|
onEvent:options.onEvent
|
|
1310
|
-
}
|
|
982
|
+
};
|
|
1311
983
|
}
|
|
1312
984
|
|
|
1313
|
-
function directSendResult(result,{
|
|
985
|
+
function directSendResult(result,{delivery,requestId}){
|
|
1314
986
|
const common={
|
|
987
|
+
...result,
|
|
1315
988
|
provider:'resend',
|
|
1316
989
|
status:result.kind==='accepted'
|
|
1317
990
|
?'accepted'
|
|
@@ -1321,42 +994,46 @@ function directSendResult(result,{recipientCount,requestId}){
|
|
|
1321
994
|
:result.kind,
|
|
1322
995
|
requestId,
|
|
1323
996
|
providerStatus:result.providerStatus,
|
|
1324
|
-
|
|
997
|
+
providerRequest:JSON.parse(delivery.providerBody),
|
|
998
|
+
report:delivery.report,
|
|
999
|
+
recipientCount:delivery.recipientCount
|
|
1325
1000
|
};
|
|
1326
1001
|
if(result.kind==='accepted'){
|
|
1327
|
-
return
|
|
1002
|
+
return {...common,providerId:result.providerId};
|
|
1328
1003
|
}
|
|
1329
1004
|
if(result.kind==='ambiguous'){
|
|
1330
|
-
return
|
|
1005
|
+
return {
|
|
1331
1006
|
...common,
|
|
1332
1007
|
code:result.code,
|
|
1008
|
+
details:result.details,
|
|
1333
1009
|
...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{}),
|
|
1334
1010
|
retryable:true,
|
|
1335
1011
|
uncertain:true
|
|
1336
|
-
}
|
|
1012
|
+
};
|
|
1337
1013
|
}
|
|
1338
|
-
return
|
|
1014
|
+
return {
|
|
1339
1015
|
...common,
|
|
1340
1016
|
code:result.fault.code,
|
|
1017
|
+
details:result.fault.details,
|
|
1018
|
+
message:result.fault.message,
|
|
1341
1019
|
...(result.fault.retryAfterMs?{retryAfterMs:result.fault.retryAfterMs}:{}),
|
|
1342
1020
|
retryable:result.fault.retryable,
|
|
1343
1021
|
uncertain:false
|
|
1344
|
-
}
|
|
1022
|
+
};
|
|
1345
1023
|
}
|
|
1346
1024
|
|
|
1347
1025
|
export async function sendResendMail(options={}){
|
|
1348
1026
|
const configuration=normalizeDirectSendOptions(options);
|
|
1349
1027
|
const delivery=normalizeReport(configuration.report,configuration);
|
|
1350
1028
|
if(configuration.signal?.aborted){
|
|
1351
|
-
const error=new Error('Mail send cancelled before provider attempt.'
|
|
1029
|
+
const error=new Error('Mail send cancelled before provider attempt.',{
|
|
1030
|
+
cause:configuration.signal.reason
|
|
1031
|
+
});
|
|
1352
1032
|
error.code='ARCANE_CANCELLED';
|
|
1353
1033
|
throw error;
|
|
1354
1034
|
}
|
|
1355
1035
|
const requestId=createRequestId(configuration.requestIdFactory);
|
|
1356
|
-
const observer=createObserver(
|
|
1357
|
-
configuration.onEvent,
|
|
1358
|
-
configuration.observerDrainTimeoutMs
|
|
1359
|
-
);
|
|
1036
|
+
const observer=createObserver(configuration.onEvent);
|
|
1360
1037
|
try{
|
|
1361
1038
|
const result=await performResendAttempt(
|
|
1362
1039
|
configuration,
|
|
@@ -1367,7 +1044,7 @@ export async function sendResendMail(options={}){
|
|
|
1367
1044
|
observer.observe
|
|
1368
1045
|
);
|
|
1369
1046
|
return directSendResult(result,{
|
|
1370
|
-
|
|
1047
|
+
delivery,
|
|
1371
1048
|
requestId
|
|
1372
1049
|
});
|
|
1373
1050
|
}finally{
|
|
@@ -1382,7 +1059,8 @@ function sendProviderResult(response,result,{origin,requestId,recipientCount}){
|
|
|
1382
1059
|
status:'accepted',
|
|
1383
1060
|
accepted:recipientCount,
|
|
1384
1061
|
rejected:0,
|
|
1385
|
-
providerId:result.providerId
|
|
1062
|
+
providerId:result.providerId,
|
|
1063
|
+
providerResponse:result.providerResponse
|
|
1386
1064
|
},{origin});
|
|
1387
1065
|
}
|
|
1388
1066
|
if(result.kind==='ambiguous'){
|
|
@@ -1391,6 +1069,7 @@ function sendProviderResult(response,result,{origin,requestId,recipientCount}){
|
|
|
1391
1069
|
status:'delivery_uncertain',
|
|
1392
1070
|
accepted:0,
|
|
1393
1071
|
rejected:0,
|
|
1072
|
+
details:result.details,
|
|
1394
1073
|
...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{})
|
|
1395
1074
|
},{origin,retryAfterMs:result.retryAfterMs});
|
|
1396
1075
|
}
|
|
@@ -1401,26 +1080,7 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1401
1080
|
const configuration=normalizeConfiguration(options);
|
|
1402
1081
|
const ownerController=new AbortController();
|
|
1403
1082
|
const activeRequests=new Set();
|
|
1404
|
-
const observer=createObserver(
|
|
1405
|
-
configuration.onEvent,
|
|
1406
|
-
configuration.observerDrainTimeoutMs
|
|
1407
|
-
);
|
|
1408
|
-
const bodyScheduler=createScheduler({
|
|
1409
|
-
code:'mail_body_backpressure',
|
|
1410
|
-
concurrency:configuration.maxConcurrentBodyReads,
|
|
1411
|
-
maxQueued:configuration.maxQueuedBodyReads,
|
|
1412
|
-
queueTimeoutMs:configuration.bodyQueueTimeoutMs,
|
|
1413
|
-
retryAfterMs:250
|
|
1414
|
-
});
|
|
1415
|
-
const sendScheduler=createScheduler({
|
|
1416
|
-
code:'mail_send_backpressure',
|
|
1417
|
-
concurrency:configuration.maxConcurrentSends,
|
|
1418
|
-
maxQueued:configuration.maxQueuedSends,
|
|
1419
|
-
maxQueuedWeight:configuration.maxQueuedMessageBytes,
|
|
1420
|
-
queueTimeoutMs:configuration.sendQueueTimeoutMs,
|
|
1421
|
-
retryAfterMs:configuration.retryableDelayMs,
|
|
1422
|
-
weightCode:'mail_send_byte_backpressure'
|
|
1423
|
-
});
|
|
1083
|
+
const observer=createObserver(configuration.onEvent);
|
|
1424
1084
|
let closePromise=null;
|
|
1425
1085
|
|
|
1426
1086
|
function forwardOwnerAbort(){
|
|
@@ -1435,8 +1095,12 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1435
1095
|
const requestId=createRequestId(configuration.requestIdFactory);
|
|
1436
1096
|
const startedAt=Date.now();
|
|
1437
1097
|
const requestController=new AbortController();
|
|
1098
|
+
let delivery=null;
|
|
1099
|
+
let idempotencyKey=null;
|
|
1438
1100
|
let origin='';
|
|
1439
1101
|
let providerAttempted=false;
|
|
1102
|
+
let result=null;
|
|
1103
|
+
let serialized=null;
|
|
1440
1104
|
|
|
1441
1105
|
function abortFromOwner(){
|
|
1442
1106
|
requestController.abort(ownerController.signal.reason);
|
|
@@ -1493,7 +1157,7 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1493
1157
|
throw new MailGatewayFault('mail_app_not_allowed',{statusCode:403});
|
|
1494
1158
|
}
|
|
1495
1159
|
authenticateLocalCaller(request,configuration);
|
|
1496
|
-
|
|
1160
|
+
idempotencyKey=singleHeader(request,'idempotency-key');
|
|
1497
1161
|
if(!IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)){
|
|
1498
1162
|
throw new MailGatewayFault('invalid_idempotency_key',{statusCode:400});
|
|
1499
1163
|
}
|
|
@@ -1501,34 +1165,19 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1501
1165
|
if(!JSON_CONTENT_TYPE_PATTERN.test(contentType)){
|
|
1502
1166
|
throw new MailGatewayFault('mail_unsupported_content_type',{statusCode:415});
|
|
1503
1167
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
function makeSingleResendAttempt(){
|
|
1518
|
-
providerAttempted=true;
|
|
1519
|
-
return performResendAttempt(
|
|
1520
|
-
configuration,
|
|
1521
|
-
delivery,
|
|
1522
|
-
idempotencyKey,
|
|
1523
|
-
requestController.signal,
|
|
1524
|
-
requestId,
|
|
1525
|
-
observer.observe
|
|
1526
|
-
);
|
|
1527
|
-
},
|
|
1528
|
-
{
|
|
1529
|
-
signal:requestController.signal,
|
|
1530
|
-
weight:delivery.providerBodyBytes
|
|
1531
|
-
}
|
|
1168
|
+
serialized=await readRequestBody(request,{
|
|
1169
|
+
signal:requestController.signal,
|
|
1170
|
+
timeoutMs:configuration.bodyTimeoutMs
|
|
1171
|
+
});
|
|
1172
|
+
delivery=parseReport(serialized,configuration);
|
|
1173
|
+
providerAttempted=true;
|
|
1174
|
+
result=await performResendAttempt(
|
|
1175
|
+
configuration,
|
|
1176
|
+
delivery,
|
|
1177
|
+
idempotencyKey,
|
|
1178
|
+
requestController.signal,
|
|
1179
|
+
requestId,
|
|
1180
|
+
observer.observe
|
|
1532
1181
|
);
|
|
1533
1182
|
sendProviderResult(response,result,{
|
|
1534
1183
|
origin,
|
|
@@ -1539,8 +1188,11 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1539
1188
|
type:'mail.request.completed',
|
|
1540
1189
|
appId:configuration.appId,
|
|
1541
1190
|
classification:result.kind,
|
|
1191
|
+
delivery,
|
|
1542
1192
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
1193
|
+
idempotencyKey,
|
|
1543
1194
|
providerAttempted,
|
|
1195
|
+
result,
|
|
1544
1196
|
requestId
|
|
1545
1197
|
});
|
|
1546
1198
|
}catch(error){
|
|
@@ -1553,7 +1205,18 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1553
1205
|
type:'mail.request.completed',
|
|
1554
1206
|
appId:configuration.appId,
|
|
1555
1207
|
classification:fault.uncertain?'ambiguous':fault.retryable?'retryable':'permanent',
|
|
1208
|
+
delivery,
|
|
1556
1209
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
1210
|
+
fault:{
|
|
1211
|
+
code:fault.code,
|
|
1212
|
+
details:fault.details,
|
|
1213
|
+
message:fault.message,
|
|
1214
|
+
retryAfterMs:fault.retryAfterMs,
|
|
1215
|
+
retryable:fault.retryable,
|
|
1216
|
+
statusCode:fault.statusCode,
|
|
1217
|
+
uncertain:fault.uncertain
|
|
1218
|
+
},
|
|
1219
|
+
idempotencyKey,
|
|
1557
1220
|
providerAttempted,
|
|
1558
1221
|
requestId
|
|
1559
1222
|
});
|
|
@@ -1589,13 +1252,7 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1589
1252
|
if(!ownerController.signal.aborted){
|
|
1590
1253
|
ownerController.abort(new Error('Mail server closed.'));
|
|
1591
1254
|
}
|
|
1592
|
-
|
|
1593
|
-
sendScheduler.close();
|
|
1594
|
-
await Promise.all([
|
|
1595
|
-
bodyScheduler.idle(),
|
|
1596
|
-
sendScheduler.idle(),
|
|
1597
|
-
Promise.allSettled([...activeRequests])
|
|
1598
|
-
]);
|
|
1255
|
+
await Promise.allSettled([...activeRequests]);
|
|
1599
1256
|
await observer.drain();
|
|
1600
1257
|
}
|
|
1601
1258
|
|
|
@@ -1606,14 +1263,14 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1606
1263
|
return closePromise;
|
|
1607
1264
|
}
|
|
1608
1265
|
|
|
1609
|
-
return
|
|
1266
|
+
return {
|
|
1610
1267
|
appId:configuration.appId,
|
|
1611
1268
|
callerAuthentication:configuration.callerAuthentication,
|
|
1612
1269
|
close,
|
|
1613
1270
|
handle,
|
|
1614
1271
|
path:RESEND_MAIL_PATH,
|
|
1615
1272
|
protocol:RESEND_MAIL_SERVER_PROTOCOL
|
|
1616
|
-
}
|
|
1273
|
+
};
|
|
1617
1274
|
}
|
|
1618
1275
|
|
|
1619
1276
|
function listen(server,{host,port}){
|
|
@@ -1659,20 +1316,7 @@ export async function startResendMailServer(options={}){
|
|
|
1659
1316
|
throw configuration.signal.reason??new Error('Mail server start was cancelled.');
|
|
1660
1317
|
}
|
|
1661
1318
|
const requestHandler=createResendMailRequestHandler(options);
|
|
1662
|
-
const server=http.createServer(
|
|
1663
|
-
headersTimeout:5_000,
|
|
1664
|
-
keepAlive:true,
|
|
1665
|
-
maxHeaderSize:16_384,
|
|
1666
|
-
requestTimeout:configuration.bodyQueueTimeoutMs+configuration.bodyTimeoutMs+1_000
|
|
1667
|
-
},requestHandler.handle);
|
|
1668
|
-
server.keepAliveTimeout=5_000;
|
|
1669
|
-
server.maxConnections=configuration.maxConcurrentBodyReads
|
|
1670
|
-
+configuration.maxQueuedBodyReads
|
|
1671
|
-
+configuration.maxConcurrentSends
|
|
1672
|
-
+configuration.maxQueuedSends
|
|
1673
|
-
+16;
|
|
1674
|
-
server.maxHeadersCount=32;
|
|
1675
|
-
server.maxRequestsPerSocket=20;
|
|
1319
|
+
const server=http.createServer(requestHandler.handle);
|
|
1676
1320
|
server.on('clientError',function rejectMalformedClient(error,socket){
|
|
1677
1321
|
if(!socket.writable){
|
|
1678
1322
|
return;
|
|
@@ -1750,7 +1394,7 @@ export async function startResendMailServer(options={}){
|
|
|
1750
1394
|
closeFromSignal();
|
|
1751
1395
|
}
|
|
1752
1396
|
|
|
1753
|
-
return
|
|
1397
|
+
return {
|
|
1754
1398
|
appId:configuration.appId,
|
|
1755
1399
|
callerAuthentication:configuration.callerAuthentication,
|
|
1756
1400
|
close,
|
|
@@ -1765,5 +1409,5 @@ export async function startResendMailServer(options={}){
|
|
|
1765
1409
|
server,
|
|
1766
1410
|
target:'mail',
|
|
1767
1411
|
url:`${origin}${RESEND_MAIL_PATH}`
|
|
1768
|
-
}
|
|
1412
|
+
};
|
|
1769
1413
|
}
|