arcane-os 0.3.1 → 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.
Files changed (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -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._:-]{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;
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=boundedRetryAfter(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 boundedInteger(value,fallback,{label,min,max}){
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
- if(!Number.isSafeInteger(resolved)||resolved<min||resolved>max){
50
- throw configurationError(`${label} must be an integer between ${min} and ${max}.`);
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 boundedRetryAfter(value){
56
- return Number.isSafeInteger(value)&&value>0
57
- ? Math.min(MAX_RETRY_AFTER_MS,value)
58
- : 0;
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||value.length>4096
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 Object.freeze({
117
+ return {
99
118
  appKeyDigest:null,
100
119
  callerAuthentication:'origin-app-id-only'
101
- });
120
+ };
102
121
  }
103
- if(typeof options.appKey!=='string'||options.appKey.length<16
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 contain 16-512 printable ASCII characters unless unauthenticated caller mode is explicitly enabled.'
124
+ 'appKey must be a nonempty printable ASCII string unless unauthenticated caller mode is explicitly enabled.'
107
125
  );
108
126
  }
109
- return Object.freeze({
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)||value.length>50||(!allowEmpty&&value.length===0)){
144
- throw configurationError(`${label} must contain ${allowEmpty?'zero to ':'one to '}50 addresses.`);
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||value.length>64){
178
- throw configurationError('allowedOrigins must contain one to 64 exact origins.');
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 Object.freeze({
244
- allowAnyRecipient:false,
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
- 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
- ),
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
- 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
- ),
258
+ port:portNumber(options.port,8025),
259
+ providerTimeoutMs:optionalPositiveInteger(options.providerTimeoutMs,'providerTimeoutMs'),
309
260
  requestIdFactory:options.requestIdFactory??randomUUID,
310
- retryableDelayMs:boundedInteger(
261
+ retryableDelayMs:positiveInteger(
311
262
  options.retryableDelayMs,
312
263
  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}
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 a bounded error response.
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=candidate.length>=16&&candidate.length<=512
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 boundedDelay=boundedRetryAfter(retryAfterMs);
459
- if(boundedDelay){
460
- headers['retry-after']=String(Math.max(1,Math.ceil(boundedDelay/1000)));
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=boundedRetryAfter(fault.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',{statusCode:500});
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,drainTimeoutMs){
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(Object.freeze({...event}));
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
- 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();
488
+ await Promise.allSettled([...pending]);
555
489
  }
556
490
  return {drain,observe};
557
491
  }
558
492
 
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}){
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=setTimeout(function expireRequestBody(){
772
- finish(new MailGatewayFault('mail_body_timeout',{
773
- retryable:true,
774
- statusCode:408
775
- }));
776
- request.resume();
777
- },timeoutMs);
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,byteLength).toString('utf8'));
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)||report.to.length>50){
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
- 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')
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'||value.subject!==value.subject.trim()
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 boundedRetryAfter(Math.ceil(Number(trimmed)*1000));
659
+ return normalizeRetryAfter(Math.ceil(Number(trimmed)*1000));
963
660
  }
964
661
  const timestamp=Date.parse(trimmed);
965
662
  return Number.isFinite(timestamp)
966
- ? boundedRetryAfter(Math.max(0,timestamp-now))
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,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
- }
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 chunks=[];
1056
- let byteLength=0;
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
- 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));
757
+ text+=decoder.decode(result.value,{stream:true});
1079
758
  }
1080
- return Buffer.concat(chunks,byteLength).toString('utf8');
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
- ? boundedRetryAfter(retryAfterMs||defaultRetryAfterMs)
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:boundedRetryAfter(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=setTimeout(function expireResendAttempt(){
1154
- timedOut=true;
1155
- controller.abort(new Error('Resend request timed out.'));
1156
- },configuration.providerTimeoutMs);
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('resend_invalid_response',configuration.retryableDelayMs);
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 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.');
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 Object.freeze({
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
- 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
- ),
971
+ providerTimeoutMs:optionalPositiveInteger(options.providerTimeoutMs,'providerTimeoutMs'),
1300
972
  requestIdFactory:options.requestIdFactory??randomUUID,
1301
- retryableDelayMs:boundedInteger(
973
+ retryableDelayMs:positiveInteger(
1302
974
  options.retryableDelayMs,
1303
975
  1_000,
1304
- {label:'retryableDelayMs',min:1,max:60_000}
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,{recipientCount,requestId}){
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
- recipientCount
997
+ providerRequest:JSON.parse(delivery.providerBody),
998
+ report:delivery.report,
999
+ recipientCount:delivery.recipientCount
1325
1000
  };
1326
1001
  if(result.kind==='accepted'){
1327
- return Object.freeze({...common,providerId:result.providerId});
1002
+ return {...common,providerId:result.providerId};
1328
1003
  }
1329
1004
  if(result.kind==='ambiguous'){
1330
- return Object.freeze({
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 Object.freeze({
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
- recipientCount:delivery.recipientCount,
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
- const idempotencyKey=singleHeader(request,'idempotency-key');
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
- 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
- }
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
- bodyScheduler.close();
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 Object.freeze({
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 Object.freeze({
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
  }