arcane-os 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -0,0 +1,1769 @@
1
+ import {createHash,randomUUID,timingSafeEqual} from 'node:crypto';
2
+ import http from 'node:http';
3
+
4
+ export const RESEND_MAIL_SERVER_PROTOCOL='arcane-resend-mail-gateway/1';
5
+ export const RESEND_MAIL_PATH='/v1/mail';
6
+
7
+ const RESEND_EMAIL_ENDPOINT='https://api.resend.com/emails';
8
+ const APP_ID_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,62})$/u;
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._:-]{8,128}$/u;
11
+ const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]{1,256}$/u;
12
+ const PROVIDER_CODE_PATTERN=/^[a-z0-9_]{1,80}$/u;
13
+ const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]{8,128}$/u;
14
+ const JSON_CONTENT_TYPE_PATTERN=/^application\/json(?:\s*;\s*charset\s*=\s*"?utf-8"?)?$/iu;
15
+ const HEADER_NAME_PATTERN=/^[!#$%&'*+.^_`|~0-9a-z-]+$/iu;
16
+ const MAIL_TYPES=new Set(['error','report','crisis_detected']);
17
+ const REPORT_KEYS=new Set(['html','subject','text','to','type']);
18
+ const PREFLIGHT_HEADERS=new Set([
19
+ 'content-type','idempotency-key','x-mail-app','x-mail-key'
20
+ ]);
21
+ const PERMANENT_RATE_CODES=new Set(['daily_quota_exceeded','monthly_quota_exceeded']);
22
+ 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
+
29
+ class MailGatewayFault extends Error {
30
+ constructor(code,{retryable=false,retryAfterMs=0,statusCode=400,uncertain=false}={}){
31
+ super(code);
32
+ this.name='MailGatewayFault';
33
+ this.code=code;
34
+ this.retryable=Boolean(retryable);
35
+ this.retryAfterMs=boundedRetryAfter(retryAfterMs);
36
+ this.statusCode=statusCode;
37
+ this.uncertain=Boolean(uncertain);
38
+ }
39
+ }
40
+
41
+ function configurationError(message){
42
+ const error=new Error(message);
43
+ error.code='ARCANE_MAIL_CONFIG_INVALID';
44
+ return error;
45
+ }
46
+
47
+ function boundedInteger(value,fallback,{label,min,max}){
48
+ const resolved=value===undefined?fallback:value;
49
+ if(!Number.isSafeInteger(resolved)||resolved<min||resolved>max){
50
+ throw configurationError(`${label} must be an integer between ${min} and ${max}.`);
51
+ }
52
+ return resolved;
53
+ }
54
+
55
+ function boundedRetryAfter(value){
56
+ return Number.isSafeInteger(value)&&value>0
57
+ ? Math.min(MAX_RETRY_AFTER_MS,value)
58
+ : 0;
59
+ }
60
+
61
+ function validateSignal(signal){
62
+ if(signal!==undefined&&!(signal instanceof AbortSignal)){
63
+ throw new TypeError('Mail server signal must be an AbortSignal.');
64
+ }
65
+ return signal;
66
+ }
67
+
68
+ function validateApiKey(value){
69
+ if(typeof value!=='string'||value.length<1||value.length>4096
70
+ ||!/^[\x21-\x7e]+$/u.test(value)){
71
+ throw configurationError('Resend API key must be a nonempty printable ASCII string.');
72
+ }
73
+ return value;
74
+ }
75
+
76
+ function validateAppId(value){
77
+ if(typeof value!=='string'||!APP_ID_PATTERN.test(value)){
78
+ throw configurationError('Mail application identity is invalid.');
79
+ }
80
+ return value;
81
+ }
82
+
83
+ function appKeyDigest(value){
84
+ return createHash('sha256').update(value,'utf8').digest();
85
+ }
86
+
87
+ function normalizeCallerAuthentication(options){
88
+ const allowUnauthenticatedCaller=options.allowUnauthenticatedCaller??false;
89
+ if(typeof allowUnauthenticatedCaller!=='boolean'){
90
+ throw configurationError('allowUnauthenticatedCaller must be a boolean.');
91
+ }
92
+ if(allowUnauthenticatedCaller){
93
+ if(options.appKey!==undefined){
94
+ throw configurationError(
95
+ 'appKey must be omitted when allowUnauthenticatedCaller is true.'
96
+ );
97
+ }
98
+ return Object.freeze({
99
+ appKeyDigest:null,
100
+ callerAuthentication:'origin-app-id-only'
101
+ });
102
+ }
103
+ if(typeof options.appKey!=='string'||options.appKey.length<16
104
+ ||options.appKey.length>512||!/^[\x21-\x7e]+$/u.test(options.appKey)){
105
+ throw configurationError(
106
+ 'appKey must contain 16-512 printable ASCII characters unless unauthenticated caller mode is explicitly enabled.'
107
+ );
108
+ }
109
+ return Object.freeze({
110
+ appKeyDigest:appKeyDigest(options.appKey),
111
+ callerAuthentication:'app-key'
112
+ });
113
+ }
114
+
115
+ function normalizedEmail(value,label){
116
+ if(typeof value!=='string'){
117
+ throw configurationError(`${label} must be an email address.`);
118
+ }
119
+ const normalized=value.trim().toLowerCase();
120
+ if(normalized.length<3||normalized.length>254||!EMAIL_PATTERN.test(normalized)){
121
+ throw configurationError(`${label} must be a valid email address.`);
122
+ }
123
+ return normalized;
124
+ }
125
+
126
+ function validateFrom(value){
127
+ if(typeof value!=='string'||value!==value.trim()||value.length<3||value.length>320
128
+ ||/[\u0000-\u001f\u007f]/u.test(value)){
129
+ throw configurationError('Mail sender is invalid.');
130
+ }
131
+ if(EMAIL_PATTERN.test(value)){
132
+ return value.toLowerCase();
133
+ }
134
+ const match=/^([^<>]{1,64}) <([^<>]+)>$/u.exec(value);
135
+ if(!match||!match[1].trim()){
136
+ throw configurationError('Mail sender must be an email address or Name <email> value.');
137
+ }
138
+ const address=normalizedEmail(match[2],'Mail sender');
139
+ return `${match[1]} <${address}>`;
140
+ }
141
+
142
+ function normalizeEmailList(value,label,{allowEmpty=false}={}){
143
+ if(!Array.isArray(value)||value.length>50||(!allowEmpty&&value.length===0)){
144
+ throw configurationError(`${label} must contain ${allowEmpty?'zero to ':'one to '}50 addresses.`);
145
+ }
146
+ const result=[];
147
+ const seen=new Set();
148
+ for(const entry of value){
149
+ 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
+ result.push(address);
155
+ }
156
+ return result;
157
+ }
158
+
159
+ function normalizeOrigin(value){
160
+ if(typeof value!=='string'||!value.trim()){
161
+ throw configurationError('Every allowed mail origin must be a URL origin.');
162
+ }
163
+ let url;
164
+ try{
165
+ url=new URL(value.trim());
166
+ }catch{
167
+ throw configurationError('Every allowed mail origin must be a valid URL origin.');
168
+ }
169
+ if(!['http:','https:'].includes(url.protocol)||url.username||url.password
170
+ ||url.pathname!=='/'||url.search||url.hash){
171
+ throw configurationError('Every allowed mail origin must be an HTTP or HTTPS origin without credentials, path, query, or fragment.');
172
+ }
173
+ return url.origin;
174
+ }
175
+
176
+ function normalizeOrigins(value){
177
+ if(!Array.isArray(value)||value.length===0||value.length>64){
178
+ throw configurationError('allowedOrigins must contain one to 64 exact origins.');
179
+ }
180
+ const origins=[];
181
+ const seen=new Set();
182
+ for(const entry of value){
183
+ const origin=normalizeOrigin(entry);
184
+ if(seen.has(origin)){
185
+ throw configurationError('allowedOrigins must not contain duplicate origins.');
186
+ }
187
+ seen.add(origin);
188
+ origins.push(origin);
189
+ }
190
+ return new Set(origins);
191
+ }
192
+
193
+ function validateLoopbackHost(value){
194
+ if(value!=='127.0.0.1'&&value!=='::1'){
195
+ throw configurationError('Mail server host must be the numeric loopback address 127.0.0.1 or ::1.');
196
+ }
197
+ return value;
198
+ }
199
+
200
+ function normalizeConfiguration(options={}){
201
+ if(!options||typeof options!=='object'||Array.isArray(options)){
202
+ throw configurationError('Mail server options must be an object.');
203
+ }
204
+ const callerAuthentication=normalizeCallerAuthentication(options);
205
+ const recipientAllowlist=normalizeEmailList(
206
+ options.recipientAllowlist,
207
+ 'recipientAllowlist'
208
+ );
209
+ const errorRecipients=normalizeEmailList(
210
+ options.errorRecipients??[],
211
+ 'errorRecipients',
212
+ {allowEmpty:true}
213
+ );
214
+ const allowedRecipients=new Set(recipientAllowlist);
215
+ if(errorRecipients.some(function errorRecipientIsNotAllowed(address){
216
+ return !allowedRecipients.has(address);
217
+ })){
218
+ throw configurationError('Every error recipient must also be in recipientAllowlist.');
219
+ }
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
+ const fetchImpl=options.fetchImpl??globalThis.fetch;
234
+ if(typeof fetchImpl!=='function'){
235
+ throw configurationError('A fetch implementation is required for Resend delivery.');
236
+ }
237
+ if(options.onEvent!==undefined&&typeof options.onEvent!=='function'){
238
+ throw configurationError('onEvent must be a function when supplied.');
239
+ }
240
+ if(options.requestIdFactory!==undefined&&typeof options.requestIdFactory!=='function'){
241
+ throw configurationError('requestIdFactory must be a function when supplied.');
242
+ }
243
+ return Object.freeze({
244
+ allowAnyRecipient:false,
245
+ allowedOrigins:normalizeOrigins(options.allowedOrigins),
246
+ allowedRecipients,
247
+ apiKey:validateApiKey(options.apiKey),
248
+ appKeyDigest:callerAuthentication.appKeyDigest,
249
+ appId:validateAppId(options.appId),
250
+ bodyQueueTimeoutMs:boundedInteger(
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
+ ),
260
+ errorRecipients,
261
+ fetchImpl,
262
+ from:validateFrom(options.from),
263
+ host:validateLoopbackHost(options.host??'127.0.0.1'),
264
+ 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
+ onEvent:options.onEvent,
298
+ observerDrainTimeoutMs:boundedInteger(
299
+ options.observerDrainTimeoutMs,
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
+ ),
309
+ requestIdFactory:options.requestIdFactory??randomUUID,
310
+ retryableDelayMs:boundedInteger(
311
+ options.retryableDelayMs,
312
+ 1_000,
313
+ {label:'retryableDelayMs',min:1,max:60_000}
314
+ ),
315
+ sendQueueTimeoutMs:boundedInteger(
316
+ options.sendQueueTimeoutMs,
317
+ 10_000,
318
+ {label:'sendQueueTimeoutMs',min:1,max:120_000}
319
+ ),
320
+ signal:validateSignal(options.signal)
321
+ });
322
+ }
323
+
324
+ function createRequestId(factory){
325
+ try{
326
+ const candidate=factory();
327
+ if(typeof candidate==='string'&&REQUEST_ID_PATTERN.test(candidate)){
328
+ return candidate;
329
+ }
330
+ }catch{
331
+ // A diagnostic identifier must never prevent a bounded error response.
332
+ }
333
+ return randomUUID();
334
+ }
335
+
336
+ function isNumericLoopback(value){
337
+ return value==='127.0.0.1'||value==='::1'||value==='::ffff:127.0.0.1';
338
+ }
339
+
340
+ function headerValues(request,name){
341
+ const distinct=request.headersDistinct?.[name];
342
+ if(Array.isArray(distinct)){
343
+ return distinct.map(function stringifyDistinctHeader(value){return String(value);});
344
+ }
345
+ const values=[];
346
+ for(let index=0;index<(request.rawHeaders?.length??0);index+=2){
347
+ if(String(request.rawHeaders[index]).toLowerCase()===name){
348
+ values.push(String(request.rawHeaders[index+1]??''));
349
+ }
350
+ }
351
+ if(values.length>0){
352
+ return values;
353
+ }
354
+ const fallback=request.headers?.[name];
355
+ if(fallback===undefined){
356
+ return [];
357
+ }
358
+ return Array.isArray(fallback)?fallback.map(String):[String(fallback)];
359
+ }
360
+
361
+ function singleHeader(request,name,{required=true}={}){
362
+ const values=headerValues(request,name);
363
+ if(values.length===0&&!required){
364
+ return '';
365
+ }
366
+ if(values.length!==1||!values[0]){
367
+ throw new MailGatewayFault('mail_invalid_headers',{statusCode:400});
368
+ }
369
+ return values[0];
370
+ }
371
+
372
+ function validateLoopbackRequest(request){
373
+ if(!isNumericLoopback(request.socket?.remoteAddress)
374
+ ||!isNumericLoopback(request.socket?.localAddress)){
375
+ throw new MailGatewayFault('mail_loopback_required',{statusCode:421});
376
+ }
377
+ const rawHost=singleHeader(request,'host');
378
+ let parsed;
379
+ try{
380
+ parsed=new URL(`http://${rawHost}`);
381
+ }catch{
382
+ throw new MailGatewayFault('mail_invalid_host',{statusCode:421});
383
+ }
384
+ const hostname=parsed.hostname.replace(/^\[|\]$/gu,'');
385
+ const localPort=request.socket?.localPort;
386
+ const statedPort=parsed.port?Number(parsed.port):80;
387
+ const hostLiteral=hostname.includes(':')?`[${hostname}]`:hostname;
388
+ const expectedAuthority=localPort===80?hostLiteral:`${hostLiteral}:${String(localPort)}`;
389
+ if(parsed.username||parsed.password||parsed.pathname!=='/'||parsed.search||parsed.hash
390
+ ||!isNumericLoopback(hostname)||!Number.isSafeInteger(localPort)
391
+ ||statedPort!==localPort||rawHost!==expectedAuthority){
392
+ throw new MailGatewayFault('mail_invalid_host',{statusCode:421});
393
+ }
394
+ }
395
+
396
+ function allowedOrigin(request,configuration){
397
+ const origin=singleHeader(request,'origin');
398
+ if(!configuration.allowedOrigins.has(origin)){
399
+ throw new MailGatewayFault('mail_origin_not_allowed',{statusCode:403});
400
+ }
401
+ return origin;
402
+ }
403
+
404
+ function authenticateLocalCaller(request,configuration){
405
+ const values=headerValues(request,'x-mail-key');
406
+ if(configuration.callerAuthentication==='origin-app-id-only'){
407
+ if(values.length!==0){
408
+ throw new MailGatewayFault('mail_app_key_unexpected',{statusCode:403});
409
+ }
410
+ return;
411
+ }
412
+ const candidate=values.length===1?values[0]:'';
413
+ const candidateIsValid=candidate.length>=16&&candidate.length<=512
414
+ &&/^[\x21-\x7e]+$/u.test(candidate);
415
+ const digest=appKeyDigest(candidateIsValid?candidate:'');
416
+ const authenticated=timingSafeEqual(configuration.appKeyDigest,digest);
417
+ if(values.length!==1||!candidateIsValid||!authenticated){
418
+ throw new MailGatewayFault('mail_app_key_invalid',{statusCode:401});
419
+ }
420
+ }
421
+
422
+ function corsHeaders(origin,{allowPrivateNetwork=false}={}){
423
+ if(!origin){
424
+ return {};
425
+ }
426
+ return {
427
+ 'access-control-allow-headers':'Content-Type, Idempotency-Key, X-Mail-App, X-Mail-Key',
428
+ 'access-control-allow-methods':'POST, OPTIONS',
429
+ 'access-control-allow-origin':origin,
430
+ 'access-control-expose-headers':'Retry-After',
431
+ 'access-control-max-age':'600',
432
+ ...(allowPrivateNetwork?{'access-control-allow-private-network':'true'}:{}),
433
+ 'vary':'Origin'
434
+ };
435
+ }
436
+
437
+ 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
+ };
446
+ }
447
+
448
+ function writeJson(response,statusCode,value,{origin='',retryAfterMs=0}={}){
449
+ if(response.destroyed||response.writableEnded){
450
+ return false;
451
+ }
452
+ const body=JSON.stringify(value);
453
+ const headers={
454
+ ...baseResponseHeaders(origin),
455
+ 'content-length':String(Buffer.byteLength(body,'utf8')),
456
+ 'content-type':'application/json; charset=utf-8'
457
+ };
458
+ const boundedDelay=boundedRetryAfter(retryAfterMs);
459
+ if(boundedDelay){
460
+ headers['retry-after']=String(Math.max(1,Math.ceil(boundedDelay/1000)));
461
+ }
462
+ response.writeHead(statusCode,headers);
463
+ response.end(body);
464
+ return true;
465
+ }
466
+
467
+ function writePreflight(response,origin,{allowPrivateNetwork=false}={}){
468
+ if(response.destroyed||response.writableEnded){
469
+ return false;
470
+ }
471
+ response.writeHead(204,{
472
+ ...baseResponseHeaders(origin),
473
+ ...corsHeaders(origin,{allowPrivateNetwork}),
474
+ 'content-length':'0'
475
+ });
476
+ response.end();
477
+ return true;
478
+ }
479
+
480
+ function writeFault(response,requestId,fault,origin=''){
481
+ const retryAfterMs=boundedRetryAfter(fault.retryAfterMs);
482
+ return writeJson(response,fault.statusCode,{
483
+ requestId,
484
+ error:{
485
+ code:PROVIDER_CODE_PATTERN.test(fault.code)?fault.code:'mail_gateway_error',
486
+ retryable:Boolean(fault.retryable),
487
+ uncertain:Boolean(fault.uncertain),
488
+ ...(retryAfterMs?{retryAfterMs}:{})
489
+ }
490
+ },{origin,retryAfterMs});
491
+ }
492
+
493
+ function normalizeFault(error){
494
+ if(error instanceof MailGatewayFault){
495
+ return error;
496
+ }
497
+ return new MailGatewayFault('mail_gateway_error',{statusCode:500});
498
+ }
499
+
500
+ function validatePreflight(request){
501
+ if(singleHeader(request,'access-control-request-method')!=='POST'){
502
+ throw new MailGatewayFault('mail_preflight_denied',{statusCode:403});
503
+ }
504
+ const rawHeaders=singleHeader(request,'access-control-request-headers');
505
+ const requestedHeaders=rawHeaders.split(',').map(function normalizeRequestedHeader(value){
506
+ return value.trim().toLowerCase();
507
+ });
508
+ if(requestedHeaders.length===0||requestedHeaders.some(function headerIsNotAllowed(value){
509
+ return !value||!HEADER_NAME_PATTERN.test(value)||!PREFLIGHT_HEADERS.has(value);
510
+ })){
511
+ throw new MailGatewayFault('mail_preflight_denied',{statusCode:403});
512
+ }
513
+ const privateNetwork=singleHeader(
514
+ request,
515
+ 'access-control-request-private-network',
516
+ {required:false}
517
+ );
518
+ if(privateNetwork&&privateNetwork!=='true'){
519
+ throw new MailGatewayFault('mail_preflight_denied',{statusCode:403});
520
+ }
521
+ return {allowPrivateNetwork:privateNetwork==='true'};
522
+ }
523
+
524
+ function createObserver(onEvent,drainTimeoutMs){
525
+ const pending=new Set();
526
+ function observe(event){
527
+ if(!onEvent){
528
+ return;
529
+ }
530
+ let result;
531
+ try{
532
+ result=onEvent(Object.freeze({...event}));
533
+ }catch{
534
+ return;
535
+ }
536
+ if(!result||typeof result.then!=='function'){
537
+ return;
538
+ }
539
+ const task=Promise.resolve(result);
540
+ pending.add(task);
541
+ task.catch(function ignoreObserverFailure(){})
542
+ .finally(function releaseObserverTask(){pending.delete(task);});
543
+ }
544
+ async function drain(){
545
+ if(pending.size===0){
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();
555
+ }
556
+ return {drain,observe};
557
+ }
558
+
559
+ function createScheduler({concurrency,maxQueued,queueTimeoutMs,retryAfterMs,code,
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}){
767
+ return new Promise(function collectRequestBody(resolve,reject){
768
+ const chunks=[];
769
+ let byteLength=0;
770
+ let settled=false;
771
+ const timer=setTimeout(function expireRequestBody(){
772
+ finish(new MailGatewayFault('mail_body_timeout',{
773
+ retryable:true,
774
+ statusCode:408
775
+ }));
776
+ request.resume();
777
+ },timeoutMs);
778
+
779
+ function cleanup(){
780
+ clearTimeout(timer);
781
+ request.removeListener('data',onData);
782
+ request.removeListener('end',onEnd);
783
+ request.removeListener('error',onError);
784
+ request.removeListener('aborted',onAborted);
785
+ signal?.removeEventListener('abort',onSignalAbort);
786
+ }
787
+
788
+ function finish(error,value){
789
+ if(settled){
790
+ return;
791
+ }
792
+ settled=true;
793
+ cleanup();
794
+ if(error){
795
+ reject(error);
796
+ }else{
797
+ resolve(value);
798
+ }
799
+ }
800
+
801
+ function onData(chunk){
802
+ 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
+ chunks.push(bytes);
810
+ }
811
+
812
+ function onEnd(){
813
+ finish(null,Buffer.concat(chunks,byteLength).toString('utf8'));
814
+ }
815
+
816
+ function onError(){
817
+ finish(new MailGatewayFault('mail_request_stream_failed',{
818
+ retryable:true,
819
+ statusCode:400
820
+ }));
821
+ }
822
+
823
+ function onAborted(){
824
+ finish(new MailGatewayFault('mail_request_cancelled',{
825
+ retryable:true,
826
+ statusCode:408
827
+ }));
828
+ }
829
+
830
+ function onSignalAbort(){
831
+ finish(new MailGatewayFault('mail_request_cancelled',{
832
+ retryable:true,
833
+ statusCode:408
834
+ }));
835
+ request.resume();
836
+ }
837
+
838
+ request.on('data',onData);
839
+ request.once('end',onEnd);
840
+ request.once('error',onError);
841
+ request.once('aborted',onAborted);
842
+ signal?.addEventListener('abort',onSignalAbort,{once:true});
843
+ if(signal?.aborted){
844
+ onSignalAbort();
845
+ }
846
+ });
847
+ }
848
+
849
+ function utf8Bytes(value){
850
+ return Buffer.byteLength(value,'utf8');
851
+ }
852
+
853
+ function normalizedReportEmail(value){
854
+ if(typeof value!=='string'){
855
+ throw new MailGatewayFault('mail_invalid_recipient',{statusCode:422});
856
+ }
857
+ const address=value.trim().toLowerCase();
858
+ if(address.length<3||address.length>254||!EMAIL_PATTERN.test(address)){
859
+ throw new MailGatewayFault('mail_invalid_recipient',{statusCode:422});
860
+ }
861
+ return address;
862
+ }
863
+
864
+ function normalizeReportRecipients(report,configuration){
865
+ if(!Array.isArray(report.to)||report.to.length>50){
866
+ throw new MailGatewayFault('mail_invalid_recipients',{statusCode:422});
867
+ }
868
+ const recipients=[];
869
+ const seen=new Set();
870
+ for(const value of report.to){
871
+ const address=normalizedReportEmail(value);
872
+ if(seen.has(address)){
873
+ throw new MailGatewayFault('mail_duplicate_recipient',{statusCode:422});
874
+ }
875
+ if(!configuration.allowAnyRecipient&&!configuration.allowedRecipients.has(address)){
876
+ throw new MailGatewayFault('mail_recipient_not_allowed',{statusCode:403});
877
+ }
878
+ seen.add(address);
879
+ recipients.push(address);
880
+ }
881
+ if(recipients.length===0&&report.type==='error'){
882
+ recipients.push(...configuration.errorRecipients);
883
+ }
884
+ if(recipients.length===0){
885
+ throw new MailGatewayFault('mail_recipients_required',{statusCode:422});
886
+ }
887
+ return recipients;
888
+ }
889
+
890
+ function normalizeReport(value,configuration){
891
+ if(!value||typeof value!=='object'||Array.isArray(value)){
892
+ throw new MailGatewayFault('mail_invalid_report',{statusCode:422});
893
+ }
894
+ const keys=Object.keys(value);
895
+ if(keys.some(function reportKeyIsUnknown(key){return !REPORT_KEYS.has(key);})
896
+ ||!Object.hasOwn(value,'subject')||!Object.hasOwn(value,'to')
897
+ ||!Object.hasOwn(value,'type')){
898
+ throw new MailGatewayFault('mail_invalid_report_shape',{statusCode:422});
899
+ }
900
+ if(typeof value.type!=='string'||!MAIL_TYPES.has(value.type)){
901
+ throw new MailGatewayFault('mail_invalid_type',{statusCode:422});
902
+ }
903
+ if(typeof value.subject!=='string'||value.subject!==value.subject.trim()
904
+ ||value.subject.length<1||value.subject.length>160
905
+ ||/[\u0000-\u001f\u007f]/u.test(value.subject)){
906
+ throw new MailGatewayFault('mail_invalid_subject',{statusCode:422});
907
+ }
908
+ const hasText=Object.hasOwn(value,'text');
909
+ const hasHtml=Object.hasOwn(value,'html');
910
+ if(!hasText&&!hasHtml||(hasText&&typeof value.text!=='string')
911
+ ||(hasHtml&&typeof value.html!=='string')){
912
+ throw new MailGatewayFault('mail_content_required',{statusCode:422});
913
+ }
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
+ const recipients=normalizeReportRecipients(value,configuration);
928
+ const providerBody={
929
+ from:configuration.from,
930
+ to:recipients,
931
+ subject:value.subject,
932
+ ...(hasText?{text:value.text}:{}),
933
+ ...(hasHtml?{html:value.html}:{})
934
+ };
935
+ const serializedProviderBody=JSON.stringify(providerBody);
936
+ if(utf8Bytes(serializedProviderBody)>configuration.maxRequestBytes){
937
+ throw new MailGatewayFault('mail_request_too_large',{statusCode:413});
938
+ }
939
+ return {
940
+ providerBody:serializedProviderBody,
941
+ providerBodyBytes:utf8Bytes(serializedProviderBody),
942
+ recipientCount:recipients.length
943
+ };
944
+ }
945
+
946
+ function parseReport(serialized,configuration){
947
+ let value;
948
+ try{
949
+ value=JSON.parse(serialized);
950
+ }catch{
951
+ throw new MailGatewayFault('mail_invalid_json',{statusCode:400});
952
+ }
953
+ return normalizeReport(value,configuration);
954
+ }
955
+
956
+ function parseRetryAfter(value,now=Date.now()){
957
+ if(typeof value!=='string'||!value.trim()){
958
+ return 0;
959
+ }
960
+ const trimmed=value.trim();
961
+ if(/^\d+(?:\.\d+)?$/u.test(trimmed)){
962
+ return boundedRetryAfter(Math.ceil(Number(trimmed)*1000));
963
+ }
964
+ const timestamp=Date.parse(trimmed);
965
+ return Number.isFinite(timestamp)
966
+ ? boundedRetryAfter(Math.max(0,timestamp-now))
967
+ : 0;
968
+ }
969
+
970
+ function responseHeader(response,name){
971
+ try{
972
+ const value=response.headers?.get?.(name);
973
+ return value===null||value===undefined?'':String(value);
974
+ }catch{
975
+ return '';
976
+ }
977
+ }
978
+
979
+ function awaitAbortable(value,signal){
980
+ return new Promise(function waitForAbortable(resolve,reject){
981
+ let settled=false;
982
+ function cleanup(){
983
+ signal?.removeEventListener('abort',onAbort);
984
+ }
985
+ function settleResolved(result){
986
+ if(settled)return;
987
+ settled=true;
988
+ cleanup();
989
+ resolve(result);
990
+ }
991
+ function settleRejected(error){
992
+ if(settled)return;
993
+ settled=true;
994
+ cleanup();
995
+ reject(error);
996
+ }
997
+ function onAbort(){
998
+ settleRejected(signal.reason??new Error('Operation cancelled.'));
999
+ }
1000
+ signal?.addEventListener('abort',onAbort,{once:true});
1001
+ Promise.resolve(value).then(settleResolved,settleRejected);
1002
+ if(signal?.aborted){
1003
+ onAbort();
1004
+ }
1005
+ });
1006
+ }
1007
+
1008
+ function ignoreCancellationFailure(){}
1009
+
1010
+ function cancelProviderBody(body){
1011
+ if(!body||typeof body.cancel!=='function'){
1012
+ return;
1013
+ }
1014
+ try{
1015
+ Promise.resolve(body.cancel()).catch(ignoreCancellationFailure);
1016
+ }catch{
1017
+ // Cancellation is best-effort after the response boundary is classified.
1018
+ }
1019
+ }
1020
+
1021
+ function cancelProviderReader(reader){
1022
+ if(!reader||typeof reader.cancel!=='function'){
1023
+ return;
1024
+ }
1025
+ try{
1026
+ Promise.resolve(reader.cancel()).catch(ignoreCancellationFailure);
1027
+ }catch{
1028
+ // Cancellation is best-effort after the response boundary is classified.
1029
+ }
1030
+ }
1031
+
1032
+ async function readProviderBody(response,maxBytes,signal){
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
+ }
1044
+ if(response.body===null||response.body===undefined){
1045
+ return '';
1046
+ }
1047
+ if(typeof response.body.getReader!=='function'){
1048
+ cancelProviderBody(response.body);
1049
+ throw new MailGatewayFault('resend_unreadable_response',{
1050
+ statusCode:502,
1051
+ uncertain:true
1052
+ });
1053
+ }
1054
+ const reader=response.body.getReader();
1055
+ const chunks=[];
1056
+ let byteLength=0;
1057
+ let fullyRead=false;
1058
+ try{
1059
+ while(true){
1060
+ const result=await awaitAbortable(reader.read(),signal);
1061
+ if(result.done){
1062
+ fullyRead=true;
1063
+ break;
1064
+ }
1065
+ if(!(result.value instanceof Uint8Array)){
1066
+ throw new MailGatewayFault('resend_unreadable_response',{
1067
+ statusCode:502,
1068
+ uncertain:true
1069
+ });
1070
+ }
1071
+ byteLength+=result.value.byteLength;
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));
1079
+ }
1080
+ return Buffer.concat(chunks,byteLength).toString('utf8');
1081
+ }finally{
1082
+ if(!fullyRead){
1083
+ cancelProviderReader(reader);
1084
+ }
1085
+ try{
1086
+ reader.releaseLock();
1087
+ }catch{
1088
+ // An untrusted stream implementation cannot replace the provider classification.
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ function parseProviderObject(text){
1094
+ if(!text){
1095
+ return null;
1096
+ }
1097
+ try{
1098
+ const value=JSON.parse(text);
1099
+ return value&&typeof value==='object'&&!Array.isArray(value)?value:null;
1100
+ }catch{
1101
+ return null;
1102
+ }
1103
+ }
1104
+
1105
+ function providerCode(value,statusCode){
1106
+ const candidate=value?.name;
1107
+ if(typeof candidate==='string'&&PROVIDER_CODE_PATTERN.test(candidate)){
1108
+ return candidate;
1109
+ }
1110
+ return `resend_http_${String(statusCode)}`;
1111
+ }
1112
+
1113
+ function providerRejection(statusCode,value,retryAfterMs,defaultRetryAfterMs){
1114
+ const code=providerCode(value,statusCode);
1115
+ const permanentRateLimit=PERMANENT_RATE_CODES.has(code);
1116
+ const retryable=code==='concurrent_idempotent_requests'
1117
+ ||(statusCode===409&&code!=='invalid_idempotent_request')
1118
+ ||(!permanentRateLimit&&code!=='invalid_idempotent_request'
1119
+ &&RETRYABLE_PROVIDER_STATUSES.has(statusCode));
1120
+ const resolvedDelay=retryable
1121
+ ? boundedRetryAfter(retryAfterMs||defaultRetryAfterMs)
1122
+ : 0;
1123
+ return {
1124
+ kind:'rejected',
1125
+ fault:new MailGatewayFault(code,{
1126
+ retryable,
1127
+ retryAfterMs:resolvedDelay,
1128
+ statusCode:retryable?(statusCode===429?429:503):422
1129
+ }),
1130
+ providerStatus:statusCode
1131
+ };
1132
+ }
1133
+
1134
+ function ambiguousResult(code,retryAfterMs,providerStatus=0){
1135
+ return {
1136
+ code,
1137
+ kind:'ambiguous',
1138
+ providerStatus,
1139
+ retryAfterMs:boundedRetryAfter(retryAfterMs)
1140
+ };
1141
+ }
1142
+
1143
+ async function performResendAttempt(configuration,delivery,idempotencyKey,signal,requestId,observe){
1144
+ const controller=new AbortController();
1145
+ let timedOut=false;
1146
+ function forwardAbort(){
1147
+ controller.abort(signal?.reason??new Error('Mail request cancelled.'));
1148
+ }
1149
+ signal?.addEventListener('abort',forwardAbort,{once:true});
1150
+ if(signal?.aborted){
1151
+ forwardAbort();
1152
+ }
1153
+ const timeout=setTimeout(function expireResendAttempt(){
1154
+ timedOut=true;
1155
+ controller.abort(new Error('Resend request timed out.'));
1156
+ },configuration.providerTimeoutMs);
1157
+ const startedAt=Date.now();
1158
+ observe({
1159
+ type:'mail.provider.started',
1160
+ appId:configuration.appId,
1161
+ requestId
1162
+ });
1163
+ let response;
1164
+ try{
1165
+ try{
1166
+ response=await awaitAbortable(configuration.fetchImpl(RESEND_EMAIL_ENDPOINT,{
1167
+ method:'POST',
1168
+ headers:{
1169
+ 'Authorization':`Bearer ${configuration.apiKey}`,
1170
+ 'Content-Type':'application/json',
1171
+ 'Idempotency-Key':idempotencyKey,
1172
+ 'User-Agent':'arcane-os-sdk-mail/1'
1173
+ },
1174
+ body:delivery.providerBody,
1175
+ redirect:'error',
1176
+ referrerPolicy:'no-referrer',
1177
+ signal:controller.signal
1178
+ }),controller.signal);
1179
+ }catch{
1180
+ return ambiguousResult(
1181
+ timedOut?'resend_timeout':'resend_transport_uncertain',
1182
+ configuration.retryableDelayMs
1183
+ );
1184
+ }
1185
+ const statusCode=Number(response?.status);
1186
+ if(!Number.isSafeInteger(statusCode)||statusCode<100||statusCode>599){
1187
+ return ambiguousResult('resend_invalid_response',configuration.retryableDelayMs);
1188
+ }
1189
+ let text='';
1190
+ try{
1191
+ text=await readProviderBody(
1192
+ response,
1193
+ configuration.maxProviderResponseBytes,
1194
+ controller.signal
1195
+ );
1196
+ }catch(error){
1197
+ if(statusCode>=200&&statusCode<300||controller.signal.aborted){
1198
+ return ambiguousResult(
1199
+ error instanceof MailGatewayFault?error.code:'resend_transport_uncertain',
1200
+ configuration.retryableDelayMs,
1201
+ statusCode
1202
+ );
1203
+ }
1204
+ return providerRejection(
1205
+ statusCode,
1206
+ null,
1207
+ parseRetryAfter(responseHeader(response,'retry-after')),
1208
+ configuration.retryableDelayMs
1209
+ );
1210
+ }
1211
+ const value=parseProviderObject(text);
1212
+ if(statusCode>=200&&statusCode<300){
1213
+ if(!value||typeof value.id!=='string'||!PROVIDER_ID_PATTERN.test(value.id)){
1214
+ return ambiguousResult(
1215
+ 'resend_invalid_success_response',
1216
+ configuration.retryableDelayMs,
1217
+ statusCode
1218
+ );
1219
+ }
1220
+ return {
1221
+ kind:'accepted',
1222
+ providerId:value.id,
1223
+ providerStatus:statusCode
1224
+ };
1225
+ }
1226
+ return providerRejection(
1227
+ statusCode,
1228
+ value,
1229
+ parseRetryAfter(responseHeader(response,'retry-after')),
1230
+ configuration.retryableDelayMs
1231
+ );
1232
+ }finally{
1233
+ clearTimeout(timeout);
1234
+ signal?.removeEventListener('abort',forwardAbort);
1235
+ observe({
1236
+ type:'mail.provider.completed',
1237
+ appId:configuration.appId,
1238
+ durationMs:Math.max(0,Date.now()-startedAt),
1239
+ requestId,
1240
+ providerStatus:Number.isSafeInteger(Number(response?.status))?Number(response.status):0
1241
+ });
1242
+ }
1243
+ }
1244
+
1245
+ function normalizeDirectSendOptions(options){
1246
+ if(!options||typeof options!=='object'||Array.isArray(options)){
1247
+ throw configurationError('Mail send options must be an object.');
1248
+ }
1249
+ if(typeof options.reportKey!=='string'||!IDEMPOTENCY_KEY_PATTERN.test(options.reportKey)){
1250
+ throw configurationError('reportKey must contain 8-128 safe identifier characters.');
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.');
1264
+ }
1265
+ const fetchImpl=options.fetchImpl??globalThis.fetch;
1266
+ if(typeof fetchImpl!=='function'){
1267
+ throw configurationError('A fetch implementation is required for Resend delivery.');
1268
+ }
1269
+ if(options.onEvent!==undefined&&typeof options.onEvent!=='function'){
1270
+ throw configurationError('onEvent must be a function when supplied.');
1271
+ }
1272
+ if(options.requestIdFactory!==undefined&&typeof options.requestIdFactory!=='function'){
1273
+ throw configurationError('requestIdFactory must be a function when supplied.');
1274
+ }
1275
+ return Object.freeze({
1276
+ allowAnyRecipient:true,
1277
+ allowedRecipients:null,
1278
+ apiKey:validateApiKey(options.apiKey),
1279
+ appId:validateAppId(options.appId),
1280
+ errorRecipients:[],
1281
+ fetchImpl,
1282
+ from:validateFrom(options.from),
1283
+ maxMessageBytes,
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
+ ),
1300
+ requestIdFactory:options.requestIdFactory??randomUUID,
1301
+ retryableDelayMs:boundedInteger(
1302
+ options.retryableDelayMs,
1303
+ 1_000,
1304
+ {label:'retryableDelayMs',min:1,max:60_000}
1305
+ ),
1306
+ signal:validateSignal(options.signal),
1307
+ report:options.report,
1308
+ reportKey:options.reportKey,
1309
+ onEvent:options.onEvent
1310
+ });
1311
+ }
1312
+
1313
+ function directSendResult(result,{recipientCount,requestId}){
1314
+ const common={
1315
+ provider:'resend',
1316
+ status:result.kind==='accepted'
1317
+ ?'accepted'
1318
+ :result.kind==='ambiguous'?'delivery_uncertain':'rejected',
1319
+ classification:result.kind==='rejected'
1320
+ ?result.fault.retryable?'retryable':'permanent'
1321
+ :result.kind,
1322
+ requestId,
1323
+ providerStatus:result.providerStatus,
1324
+ recipientCount
1325
+ };
1326
+ if(result.kind==='accepted'){
1327
+ return Object.freeze({...common,providerId:result.providerId});
1328
+ }
1329
+ if(result.kind==='ambiguous'){
1330
+ return Object.freeze({
1331
+ ...common,
1332
+ code:result.code,
1333
+ ...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{}),
1334
+ retryable:true,
1335
+ uncertain:true
1336
+ });
1337
+ }
1338
+ return Object.freeze({
1339
+ ...common,
1340
+ code:result.fault.code,
1341
+ ...(result.fault.retryAfterMs?{retryAfterMs:result.fault.retryAfterMs}:{}),
1342
+ retryable:result.fault.retryable,
1343
+ uncertain:false
1344
+ });
1345
+ }
1346
+
1347
+ export async function sendResendMail(options={}){
1348
+ const configuration=normalizeDirectSendOptions(options);
1349
+ const delivery=normalizeReport(configuration.report,configuration);
1350
+ if(configuration.signal?.aborted){
1351
+ const error=new Error('Mail send cancelled before provider attempt.');
1352
+ error.code='ARCANE_CANCELLED';
1353
+ throw error;
1354
+ }
1355
+ const requestId=createRequestId(configuration.requestIdFactory);
1356
+ const observer=createObserver(
1357
+ configuration.onEvent,
1358
+ configuration.observerDrainTimeoutMs
1359
+ );
1360
+ try{
1361
+ const result=await performResendAttempt(
1362
+ configuration,
1363
+ delivery,
1364
+ configuration.reportKey,
1365
+ configuration.signal,
1366
+ requestId,
1367
+ observer.observe
1368
+ );
1369
+ return directSendResult(result,{
1370
+ recipientCount:delivery.recipientCount,
1371
+ requestId
1372
+ });
1373
+ }finally{
1374
+ await observer.drain();
1375
+ }
1376
+ }
1377
+
1378
+ function sendProviderResult(response,result,{origin,requestId,recipientCount}){
1379
+ if(result.kind==='accepted'){
1380
+ return writeJson(response,202,{
1381
+ requestId,
1382
+ status:'accepted',
1383
+ accepted:recipientCount,
1384
+ rejected:0,
1385
+ providerId:result.providerId
1386
+ },{origin});
1387
+ }
1388
+ if(result.kind==='ambiguous'){
1389
+ return writeJson(response,207,{
1390
+ requestId,
1391
+ status:'delivery_uncertain',
1392
+ accepted:0,
1393
+ rejected:0,
1394
+ ...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{})
1395
+ },{origin,retryAfterMs:result.retryAfterMs});
1396
+ }
1397
+ return writeFault(response,requestId,result.fault,origin);
1398
+ }
1399
+
1400
+ export function createResendMailRequestHandler(options={}){
1401
+ const configuration=normalizeConfiguration(options);
1402
+ const ownerController=new AbortController();
1403
+ 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
+ });
1424
+ let closePromise=null;
1425
+
1426
+ function forwardOwnerAbort(){
1427
+ ownerController.abort(configuration.signal?.reason??new Error('Mail server cancelled.'));
1428
+ }
1429
+ configuration.signal?.addEventListener('abort',forwardOwnerAbort,{once:true});
1430
+ if(configuration.signal?.aborted){
1431
+ forwardOwnerAbort();
1432
+ }
1433
+
1434
+ async function handleOwnedRequest(request,response){
1435
+ const requestId=createRequestId(configuration.requestIdFactory);
1436
+ const startedAt=Date.now();
1437
+ const requestController=new AbortController();
1438
+ let origin='';
1439
+ let providerAttempted=false;
1440
+
1441
+ function abortFromOwner(){
1442
+ requestController.abort(ownerController.signal.reason);
1443
+ }
1444
+ function abortFromRequest(){
1445
+ requestController.abort(new Error('Mail client disconnected.'));
1446
+ }
1447
+ function abortFromResponseClose(){
1448
+ if(!response.writableEnded){
1449
+ abortFromRequest();
1450
+ }
1451
+ }
1452
+ function absorbResponseError(){
1453
+ abortFromRequest();
1454
+ }
1455
+ function releaseRequestListeners(){
1456
+ request.removeListener('aborted',abortFromRequest);
1457
+ request.removeListener('error',abortFromRequest);
1458
+ }
1459
+ function releaseResponseListeners(){
1460
+ response.removeListener('close',abortFromResponseClose);
1461
+ response.removeListener('error',absorbResponseError);
1462
+ }
1463
+
1464
+ ownerController.signal.addEventListener('abort',abortFromOwner,{once:true});
1465
+ request.once('aborted',abortFromRequest);
1466
+ request.once('error',abortFromRequest);
1467
+ response.once('close',abortFromResponseClose);
1468
+ response.once('error',absorbResponseError);
1469
+ if(ownerController.signal.aborted){
1470
+ abortFromOwner();
1471
+ }
1472
+ observer.observe({
1473
+ type:'mail.request.received',
1474
+ appId:configuration.appId,
1475
+ requestId
1476
+ });
1477
+
1478
+ try{
1479
+ validateLoopbackRequest(request);
1480
+ origin=allowedOrigin(request,configuration);
1481
+ if(request.url!==RESEND_MAIL_PATH){
1482
+ throw new MailGatewayFault('mail_route_not_found',{statusCode:404});
1483
+ }
1484
+ if(request.method==='OPTIONS'){
1485
+ const preflight=validatePreflight(request);
1486
+ writePreflight(response,origin,preflight);
1487
+ return;
1488
+ }
1489
+ if(request.method!=='POST'){
1490
+ throw new MailGatewayFault('mail_method_not_allowed',{statusCode:405});
1491
+ }
1492
+ if(singleHeader(request,'x-mail-app')!==configuration.appId){
1493
+ throw new MailGatewayFault('mail_app_not_allowed',{statusCode:403});
1494
+ }
1495
+ authenticateLocalCaller(request,configuration);
1496
+ const idempotencyKey=singleHeader(request,'idempotency-key');
1497
+ if(!IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)){
1498
+ throw new MailGatewayFault('invalid_idempotency_key',{statusCode:400});
1499
+ }
1500
+ const contentType=singleHeader(request,'content-type');
1501
+ if(!JSON_CONTENT_TYPE_PATTERN.test(contentType)){
1502
+ throw new MailGatewayFault('mail_unsupported_content_type',{statusCode:415});
1503
+ }
1504
+ requestContentLength(request,configuration.maxRequestBytes);
1505
+ const serialized=await bodyScheduler.schedule(
1506
+ function readAdmittedMailBody(){
1507
+ return readRequestBody(request,{
1508
+ maxRequestBytes:configuration.maxRequestBytes,
1509
+ signal:requestController.signal,
1510
+ timeoutMs:configuration.bodyTimeoutMs
1511
+ });
1512
+ },
1513
+ {signal:requestController.signal}
1514
+ );
1515
+ const delivery=parseReport(serialized,configuration);
1516
+ const result=await sendScheduler.schedule(
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
+ }
1532
+ );
1533
+ sendProviderResult(response,result,{
1534
+ origin,
1535
+ recipientCount:delivery.recipientCount,
1536
+ requestId
1537
+ });
1538
+ observer.observe({
1539
+ type:'mail.request.completed',
1540
+ appId:configuration.appId,
1541
+ classification:result.kind,
1542
+ durationMs:Math.max(0,Date.now()-startedAt),
1543
+ providerAttempted,
1544
+ requestId
1545
+ });
1546
+ }catch(error){
1547
+ const fault=normalizeFault(error);
1548
+ writeFault(response,requestId,fault,origin);
1549
+ if(!request.readableEnded&&!request.destroyed){
1550
+ request.resume();
1551
+ }
1552
+ observer.observe({
1553
+ type:'mail.request.completed',
1554
+ appId:configuration.appId,
1555
+ classification:fault.uncertain?'ambiguous':fault.retryable?'retryable':'permanent',
1556
+ durationMs:Math.max(0,Date.now()-startedAt),
1557
+ providerAttempted,
1558
+ requestId
1559
+ });
1560
+ }finally{
1561
+ ownerController.signal.removeEventListener('abort',abortFromOwner);
1562
+ if(request.readableEnded||request.destroyed){
1563
+ releaseRequestListeners();
1564
+ }else{
1565
+ request.once('end',releaseRequestListeners);
1566
+ }
1567
+ if(response.writableFinished||response.destroyed){
1568
+ releaseResponseListeners();
1569
+ }else{
1570
+ response.once('finish',releaseResponseListeners);
1571
+ }
1572
+ }
1573
+ }
1574
+
1575
+ function handle(request,response){
1576
+ const operation=handleOwnedRequest(request,response);
1577
+ activeRequests.add(operation);
1578
+ operation.catch(function closeFailedRequest(){
1579
+ if(!response.destroyed){
1580
+ response.destroy();
1581
+ }
1582
+ }).finally(function releaseActiveRequest(){
1583
+ activeRequests.delete(operation);
1584
+ });
1585
+ }
1586
+
1587
+ async function closeOwnedHandler(){
1588
+ configuration.signal?.removeEventListener('abort',forwardOwnerAbort);
1589
+ if(!ownerController.signal.aborted){
1590
+ ownerController.abort(new Error('Mail server closed.'));
1591
+ }
1592
+ bodyScheduler.close();
1593
+ sendScheduler.close();
1594
+ await Promise.all([
1595
+ bodyScheduler.idle(),
1596
+ sendScheduler.idle(),
1597
+ Promise.allSettled([...activeRequests])
1598
+ ]);
1599
+ await observer.drain();
1600
+ }
1601
+
1602
+ function close(){
1603
+ if(!closePromise){
1604
+ closePromise=closeOwnedHandler();
1605
+ }
1606
+ return closePromise;
1607
+ }
1608
+
1609
+ return Object.freeze({
1610
+ appId:configuration.appId,
1611
+ callerAuthentication:configuration.callerAuthentication,
1612
+ close,
1613
+ handle,
1614
+ path:RESEND_MAIL_PATH,
1615
+ protocol:RESEND_MAIL_SERVER_PROTOCOL
1616
+ });
1617
+ }
1618
+
1619
+ function listen(server,{host,port}){
1620
+ return new Promise(function waitForMailListener(resolve,reject){
1621
+ function cleanup(){
1622
+ server.removeListener('error',onError);
1623
+ server.removeListener('listening',onListening);
1624
+ }
1625
+ function onError(error){
1626
+ cleanup();
1627
+ reject(error);
1628
+ }
1629
+ function onListening(){
1630
+ cleanup();
1631
+ resolve();
1632
+ }
1633
+ server.once('error',onError);
1634
+ server.once('listening',onListening);
1635
+ server.listen({exclusive:true,host,port});
1636
+ });
1637
+ }
1638
+
1639
+ function closeHttpServer(server){
1640
+ return new Promise(function waitForHttpServerClose(resolve,reject){
1641
+ if(!server.listening){
1642
+ resolve();
1643
+ return;
1644
+ }
1645
+ server.close(function finishHttpServerClose(error){
1646
+ if(error){
1647
+ reject(error);
1648
+ }else{
1649
+ resolve();
1650
+ }
1651
+ });
1652
+ server.closeIdleConnections?.();
1653
+ });
1654
+ }
1655
+
1656
+ export async function startResendMailServer(options={}){
1657
+ const configuration=normalizeConfiguration(options);
1658
+ if(configuration.signal?.aborted){
1659
+ throw configuration.signal.reason??new Error('Mail server start was cancelled.');
1660
+ }
1661
+ 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;
1676
+ server.on('clientError',function rejectMalformedClient(error,socket){
1677
+ if(!socket.writable){
1678
+ return;
1679
+ }
1680
+ socket.end(
1681
+ 'HTTP/1.1 400 Bad Request\r\n'
1682
+ +'Connection: close\r\n'
1683
+ +'Content-Length: 0\r\n'
1684
+ +'\r\n'
1685
+ );
1686
+ });
1687
+
1688
+ try{
1689
+ await listen(server,{host:configuration.host,port:configuration.port});
1690
+ }catch(error){
1691
+ await requestHandler.close();
1692
+ throw error;
1693
+ }
1694
+ const address=server.address();
1695
+ if(!address||typeof address==='string'||!isNumericLoopback(address.address)){
1696
+ await Promise.allSettled([closeHttpServer(server),requestHandler.close()]);
1697
+ throw configurationError('Mail server did not bind to a numeric loopback address.');
1698
+ }
1699
+ const displayHost=address.address.includes(':')?`[${address.address}]`:address.address;
1700
+ const origin=`http://${displayHost}:${String(address.port)}`;
1701
+ let closePromise=null;
1702
+ let resolveLifecycle;
1703
+ let rejectLifecycle;
1704
+ const lifecycle=new Promise(function createMailLifecycle(resolve,reject){
1705
+ resolveLifecycle=resolve;
1706
+ rejectLifecycle=reject;
1707
+ });
1708
+ lifecycle.catch(function observeMailLifecycleFailure(){});
1709
+
1710
+ async function closeOwnedServer(){
1711
+ configuration.signal?.removeEventListener('abort',closeFromSignal);
1712
+ const handlerClosing=requestHandler.close();
1713
+ try{
1714
+ await Promise.all([closeHttpServer(server),handlerClosing]);
1715
+ resolveLifecycle();
1716
+ }catch(error){
1717
+ rejectLifecycle(error);
1718
+ throw error;
1719
+ }
1720
+ }
1721
+
1722
+ function close(){
1723
+ if(!closePromise){
1724
+ closePromise=closeOwnedServer();
1725
+ }
1726
+ return closePromise;
1727
+ }
1728
+
1729
+ function closeFromSignal(){
1730
+ close().catch(function ignoreSignalCloseFailure(){});
1731
+ }
1732
+
1733
+ function closeFromServerError(error){
1734
+ rejectLifecycle(error);
1735
+ close().catch(function observeOperationalCloseFailure(){});
1736
+ }
1737
+
1738
+ server.once('close',function finishExternallyClosedServer(){
1739
+ if(!closePromise){
1740
+ closePromise=requestHandler.close().then(
1741
+ function resolveExternalClose(){resolveLifecycle();},
1742
+ function rejectExternalClose(error){rejectLifecycle(error);throw error;}
1743
+ );
1744
+ closePromise.catch(function observeExternalCloseFailure(){});
1745
+ }
1746
+ });
1747
+ server.on('error',closeFromServerError);
1748
+ configuration.signal?.addEventListener('abort',closeFromSignal,{once:true});
1749
+ if(configuration.signal?.aborted){
1750
+ closeFromSignal();
1751
+ }
1752
+
1753
+ return Object.freeze({
1754
+ appId:configuration.appId,
1755
+ callerAuthentication:configuration.callerAuthentication,
1756
+ close,
1757
+ closed:lifecycle,
1758
+ host:address.address,
1759
+ lifecycle,
1760
+ mode:'mail',
1761
+ origin,
1762
+ path:RESEND_MAIL_PATH,
1763
+ port:address.port,
1764
+ protocol:RESEND_MAIL_SERVER_PROTOCOL,
1765
+ server,
1766
+ target:'mail',
1767
+ url:`${origin}${RESEND_MAIL_PATH}`
1768
+ });
1769
+ }