arcane-os 0.15.2 → 0.16.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.
package/src/mail.mjs CHANGED
@@ -20,65 +20,38 @@ export const MAIL_COMMAND_ACTIONS=[
20
20
 
21
21
  const ACTION_SET=new Set(MAIL_COMMAND_ACTIONS);
22
22
 
23
- function usage(message){
23
+ function throwMailUsageError(message){
24
24
  throw new ArcaneError(ERROR_CODES.usage,message);
25
25
  }
26
26
 
27
- function validateOptions(options){
27
+ function validateMailCommandOptions(options){
28
28
  if(!options||!is.object(options)||is.array(options)){
29
- usage('Mail command options must be an object.');
29
+ throwMailUsageError('Mail command options must be an object.');
30
30
  }
31
31
  if(!is.string(options.action)||!ACTION_SET.has(options.action)){
32
- usage(`Mail action must be one of: ${MAIL_COMMAND_ACTIONS.join(', ')}.`);
32
+ throwMailUsageError(`Mail action must be one of: ${MAIL_COMMAND_ACTIONS.join(', ')}.`);
33
33
  }
34
34
  return options;
35
35
  }
36
36
 
37
- function dependency(options,name,fallback){
37
+ function resolveMailCommandDependency(options,name,fallback){
38
38
  const value=options[name]??fallback;
39
39
  if(!is.function(value)){
40
- usage(`${name} must be a function when supplied.`);
40
+ throwMailUsageError(`${name} must be a function when supplied.`);
41
41
  }
42
42
  return value;
43
43
  }
44
44
 
45
- function recipientList(value,label){
46
- if(value===undefined||value===null||value===''||(is.array(value)&&value.length===0)){
45
+ function mailRecipientOptions(value,label){
46
+ if(value===undefined||value===null||value===''){
47
47
  return [];
48
48
  }
49
- const entries=is.array(value)
50
- ? value
51
- : is.string(value)
52
- ? value.split(',')
53
- : null;
54
- if(!entries||entries.length===0){
55
- usage(`${label} must contain at least one email address when supplied.`);
56
- }
57
- const result=entries.map(function normalizeMailRecipient(entry){
58
- if(!is.string(entry)||!entry.trim()){
59
- usage(`${label} contains an empty email address.`);
60
- }
61
- return entry.trim();
62
- });
63
- if(new Set(result.map(function lowercaseRecipient(entry){
64
- return entry.toLowerCase();
65
- })).size!==result.length){
66
- usage(`${label} must not contain duplicate email addresses.`);
67
- }
68
- return result;
69
- }
70
-
71
- function originList(value){
72
- const entries=is.array(value)?value:[value];
73
- if(entries.length===0||entries.some(function invalidOrigin(entry){
74
- return !is.string(entry)||!entry.trim();
75
- })){
76
- usage('Mail serve requires at least one exact allowed origin.');
77
- }
78
- return entries.map(function normalizeAllowedOrigin(entry){return entry.trim();});
49
+ if(is.array(value))return [...value];
50
+ if(is.string(value))return value.split(',');
51
+ throwMailUsageError(`${label} must be an address array or comma-separated string.`);
79
52
  }
80
53
 
81
- function credentialOptions(options){
54
+ function mailCredentialOptions(options){
82
55
  return {
83
56
  profile:options.profile,
84
57
  platform:options.platform,
@@ -91,60 +64,40 @@ function credentialOptions(options){
91
64
  };
92
65
  }
93
66
 
94
- async function setCredential(options){
95
- const readSecret=dependency(options,'readSecret',null);
96
- const store=dependency(options,'setCredential',setMailCredential);
67
+ async function setMailCredentialFromInput(options){
68
+ const readSecret=resolveMailCommandDependency(options,'readSecret',null);
69
+ const store=resolveMailCommandDependency(options,'setCredential',setMailCredential);
97
70
  throwIfAborted(options.signal);
98
71
  let secret='';
99
72
  try{
100
73
  secret=await readSecret();
101
74
  throwIfAborted(options.signal);
102
- return await store({...credentialOptions(options),secret});
75
+ return await store({...mailCredentialOptions(options),secret});
103
76
  }finally{
104
77
  secret='';
105
78
  }
106
79
  }
107
80
 
108
- async function credentialStatus(options){
109
- const status=dependency(options,'getCredentialStatus',getMailCredentialStatus);
81
+ async function readMailCredentialStatus(options){
82
+ const status=resolveMailCommandDependency(options,'getCredentialStatus',getMailCredentialStatus);
110
83
  throwIfAborted(options.signal);
111
- return status(credentialOptions(options));
84
+ return status(mailCredentialOptions(options));
112
85
  }
113
86
 
114
- async function deleteCredential(options){
115
- const remove=dependency(options,'deleteCredential',deleteMailCredential);
87
+ async function deleteMailCredentialProfile(options){
88
+ const remove=resolveMailCommandDependency(options,'deleteCredential',deleteMailCredential);
116
89
  throwIfAborted(options.signal);
117
- return remove(credentialOptions(options));
90
+ return remove(mailCredentialOptions(options));
118
91
  }
119
92
 
120
- function withoutMailCredentials(value,seen=new WeakMap()){
121
- if(!value||!is.object(value))return value;
122
- if(seen.has(value))return seen.get(value);
123
- const copy=is.array(value)?[]:{};
124
- seen.set(value,copy);
125
- for(const [key,entry] of Object.entries(value)){
126
- if(key==='apiKey'||key==='appKey')continue;
127
- copy[key]=withoutMailCredentials(entry,seen);
128
- }
129
- return copy;
130
- }
131
-
132
- function completeSendFailure(result){
133
- if(!result||!is.object(result)||is.array(result)){
134
- return {provider:'resend',result:withoutMailCredentials(result)};
135
- }
136
- // Complete provider detail remains visible; only credential fields are omitted.
137
- return withoutMailCredentials(result);
138
- }
139
-
140
- async function sendMail(options){
141
- const readReport=dependency(options,'readReport',null);
142
- const readCredential=dependency(options,'readCredential',readMailCredential);
143
- const send=dependency(options,'sendMail',sendResendMail);
93
+ async function sendMailFromReport(options){
94
+ const readReport=resolveMailCommandDependency(options,'readReport',null);
95
+ const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
96
+ const send=resolveMailCommandDependency(options,'sendMail',sendResendMail);
144
97
  throwIfAborted(options.signal);
145
98
  const report=await readReport();
146
99
  throwIfAborted(options.signal);
147
- let apiKey=await readCredential(credentialOptions(options));
100
+ let apiKey=await readCredential(mailCredentialOptions(options));
148
101
  if(apiKey===null){
149
102
  throw new ArcaneError(
150
103
  ERROR_CODES.prerequisiteMissing,
@@ -173,9 +126,11 @@ async function sendMail(options){
173
126
  signal:options.signal
174
127
  });
175
128
  if(result?.classification==='accepted'&&result.status==='accepted'){
176
- return withoutMailCredentials(result);
129
+ return result;
177
130
  }
178
- const details=completeSendFailure(result);
131
+ const details=result&&is.object(result)&&!is.array(result)
132
+ ?result
133
+ :{provider:'resend',result};
179
134
  throw new ArcaneError(
180
135
  ERROR_CODES.operationFailed,
181
136
  `Resend did not authoritatively accept the mail request (${details.classification||'unknown'}).`,
@@ -186,12 +141,11 @@ async function sendMail(options){
186
141
  }
187
142
  }
188
143
 
189
- async function serveMail(options){
190
- const readCredential=dependency(options,'readCredential',readMailCredential);
191
- const readAppKey=dependency(options,'readAppKey',null);
192
- const startServer=dependency(options,'startServer',startResendMailServer);
144
+ async function serveMailGateway(options){
145
+ const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
146
+ const startServer=resolveMailCommandDependency(options,'startServer',startResendMailServer);
193
147
  throwIfAborted(options.signal);
194
- let apiKey=await readCredential(credentialOptions(options));
148
+ let apiKey=await readCredential(mailCredentialOptions(options));
195
149
  if(apiKey===null){
196
150
  throw new ArcaneError(
197
151
  ERROR_CODES.prerequisiteMissing,
@@ -204,60 +158,46 @@ async function serveMail(options){
204
158
  'The configured Resend credential could not be read.'
205
159
  );
206
160
  }
207
- let appKey='';
208
161
  try{
209
162
  throwIfAborted(options.signal);
210
- appKey=await readAppKey();
211
- if(!is.string(appKey)||!appKey){
212
- throw new ArcaneError(
213
- ERROR_CODES.operationFailed,
214
- 'The local Mail gateway app key could not be read.'
215
- );
216
- }
217
- throwIfAborted(options.signal);
218
- const recipientAllowlist=recipientList(options.allowTo,'allowTo');
163
+ const recipientAllowlist=mailRecipientOptions(options.allowTo,'allowTo');
219
164
  const errorRecipients=options.errorTo===undefined
220
165
  ? recipientAllowlist
221
- : recipientList(options.errorTo,'errorTo');
166
+ : mailRecipientOptions(options.errorTo,'errorTo');
222
167
  return await startServer({
223
168
  apiKey,
224
- appKey,
225
169
  appId:options.appId,
226
- allowedOrigins:originList(options.origin),
227
- bodyQueueTimeoutMs:options.bodyQueueTimeoutMs,
170
+ allowedOrigins:options.origin===undefined
171
+ ?[]
172
+ :is.array(options.origin)?[...options.origin]:[options.origin],
228
173
  bodyTimeoutMs:options.bodyTimeoutMs,
229
174
  errorRecipients,
230
175
  fetchImpl:options.fetchImpl,
231
176
  from:options.from,
232
- host:options.host,
233
- maxConcurrentBodyReads:options.maxConcurrentBodyReads,
234
- maxConcurrentSends:options.maxConcurrentSends,
235
- maxQueuedBodyReads:options.maxQueuedBodyReads,
236
- maxQueuedSends:options.maxQueuedSends,
177
+ host:options.host??'0.0.0.0',
237
178
  onEvent:options.onEvent,
238
179
  port:options.port,
239
180
  providerTimeoutMs:options.requestTimeout,
240
181
  recipientAllowlist,
241
182
  requestIdFactory:options.requestIdFactory,
242
183
  retryableDelayMs:options.retryableDelayMs,
243
- sendQueueTimeoutMs:options.sendQueueTimeoutMs,
244
- signal:options.signal
184
+ signal:options.signal,
185
+ verifySubscription:options.verifySubscription
245
186
  });
246
187
  }finally{
247
188
  apiKey='';
248
- appKey='';
249
189
  }
250
190
  }
251
191
 
252
192
  export async function executeMailCommand(rawOptions={}){
253
- const options=validateOptions(rawOptions);
193
+ const options=validateMailCommandOptions(rawOptions);
254
194
  switch(options.action){
255
- case 'key-set':return setCredential(options);
256
- case 'key-status':return credentialStatus(options);
257
- case 'key-delete':return deleteCredential(options);
258
- case 'send':return sendMail(options);
259
- case 'serve':return serveMail(options);
260
- default:usage('Unsupported Mail action.');
195
+ case 'key-set':return setMailCredentialFromInput(options);
196
+ case 'key-status':return readMailCredentialStatus(options);
197
+ case 'key-delete':return deleteMailCredentialProfile(options);
198
+ case 'send':return sendMailFromReport(options);
199
+ case 'serve':return serveMailGateway(options);
200
+ default:throwMailUsageError('Unsupported Mail action.');
261
201
  }
262
202
  }
263
203